正式版1.0

This commit is contained in:
Han 2025-03-02 18:38:46 +08:00
parent 60f5296dcb
commit e7117f0903
52 changed files with 794 additions and 513 deletions

148
README.md
View File

@ -1,82 +1,80 @@
# vhAstro-Theme
一款简约的 Astro 主题
# 🍥 Astro主题 vhAstro-Theme
# Astro Starter Kit: Blog
## 🚀 vhAstro-Theme一款基于 Astro 构建的优雅的响应式博客主题
```sh
npm i pnpm -g
**「当极简主义遇上工程之美」**
在线演示 ➡️ [https://www.vvhan.com](https://www.vvhan.com)
官方文档 ➡️ [vhAstro-Theme](https://www.vvhan.com/article/astro-theme-vhastro-theme)
![Astro主题 vhAstro-Theme](https://i0.wp.com/uxiaohan.github.io/v2/2025/03/1740899552.webp)
## ✨ 功能特性
- [x] 简洁的响应式设计
- [x] 流畅的动画和页面过渡
- [x] 两列布局
- [x] 阅读时间
- [x] 字数统计
- [x] 代码块
- [x] 语法高亮
- [x] 图片懒加载
- [x] 图片灯箱
- [x] Twikoo 评论
- [x] 本地搜索
- [x] 标签
- [x] 分类
- [x] 归档
- [x] 动态
- [x] 关于
- [x] 友情链接
- [x] 推荐文章
- [x] 谷歌广告
- [x] 内置 404 页面
- [x] Sitemap 支持
- [x] RSS 支持
- [x] 活跃的社区支持
- [x] 广泛的现代框架兼容性
- [x] 高效的性能优化
- [x] 优秀的开发体验
## 🚀 使用方法
- 使用此模板生成新仓库或 Fork 此仓库
- 进行本地开发Clone 新的仓库,执行 `pnpm install` 以安装依赖
- 若未安装 pnpm执行 `npm install -g pnpm`
- 通过配置文件 `src/config.ts` 自定义博客
- 执行 pnpm newpost '文章标题' 创建新文章,并在 src/content/posts/ 目录中编辑
- 参考官方指南将博客部署至 Vercel, Netlify,Cloudflare Pages, GitHub Pages 等
- 部署前需编辑 `astro.config.mjs` 中的站点设置。
## ⚙️ 文章格式
```md
---
title: 标题
categories: 分类
tags:
- 标签1
- 标签2
id: 文章ID
date: 文章创建日期
updated: 文章更新日期
cover: "封面图URL (为空默认随机内置封面 /public/assets/images/banner)"
recommend: false # 是否推荐文章
hide: false # 是否隐藏文章
---
```
```sh
## 💻 命令
```bash
# 安装依赖
pnpm i
```
```sh
# 启动开发环境
pnpm install
# 本地开发
pnpm dev
```
```sh
# 编译
# 构建静态文件
pnpm build
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![blog](https://github.com/withastro/astro/assets/2244813/ff10799f-a816-4703-b967-c78997e8323d)
Features:
- ✅ Minimal styling (make it your own!)
- ✅ 100/100 Lighthouse performance
- ✅ SEO-friendly with canonical URLs and OpenGraph data
- ✅ Sitemap support
- ✅ RSS Feed support
- ✅ Markdown & MDX support
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
├── public/
├── src/
│   ├── components/
│   ├── content/
│   ├── layouts/
│   └── pages/
├── astro.config.mjs
├── README.md
├── package.json
└── tsconfig.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
The `src/content/` directory contains "collections" of related Markdown and MDX documents. Use `getCollection()` to retrieve posts from `src/content/blog/`, and type-check your frontmatter using an optional schema. See [Astro's Content Collections docs](https://docs.astro.build/en/guides/content-collections/) to learn more.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
## Credit
This theme is based off of the lovely [Bear Blog](https://github.com/HermanMartinus/bearblog/).
# 创建新文章
pnpm newpost '文章标题'
```

View File

@ -4,17 +4,32 @@ import sitemap from '@astrojs/sitemap';
import { defineConfig } from 'astro/config';
import remarkDirective from "remark-directive"; /* Handle directives */
import { remarkNote, addClassNames } from './src/plugins/markdown.formate'
import swup from '@swup/astro';
// https://astro.build/config
export default defineConfig({
site: 'https://www.vvhan.com',
integrations: [mdx(), sitemap({
changefreq: 'weekly',
priority: 0.7,
lastmod: new Date(),
// 处理末尾带 / 的 url
serialize: (item) => ({ ...item, url: item.url.endsWith('/') ? item.url.slice(0, -1) : item.url })
})],
integrations: [
swup({
theme: false,
animationClass: "vh-animation-",
containers: [".vh-animation"],
smoothScrolling: true,
progress: true,
cache: true,
preload: true,
accessibility: true,
updateHead: true,
updateBodyClass: false,
globalInstance: true
}),
sitemap({
changefreq: 'weekly',
priority: 0.7,
lastmod: new Date(),
// 处理末尾带 / 的 url
serialize: (item) => ({ ...item, url: item.url.endsWith('/') ? item.url.slice(0, -1) : item.url })
}),
mdx()],
markdown: {
rehypePlugins: [addClassNames],
remarkPlugins: [remarkDirective, remarkNote],

View File

@ -10,16 +10,18 @@
"newpost": "node ./script/newpost.js"
},
"dependencies": {
"@astrojs/mdx": "^4.0.8",
"@astrojs/mdx": "^4.1.0",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1",
"astro": "^5.3.0",
"@swup/astro": "^1.5.0",
"astro": "^5.4.1",
"dayjs": "^1.11.13",
"dplayer": "^1.27.1",
"hls.js": "^1.5.20",
"nprogress": "^0.2.0",
"overlayscrollbars": "^2.11.0",
"remark-directive": "^3.0.1",
"mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.11.1",
"reading-time": "^1.5.0",
"remark-directive": "^4.0.0",
"twikoo": "1.6.41",
"unist-util-visit": "^5.0.0",
"vanilla-lazyload": "^19.1.3",
@ -34,7 +36,8 @@
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"sharp"
"sharp",
"swup"
]
}
}

View File

@ -1,4 +1,3 @@
import path from 'path';
import dayjs from 'dayjs';
import crypto from 'crypto';
@ -8,13 +7,13 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 获取命令行参数
const articleName = process.argv.slice(2).join('');
const articleID = crypto.createHash('md5').update(String(dayjs().valueOf())).digest('hex');
const articleID = crypto.createHash('sha256').update(dayjs().valueOf().toString()).digest('hex').slice(0, 16);
if (!articleName) {
console.error('请提供文章名称例如pnpm newpost "第一篇文章"');
process.exit(1);
}
const ArticleContent = `---
title: ${articleName}
title: "${articleName.replace(/"/g, '\\"')}"
categories: 分类
tags:
- 标签
@ -34,8 +33,22 @@ const init = async () => {
// 写文件
const now = dayjs();
const targetDir = path.join(__dirname, '../src/content/blog', `${now.year()}/${now.format('MM')}`);
await fs.mkdir(path.dirname(targetDir), { recursive: true });
await fs.writeFile(path.join(targetDir, `${articleName}.md`), ArticleContent, 'utf8');
console.log(`文章 ${articleName} 已创建`);
try {
await fs.mkdir(targetDir, { recursive: true });
await fs.writeFile(path.join(targetDir, `${articleName}.md`), ArticleContent, 'utf8');
const filePath = path.join(targetDir, `${articleName}.md`);
await fs.writeFile(path.join(targetDir, `${articleName}.md`), ArticleContent, 'utf8');
// 友好输出
console.log('✅ 文章创建成功');
console.log(`📅 日期:${now.format('YYYY-MM-DD')}`);
console.log(`📂 路径:${filePath}`);
console.log(`🆔 ID${articleID.slice(0, 16)} (可手动修改)`);
} catch (error) {
// 增强错误处理
console.error('❌ 创建失败:');
console.error(`错误类型:${error.code || 'UNKNOWN_ERROR'}`);
console.error(`详细信息:${error.message}`);
process.exit(1);
}
}
init();

View File

@ -5,7 +5,7 @@ const { articleList } = Astro.props;
import "../styles/Archive.less";
---
<section class="vh-animation vh-archive-main">
<section class="vh-archive-main vh-animation vh-animation-init">
<div class="archive-list">
{
articleList.map((i: any) => (
@ -13,7 +13,7 @@ import "../styles/Archive.less";
<p class="title">
<em>{i.name}</em>
<i />
<span>{i.data.length} total</span>
<span>{i.data.length}篇文章</span>
</p>
{i.data.map((_: any) => (
<a href={`/article/${_.id}`}>

View File

@ -11,7 +11,7 @@ const ARTICLE_COVER: string = await getCover(post.data.cover);
import "../styles/components/ArticleCard.less";
---
<article class="vh-article-item vh-animation" style={`animation-delay:calc(var(--vh-animation-delay) + ${index * 50}ms)`}>
<article class="vh-article-item vh-animation vh-animation-init">
<a class="vh-article-link" href={`/article/${post.data.id}`}>
<section class="vh-article-banner"><Image src="/assets/images/lazy-loading.webp" data-vh-lz-src={ARTICLE_COVER} alt={post.data.title} width="10" height="10" /></section>
<section class="vh-article-desc">

View File

@ -1,8 +1,11 @@
---
// 静态图片
import { Image } from "astro:assets";
// 时间处理
import { fmtTime } from "../utils/index";
// 获取用户配置数据
import SITE_CONFIG from "../config";
import { fmtTime } from "../utils/index";
const { Avatar, Author, Motto, WebSites } = SITE_CONFIG;
const { Avatar, Author, Motto, WebSites, GoogleAds } = SITE_CONFIG;
// 获取文章数据
import { getCategories, getTags, getRecommendArticles } from "../utils/getPostInfo";
// 分类列表
@ -11,8 +14,8 @@ const categories = getCategories();
const tags = getTags();
// 最新文章
const recommendArticles = getRecommendArticles();
// 静态图片
import { Image } from "astro:assets";
// Google 广告组件
import GoogleAd from "../components/GoogleAd.astro";
// 侧边栏样式
import "../styles/components/Aside.less";
---
@ -20,10 +23,7 @@ import "../styles/components/Aside.less";
<aside class="vh-aside">
<!-- 头像块 -->
<section class="vh-aside-item user">
<div onclick="window.history.back()" class="vh-aside-avatar">
<Image class="vh-aside-avatar" src={Avatar} alt={Author} width="1" height="1" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path d="M512 256A256 256 0 1 0 0 256a256 256 0 1 0 512 0zM116.7 244.7l112-112c4.6-4.6 11.5-5.9 17.4-3.5s9.9 8.3 9.9 14.8l0 64 96 0c17.7 0 32 14.3 32 32l0 32c0 17.7-14.3 32-32 32l-96 0 0 64c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5l-112-112c-6.2-6.2-6.2-16.4 0-22.6z"></path></svg>
</div>
<Image class="vh-aside-avatar" src="/assets/images/lazy-loading.webp" data-vh-lz-src={Avatar} alt={Author} width="1" height="1" />
<span class="vh-aside-auther">{Author}</span>
<p class="vh-aside-motto">{Motto}</p>
<section class="vh-aside-links">
@ -60,7 +60,7 @@ import "../styles/components/Aside.less";
<div class="vh-aside-tags">
{
tags.map(i => (
<a href={`/tags/${i}`}>
<a href={`/tag/${i}`}>
<span>{i}</span>
</a>
))
@ -68,23 +68,37 @@ import "../styles/components/Aside.less";
</div>
</section>
<!-- 最新文章块 -->
<section class="vh-aside-item articles">
<h3>推荐文章</h3>
<div class="vh-aside-articles">
{
recommendArticles.map(async i => (
<a href={`/article/${(await i).id}`}>
<p class="cover">
<Image src={(await i).cover} alt={(await i).title} width="1" height="1" />
</p>
<p class="info">
<span>{(await i).title}</span>
<time>{fmtTime((await i).date, "YYYY-MM-DD A")}</time>
</p>
</a>
))
}
</div>
<section class="sticky-aside">
<!-- 最新文章块 -->
{
recommendArticles.length && (
<section class="vh-aside-item articles">
<h3>推荐文章</h3>
<div class="vh-aside-articles">
{recommendArticles.map(async i => (
<a href={`/article/${(await i).id}`}>
<p class="cover">
<Image src="/assets/images/lazy-loading.webp" data-vh-lz-src={(await i).cover} alt={(await i).title} width="1" height="1" />
</p>
<p class="info">
<span>{(await i).title}</span>
<time>{fmtTime((await i).date, "YYYY-MM-DD A")}</time>
</p>
</a>
))}
</div>
</section>
)
}
<!-- 谷歌广告块 -->
{
GoogleAds.ad_Client && GoogleAds.asideAD_Slot && (
<section class="vh-aside-item ad">
<h3>广而告之</h3>
<GoogleAd className="vh-aside-ad" slotID={GoogleAds.asideAD_Slot} />
</section>
)
}
</section>
</aside>

View File

@ -14,10 +14,4 @@ import "../styles/components/BackTop.less";
<circle stroke="url(#Gradient)" cx="12" cy="12" r="10" fill="none" stroke-width="2" stroke-linecap="round" transform="rotate(-90, 12, 12)"></circle>
</svg>
<svg class="icon" viewBox="0 0 24 24"><path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m6 15l6-6l6 6"></path></svg>
</section>
<script>
import updateRouter from "../utils/updateRouter";
import BackTopInitFn from "../scripts/BackTop";
// 初始化BackTop组件
updateRouter("afterMount", BackTopInitFn);
</script>
</section>

View File

@ -3,16 +3,3 @@ import "../styles/components/Comment.less";
---
<section class="vh-comment"><section></section></section>
<script>
import updateRouter from "../utils/updateRouter";
import { LoadScript } from "../utils/index";
import SITE_INFO from "../config";
declare const twikoo: any;
// 初始化评论插件
SITE_INFO.Twikoo.envId &&
updateRouter("afterMount", async () => {
if (!document.querySelector(".vh-comment>section")) return;
await LoadScript("https://registry.npmmirror.com/twikoo/1.6.41/files/dist/twikoo.all.min.js");
twikoo.init({ envId: SITE_INFO.Twikoo.envId, el: ".vh-comment>section" });
});
</script>

View File

@ -11,15 +11,3 @@ import "../styles/Footer.less";
<p><a href="https://astro.build/" target="_blank" rel="noopener noreferrer"><Image width="1" height="1" alt="Astro" src="/assets/images/footer/astro.svg" /></a><a href="https://github.com/uxiaohan/vhAstro-Theme" target="_blank" rel="noopener noreferrer"><Image width="1" height="1" alt="vhAstro-Theme" src="/assets/images/footer/theme.svg" /></a><a href="/sitemap-index.xml" target="_blank"><Image width="1" height="1" alt="sitemap" src="/assets/images/footer/sitemap.svg" /></a><a href="/rss.xml" target="_blank"><Image width="1" height="1" alt="rss" src="/assets/images/footer/rss.svg" /></a><a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer"><Image width="1" height="1" alt="icp" src="/assets/images/footer/icp.svg" /></a></p>
</main>
</footer>
<script>
import updateRouter from "../utils/updateRouter";
// 格式化时间
import { fmtDate } from "../utils/index";
// 页面内容的元数据
import SITE_CONFIG from "../config";
const { CreateTime } = SITE_CONFIG;
// 初始化 网站运行时间
updateRouter("afterMount", () => {
document.querySelector("em.web_time")!.textContent = fmtDate(CreateTime);
});
</script>

View File

@ -0,0 +1,5 @@
---
const { slotID, className } = Astro.props;
---
<div class={`vh-ad ${className}`} set:html={slotID} />

View File

@ -1,6 +1,4 @@
---
// 导航高亮
const { activeNav } = Astro.props;
import SITE_CONFIG from "../config";
const { Navs } = SITE_CONFIG;
// 原生图片
@ -17,10 +15,10 @@ import "../styles/components/Header.less";
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"> <path d="M543.8 287.6c17 0 32-14 32-32.1c1-9-3-17-11-24L309.5 7c-6-5-14-7-21-7s-15 1-22 8L10 231.5c-7 7-10 15-10 24c0 18 14 32.1 32 32.1l32 0 0 160.4c0 35.3 28.7 64 64 64l102.3 0-31.3-52.2c-4.1-6.8-2.6-15.5 3.5-20.5L288 368l-60.2-82.8c-10.9-15 8.2-33.5 22.8-22l117.9 92.6c8 6.3 8.2 18.4 .4 24.9L288 448l38.4 64 122.1 0c35.5 0 64.2-28.8 64-64.3l-.7-160.2 32 0z"></path></svg>
Home
</a>
<div class="link-list">
<div class="link-list vh-link-list">
{
Navs.map(i => (
<a class={i.link.includes(activeNav) ? "active" : ""} href={i.link} target={i.target ? "_blank" : "_self"}>
<a class={i.link.replace("/", "")} href={i.link} target={i.target ? "_blank" : "_self"}>
{i.text}
<Image src={i.icon} alt={i.text} width="10" height="10" />
</a>

View File

@ -1,6 +1,4 @@
---
// 导航高亮
const { activeNav } = Astro.props;
import SITE_CONFIG from "../config";
const { Navs, Title } = SITE_CONFIG;
// 侧边栏 MobileSidebar 样式
@ -13,10 +11,10 @@ import "../styles/components/MobileSidebar.less";
<h3>{Title}</h3>
</div>
<div class="vh-mobilesidebar-list">
<div class="vh-mobilesidebar-list vh-link-list">
{
Navs.map(i => (
<a class={i.link.includes(activeNav) ? "active" : ""} href={i.link} target={i.target ? "_blank" : "_self"}>
<a class={i.link.replace("/", "")} href={i.link} target={i.target ? "_blank" : "_self"}>
<object data={i.icon} type="image/svg+xml" />
{i.text}
</a>
@ -25,18 +23,3 @@ import "../styles/components/MobileSidebar.less";
</div>
</section>
</nav>
<script>
import updateRouter from "../utils/updateRouter";
// 初始化搜索框
const vhSearchInit = () => {
const menuDOM: any = document.querySelector(".vh-header>.main>.nav-btn>span.menu-btn");
const mobileSidebarDOM: any = document.querySelector("body>.vh-mobilesidebar");
const addActive = () => setTimeout(() => mobileSidebarDOM.classList.add("active"));
const removeActive = () => setTimeout(() => mobileSidebarDOM.classList.remove("active"));
menuDOM.addEventListener("click", addActive);
mobileSidebarDOM.addEventListener("click", removeActive);
};
// 初始化
updateRouter("afterMount", vhSearchInit);
</script>

View File

@ -11,25 +11,4 @@ import "../styles/components/Search.less";
</div>
<section class="vh-search-list"></section>
</main>
</section>
<script>
import updateRouter from "../utils/updateRouter";
import { searchInputChange } from "../scripts/Search";
// 初始化搜索框
const vhSearchInit = () => {
const searchDOM: any = document.querySelector(".vh-header>.main>.nav-btn>span.search-btn");
const searchMainDOM: any = document.querySelector(".vh-header>.main>.vh-search>main");
const searchListDOM: any = document.querySelector(".vh-header>.main>.vh-search");
const addActive = () => setTimeout(() => searchListDOM.classList.add("active"));
const removeActive = () => setTimeout(() => searchListDOM.classList.remove("active"));
// 禁止默认事件
searchMainDOM.addEventListener("click", (e: Event) => e.stopPropagation());
searchDOM.addEventListener("click", addActive);
searchListDOM.addEventListener("click", removeActive);
// 搜索框初内容变化
searchListDOM.querySelector(".search-input>input").addEventListener("input", searchInputChange);
};
// 初始化
updateRouter("afterMount", vhSearchInit);
</script>
</section>

View File

@ -40,5 +40,13 @@ export default {
// 评论组件 Twikoo
Twikoo: { envId: 'https://twikoo.vvhan.com/.netlify/functions/twikoo' },
// Han Analytics 统计https://github.com/uxiaohan/HanAnalytics
HanAnalytics: { enable: true, server: 'https://analytics.vvhan.com', siteId: 'Hello-HanHexoBlog' }
HanAnalytics: { enable: true, server: 'https://analytics.vvhan.com', siteId: 'Hello-HanHexoBlog' },
// Google 广告
GoogleAds: {
ad_Client: 'ca-pub-3037456000720711',
// 侧边栏广告(不填不开启)
asideAD_Slot: `<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-3037456000720711" data-ad-slot="6102098907" data-ad-format="auto" data-full-width-responsive="true"></ins>`,
// 文章页广告(不填不开启)
articleAD_Slot: `<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-3037456000720711" data-ad-slot="8753809833" data-ad-format="auto" data-full-width-responsive="true"></ins>`
},
}

View File

@ -13,7 +13,8 @@ const blog = defineCollection({
tags: z.array(z.union([z.string(), z.number()])),
id: z.union([z.string(), z.number()]),
cover: z.string().optional(),
recommend: z.boolean().optional()
recommend: z.boolean().optional(),
hide: z.boolean().optional()
}),
});

View File

@ -1,6 +1,9 @@
---
import { ClientRouter } from "astro:transitions";
const { title, keywords, description, pagecover, activeNav } = Astro.props;
const { title, keywords, description, pagecover } = Astro.props;
// 网站配置
import SITE_INFO from "../config";
const { GoogleAds, Twikoo } = SITE_INFO;
const { ad_Client, asideAD_Slot, articleAD_Slot } = GoogleAds;
// Head 依赖
import Head from "../components/Head.astro";
// 顶部 Header
@ -16,44 +19,24 @@ import "../styles/Layout.less";
---
<html lang="zh-CN">
<Head Title={title} Keywords={keywords} Description={description} PageCover={pagecover} />
<ClientRouter />
<Head Title={title} Keywords={keywords} Description={description} PageCover={pagecover}>
<!-- TwikooJS 加载项 -->
{Twikoo.envId && <script is:inline async src={`https://registry.npmmirror.com/twikoo/1.6.41/files/dist/twikoo.all.min.js`} />}
<!-- 谷歌广告JS加载项 -->
{ad_Client && (asideAD_Slot || articleAD_Slot) && <script is:inline async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${ad_Client}`} crossorigin="anonymous" />}
</Head>
<body>
<MobileSidebar activeNav={activeNav} />
<Header activeNav={activeNav} />
<MobileSidebar />
<Header />
<main class="vh-main">
<slot />
</main>
<Footer />
<BackTop />
<script>
import updateRouter from "../utils/updateRouter";
// 搜索
import { searchFn } from "../scripts/Search";
// 图片懒加载
import vhLzImgInit from "../scripts/vhLazyImg";
// 加载进度条
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({ easing: "ease", speed: 500, showSpinner: false, trickleSpeed: 200, minimum: 0.3 });
// Han Analytics 统计
import SITE_INFO from "../config";
const { HanAnalytics } = SITE_INFO;
import { LoadScript } from "../utils/index";
// 初始化 图片懒加载初
updateRouter("afterMount", () => {
// 预加载搜索数据
searchFn("");
// 图片懒加载初始化
vhLzImgInit();
// 进入页面,进度条结束
NProgress.done();
// Han Analytics 统计
HanAnalytics.enable && LoadScript(`${HanAnalytics.server}/tracker.min.js`, [{ k: "data-website-id", v: HanAnalytics.siteId }]);
});
// 离开页面,进度条开始
updateRouter("beforeCreate", NProgress.start);
import InitFn from "../scripts/Init";
// 全局初始化
InitFn();
</script>
</body>
</html>

View File

@ -9,7 +9,7 @@ import "../styles/Article.less";
<Layout title="404 Not Found" keywords={[404]} description="404 Not Found">
<section class="vh-container">
<article class="vh-animation vh-article-main">
<article class="vh-article-main vh-animation vh-animation-init">
<header>
<h1 class="error-title">404 Not Found</h1>
</header>

View File

@ -8,7 +8,11 @@ export async function getStaticPaths(options: GetStaticPathsOptions) {
// 生成 Search JSON 文件 ======
await setSearchJson(posts);
// 生成 Search JSON 文件 ======
return paginate(posts, { pageSize: 15 });
// 隐藏的文章不显示
return paginate(
posts.filter(i => !i.data.hide),
{ pageSize: 15 }
);
}
const { page } = Astro.props;
@ -27,9 +31,9 @@ import Pagination from "../components/Pagination.astro";
const currentPage = page_data.url.current.replace("/", "");
---
<Layout title={currentPage ? `第${currentPage}页` : ""} description={Description}>
<Layout title={currentPage ? `第${currentPage}页文章` : ""} description={Description}>
<section class="vh-container">
<section class="vh-animation article-list">
<section class="article-list vh-animation vh-animation-init">
<!-- 文章列表 -->
{data.map((post, index) => <ArticleCard post={post} index={index} />)}
<!-- 分页 -->

View File

@ -14,9 +14,9 @@ import Comment from "../../components/Comment.astro";
import "../../styles/About.less";
---
<Layout title="关于" description={Description} activeNav="about">
<Layout title="关于" description={Description}>
<section class="vh-container">
<section class="vh-about">
<section class="vh-about vh-animation vh-animation-init">
<header class="vh-page-header">
<h1>关于我</h1>
<p>Hi there, Im Han 👋</p>

View File

@ -13,7 +13,7 @@ import Aside from "../../components/Aside.astro";
import Archive from "../../components/Archive.astro";
---
<Layout title="归档" description={Description} activeNav="archives">
<Layout title="归档" description={Description}>
<section class="vh-container">
<Archive articleList={articleList} />
<Aside />

View File

@ -14,10 +14,12 @@ import getCover from "../../utils/getCover";
const ARTICLE_COVER: string = await getCover(post.data.cover);
// 页面 Info
import SITE_CONFIG from "../../config";
const { Site, Title, Author, Twikoo } = SITE_CONFIG;
const { Site, Title, Author, Twikoo, GoogleAds } = SITE_CONFIG;
// 处理文章内容
const description = getDescription(post);
const { Content } = await render(post);
const { Content, remarkPluginFrontmatter } = await render(post);
// 文章字数和阅读时间
const { reading_time, article_word_count } = remarkPluginFrontmatter;
// 公共 Layout
import Layout from "../../layouts/Layout.astro";
// Aside组件
@ -26,19 +28,23 @@ import Aside from "../../components/Aside.astro";
import Copyright from "../../components/Copyright.astro";
// 评论组件
import Comment from "../../components/Comment.astro";
// Google 广告组件
import GoogleAd from "../../components/GoogleAd.astro";
// 文章页面样式
import "../../styles/Article.less";
---
<Layout title={post.data.title} keywords={post.data.tags} description={description} pagecover={ARTICLE_COVER}>
<section class="vh-container">
<article class="vh-animation vh-article-main">
<article class="vh-article-main vh-animation vh-animation-init">
<header>
<h1>{post.data.title}</h1>
<div class="article-meta">
<span class="article-meta-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <path d="M152 24c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40L64 64C28.7 64 0 92.7 0 128l0 16 0 48L0 448c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-256 0-48 0-16c0-35.3-28.7-64-64-64l-40 0 0-40c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40L152 64l0-40zM48 192l80 0 0 56-80 0 0-56zm0 104l80 0 0 64-80 0 0-64zm128 0l96 0 0 64-96 0 0-64zm144 0l80 0 0 64-80 0 0-64zm80-48l-80 0 0-56 80 0 0 56zm0 160l0 40c0 8.8-7.2 16-16 16l-64 0 0-56 80 0zm-128 0l0 56-96 0 0-56 96 0zm-144 0l0 56-64 0c-8.8 0-16-7.2-16-16l0-40 80 0zM272 248l-96 0 0-56 96 0 0 56z"></path></svg>
<time>{fmtTime(post.data.date, "YYYY-MM-DD A")}</time>
<span class="count"><strong>{article_word_count}</strong>字</span>
<span class="time"><strong>{parseFloat((Number(reading_time) || 0).toFixed(1).replace(/\.0+$/, ""))}</strong>分钟</span>
</span>
<a class="article-meta-item" href={`/categories/${post.data.categories}`}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <path d="M40 48C26.7 48 16 58.7 16 72l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24L40 48zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 64zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zM16 232l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24l-48 0c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24l0 48c0 13.3 10.7 24 24 24l48 0c13.3 0 24-10.7 24-24l0-48c0-13.3-10.7-24-24-24l-48 0z"></path></svg>
@ -49,41 +55,16 @@ import "../../styles/Article.less";
<main>
<Content />
<nav class="tag-list">
{post.data.tags.map((i: any) => <a href={`/tags/${i}`}>{i}</a>)}
{post.data.tags.map((i: any) => <a href={`/tag/${i}`}>{i}</a>)}
</nav>
</main>
<footer>
<!-- 底部谷歌广告 -->
{GoogleAds.ad_Client && GoogleAds.articleAD_Slot && <GoogleAd className="vh-article-ad" slotID={GoogleAds.articleAD_Slot} />}
<Copyright site={Site} id={post.data.id} title={post.data.title} sitename={Title} time={fmtTime(post.data.date, "YYYY-MM-DD A")} auther={Author} />
</footer>
{Twikoo.envId && <Comment envId={Twikoo.envId} />}
</article>
<Aside />
</section>
<script>
import updateRouter from "../../utils/updateRouter";
// 初始化文章功能脚本
import ArticleInit from "../../scripts/Article";
// 初始化视频播放器
import videoInit from "../../scripts/Video";
// 初始化音乐播放器
import musicInit from "../../scripts/Music";
// 进入页面时初始化
const videoList: any[] = [];
const MusicList: any[] = [];
updateRouter("afterMount", () => {
ArticleInit();
videoInit(videoList);
musicInit(MusicList);
});
// 页面离开卸载播放器
updateRouter("beforeCreate", () => {
// 销毁播放器
videoList.forEach((i: any) => i.destroy());
videoList.length = 0;
// 销毁音乐
MusicList.forEach((i: any) => i.destroy());
MusicList.length = 0;
});
</script>
</Layout>

View File

@ -19,7 +19,7 @@ import Aside from "../../components/Aside.astro";
import Archive from "../../components/Archive.astro";
---
<Layout title={`分类 ${categories}`} description={Description}>
<Layout title={`分类 ${categories} 下的文章`} description={Description}>
<section class="vh-container">
<Archive articleList={articleList} />
<Aside />

View File

@ -12,26 +12,16 @@ import Comment from "../../components/Comment.astro";
import "../../styles/Links.less";
---
<Layout title="友情链接" description={Description} activeNav="links">
<Layout title="友情链接" description={Description}>
<section class="vh-container">
<section class="vh-links">
<section class="vh-links vh-animation vh-animation-init">
<header class="vh-page-header">
<h1>朋友圈 👭</h1>
<p>天下快意之事莫若友。</p>
</header>
<main></main>
<main><section class="vh-space-loading"><span></span><span></span><span></span></section></main>
{Twikoo.envId && <Comment envId={Twikoo.envId} />}
</section>
<Aside />
</section>
<script>
import updateRouter from "../../utils/updateRouter";
import LinksInit from "../../scripts/Links";
// 进入页面时初始化
// 数据源
import LINKS_DATA from "../../page_data/Link";
const { api, data } = LINKS_DATA;
updateRouter("afterMount", () => LinksInit(api || data));
</script>
</Layout>

View File

@ -10,9 +10,9 @@ import Layout from "../../layouts/Layout.astro";
import Comment from "../../components/Comment.astro";
---
<Layout title="留言" description={Description} activeNav="message">
<Layout title="留言" description={Description}>
<section class="vh-container">
<section class="vh-message" style="gap:1.25rem;">
<section class="vh-message vh-animation vh-animation-init" style="gap:1.25rem;">
<header class="vh-page-header">
<h1>留言板 🌸</h1>
<p>快友之事莫若谈。</p>
@ -21,11 +21,4 @@ import Comment from "../../components/Comment.astro";
</section>
<Aside />
</section>
<script>
import updateRouter from "../../utils/updateRouter";
import LinksInit from "../../scripts/Links";
// 进入页面时初始化
updateRouter("afterMount", LinksInit);
</script>
</Layout>

View File

@ -11,7 +11,7 @@ export async function GET(context: any) {
title: Title,
description: Description,
site: context.site,
items: posts.map((post) => ({
items: posts.filter(i => !i.data.hide).map((post) => ({
title: post.data.title,
pubDate: post.data.updated || post.data.date,
description: getDescription(post),

View File

@ -21,7 +21,7 @@ import Aside from "../../components/Aside.astro";
import Archive from "../../components/Archive.astro";
---
<Layout title={`标签 ${tags}`} description={Description}>
<Layout title={`标签 ${tags} 下的文章`} description={Description}>
<section class="vh-container">
<Archive articleList={articleList} />
<Aside />

View File

@ -12,26 +12,16 @@ import Comment from "../../components/Comment.astro";
import "../../styles/Talking.less";
---
<Layout title="动态" description={Description} activeNav="talking">
<Layout title="动态" description={Description}>
<section class="vh-container">
<section class="vh-talking">
<section class="vh-talking vh-animation vh-animation-init">
<header class="vh-page-header">
<h1>动态 🥫</h1>
<p>记录美好生活.</p>
</header>
<main></main>
<main><section class="vh-space-loading white"><span></span><span></span><span></span></section></main>
{Twikoo.envId && <Comment envId={Twikoo.envId} />}
</section>
<Aside />
</section>
<script>
import updateRouter from "../../utils/updateRouter";
import TalkingInit from "../../scripts/Talking";
// 进入页面时初始化
// 数据源
import TALKING_DATA from "../../page_data/Talking";
const { api, data } = TALKING_DATA;
updateRouter("afterMount", () => TalkingInit(api || data));
</script>
</Layout>

View File

@ -1,15 +1,17 @@
// src/plugins/remark-note.js
import { visit } from 'unist-util-visit';
import getReadingTime from 'reading-time';
import { toString } from 'mdast-util-to-string';
// 处理函数
const nodeTreeFmt = (node: any, parent: any) => {
if (node.type === 'text') parent.children = node.value.split('\n').map((i: string) => ({ type: 'paragraph', children: [{ type: 'text', value: i }] }));
if (node.children && node.children.length > 0) node.children.forEach((child: any) => nodeTreeFmt(child, node));
if (node.children && node.children.length) node.children.forEach((child: any) => nodeTreeFmt(child, node));
}
// 处理标签
const remarkNote = () => {
return (tree: any) => {
return (tree: any, { data: astroData }: any) => {
visit(tree, (node, index, parent) => {
const { type, name, attributes } = node;
// 处理组件
@ -35,6 +37,11 @@ const remarkNote = () => {
};
// 设置 class
hProperties.class = `vh-node vh-${name}${attributes.type ? ` ${name}-${attributes.type}` : ''}`;
// 文章字数统计
const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage);
astroData.astro.frontmatter.reading_time = readingTime.minutes
astroData.astro.frontmatter.article_word_count = readingTime.words
}
});
};

View File

@ -9,7 +9,9 @@ import { OverlayScrollbars } from "overlayscrollbars";
// Pre Code 代码复制功能======
let copyText = null;
// Pre Code 代码复制功能======
const ArticleInit = () => {
// 初始化
export default () => {
// 灯箱JS初始化======
ViewImage && ViewImage.init("main>.vh-container>article.vh-article-main img.vh-article-img");
// 灯箱JS初始化======
@ -34,5 +36,4 @@ const ArticleInit = () => {
});
});
// Pre Code 代码复制功能======
}
export default ArticleInit;
}

View File

@ -19,7 +19,9 @@ let backTop: any = document.querySelector(".vh-back-top");
// 彩虹圈圈 DOM
let circle: any = document.querySelector(".vh-back-top>svg>circle");
const circumference = 2 * Math.PI * 10;
const BackTopInitFn = () => {
// 初始化
export default () => {
// 更新 彩虹圈圈 DOM
circle = document.querySelector(".vh-back-top>svg>circle");
// 更新 回顶部DOM
@ -38,5 +40,3 @@ const BackTopInitFn = () => {
// 触发 scrollChangeFn
scrollChangeFn();
};
export default BackTopInitFn;

8
src/scripts/Comment.ts Normal file
View File

@ -0,0 +1,8 @@
import SITE_INFO from "../config";
declare const twikoo: any;
// 初始化评论插件
export default async () => {
const commentDOM = '.vh-comment>section'
if (!document.querySelector(commentDOM) || !SITE_INFO.Twikoo.envId) return;
twikoo.init({ envId: SITE_INFO.Twikoo.envId, el: commentDOM })
}

7
src/scripts/Footer.ts Normal file
View File

@ -0,0 +1,7 @@
// 格式化时间
import { fmtDate } from "../utils/index";
// 页面内容的元数据
import SITE_CONFIG from "../config";
const { CreateTime } = SITE_CONFIG;
// 初始化 网站运行时间
export default () => (document.querySelector("em.web_time")!.textContent = fmtDate(CreateTime))

24
src/scripts/GoogleAd.ts Normal file
View File

@ -0,0 +1,24 @@
// 声明全局变量 adsbygoogle
declare global {
interface Window {
adsbygoogle: any[];
}
}
import SITE_INFO from '../config'
const { GoogleAds } = SITE_INFO
export default () => {
const asideAD: any = document.querySelector('.vh-aside-ad')
const articleAD: any = document.querySelector('.vh-article-ad')
if (!asideAD && !articleAD) return;
// 初始化侧边栏广告
if (asideAD) {
asideAD.innerHTML = GoogleAds.asideAD_Slot;
(window.adsbygoogle = window.adsbygoogle || []).push({})
}
// 初始化文章页广告
if (articleAD) {
articleAD.innerHTML = GoogleAds.articleAD_Slot;
(window.adsbygoogle = window.adsbygoogle || []).push({})
}
}

11
src/scripts/Header.ts Normal file
View File

@ -0,0 +1,11 @@
const linkListArr = ['links', 'talking', 'archives', 'message', 'about']
export default () => {
const linkARR = document.querySelectorAll('.vh-link-list>a');
if (!linkARR.length) return;
linkARR.forEach((i: any) => {
i.classList.remove('active');
const linkName = (window.location.pathname).split('/')[1]
if (!linkListArr.includes(linkName)) return;
document.querySelectorAll(`.${linkName}`).forEach((i: any) => i.classList.add('active'));
})
}

91
src/scripts/Init.ts Normal file
View File

@ -0,0 +1,91 @@
import { inRouter, outRouter } from "../utils/updateRouter";
// 初始化文章功能脚本
import ArticleInit from "../scripts/Article";
// 初始化视频播放器
import videoInit from "../scripts/Video";
// 初始化音乐播放器
import musicInit from "../scripts/Music";
// 初始化BackTop组件
import BackTopInitFn from "../scripts/BackTop";
// 搜索
import { searchFn, vhSearchInit } from "../scripts/Search";
// 图片懒加载
import vhLzImgInit from "../scripts/vhLazyImg";
// 顶部导航 Current 状态
import initLinkCurrent from "../scripts/Header";
// 底部网站运行时间
import initWebSiteTime from "../scripts/Footer";
// 友情链接初始化
import initLinks from "../scripts/Links";
// 动态说说初始化
import initTalking from "../scripts/Talking";
// 文章评论初始化
import initComment from "../scripts/Comment";
// 移动端侧边栏初始化
import initMobileSidebar from "../scripts/MobileSidebar";
// Google 广告
import GoogleAdInit from "../scripts/GoogleAd";
// Han Analytics 统计
import SITE_INFO from "../config";
const { HanAnalytics } = SITE_INFO;
import { LoadScript } from "../utils/index";
// ============================================================
// 页面初始化 Only
const videoList: any[] = [];
const MusicList: any[] = [];
const indexInit = async (only: boolean = true) => {
// 预加载搜索数据
only && searchFn("");
// 初始化搜索功能
only && vhSearchInit();
// 初始化网站运行时间
only && initWebSiteTime();
// 初始化BackTop组件
only && BackTopInitFn();
// 移动端侧边栏初始化
only && initMobileSidebar();
// 顶部导航 Current 状态
initLinkCurrent()
// 初始化文章功能脚本
ArticleInit();
// 图片懒加载初始化
vhLzImgInit();
// 友情链接初始化
initLinks();
// 动态说说初始化
initTalking();
// 文章评论初始化
initComment();
// Google 广告
GoogleAdInit();
// 文章视频播放器初始化
videoInit(videoList);
// 文章音乐播放器初始化
musicInit(MusicList);
// Han Analytics 统计
HanAnalytics.enable && LoadScript(`${HanAnalytics.server}/tracker.min.js`, [{ k: "data-website-id", v: HanAnalytics.siteId }]);
};
export default () => {
// 首次初始化
indexInit();
// 进入页面时触发
inRouter(() => indexInit(false));
// 离开当前页面时触发
outRouter(() => {
// 销毁播放器
videoList.forEach((i: any) => i.destroy());
videoList.length = 0;
// 销毁音乐
MusicList.forEach((i: any) => i.destroy());
MusicList.length = 0;
});
console.log(
"%c\u4E3B\u9898\uFF1AvhAstro-Theme%c https://github.com/uxiaohan/vhAstro-Theme ",
"background: linear-gradient(90deg, #030307, #1a1a2e); color: #fadfa3; padding: 4px;",
"background: #EDEDED; padding: 4px;"
);
console.log("%c\u521D\u59CB\u5316\u5B8C\u6BD5.", "color: #ffffff; background: #000; padding:5px");
}

View File

@ -1,13 +1,11 @@
import vh from 'vh-plugin'
import { $GET } from '../utils/index'
// 图片懒加载
import vhLzImgInit from "../scripts/vhLazyImg";
// 渲染
const LinksInit = async (data: any) => {
const linksDOM = document.querySelector('.vh-container>.vh-links>main')
if (!linksDOM) return;
vh.showLoading();
try {
let res = data;
if (typeof data === 'string') {
@ -18,8 +16,10 @@ const LinksInit = async (data: any) => {
vhLzImgInit();
} catch {
vh.Toast('获取数据失败')
} finally {
vh.hideLoading();
}
}
export default LinksInit;
// 友情链接初始化
import LINKS_DATA from "../page_data/Link";
const { api, data } = LINKS_DATA;
export default () => LinksInit(api || data)

View File

@ -0,0 +1,9 @@
// 初始化侧边栏
export default () => {
const menuDOM: any = document.querySelector(".vh-header>.main>.nav-btn>span.menu-btn");
const mobileSidebarDOM: any = document.querySelector("body>.vh-mobilesidebar");
const addActive = () => setTimeout(() => mobileSidebarDOM.classList.add("active"));
const removeActive = () => setTimeout(() => mobileSidebarDOM.classList.remove("active"));
menuDOM.addEventListener("click", addActive);
mobileSidebarDOM.addEventListener("click", removeActive);
};

View File

@ -4,19 +4,17 @@ import { $GET } from '../utils/index'
import { LoadScript, LoadStyle } from "../utils/index";
declare const APlayer: any;
const musicInit = async (musicList: any[]) => {
// 初始化音乐播放器
export default async (musicList: any[]) => {
const musicDOM: any = document.querySelectorAll(".vh-node.vh-vhMusic");
if (musicDOM.length === 0) return;
// 载入依赖
await LoadStyle("https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.css");
await LoadScript("https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js");
musicDOM.forEach(async (container: any) => {
const { type, id, list } = container.dataset;
const audio = await $GET(`${vhMusicApi}?server=${type}&type=${id ? 'song' : 'playlist'}&id=${id ? id : list}&r=${Math.random()}`);
const { type = 'song', server = 'netease', id } = container.dataset;
const audio = await $GET(`${vhMusicApi}?server=${server}&type=${type}&id=${id}&r=${Math.random()}`);
const ap = new APlayer({ container, audio, lrcType: 3 });
musicList.push(ap);
});
};
export default musicInit;

View File

@ -7,7 +7,7 @@ const getSearchJson = async () => (searchJson = await $GET('/vh-search.json'))
// 搜索
const searchFn = async (value: string) => {
if (searchJson.length < 1) await getSearchJson();
if (!searchJson.length) await getSearchJson();
// 渲染页面
renderSearch(findAndModifyElements(searchJson, value))
}
@ -31,7 +31,7 @@ const findAndModifyElements = (arr: any[], keyword: string) => {
// 渲染页面
let searchHTML = '';
const renderSearch = (arr: any[]) => {
searchHTML = arr.length < 1 ? '' : arr.map(i => `<a class="vh-search-item" href="${i.url}"><span>${i.title}</span><p>${i.content}</p></a>`).join('');
searchHTML = !arr.length ? '' : arr.map(i => `<a class="vh-search-item" href="${i.url}"><span>${i.title}</span><p>${i.content}</p></a>`).join('');
document.querySelector('.vh-header>.main>.vh-search>main>.vh-search-list')!.innerHTML = searchHTML;
}
@ -43,4 +43,19 @@ const searchInputChange = (v: any) => {
fnTimer = setTimeout(() => searchFn(value), 266);
}
export { searchFn, searchInputChange };
// 初始化搜索框
const vhSearchInit = () => {
const searchDOM: any = document.querySelector(".vh-header>.main>.nav-btn>span.search-btn");
const searchMainDOM: any = document.querySelector(".vh-header>.main>.vh-search>main");
const searchListDOM: any = document.querySelector(".vh-header>.main>.vh-search");
const addActive = () => setTimeout(() => searchListDOM.classList.add("active"));
const removeActive = () => setTimeout(() => searchListDOM.classList.remove("active"));
// 禁止默认事件
searchMainDOM.addEventListener("click", (e: Event) => e.stopPropagation());
searchDOM.addEventListener("click", addActive);
searchListDOM.addEventListener("click", removeActive);
// 搜索框初内容变化
searchListDOM.querySelector(".search-input>input").addEventListener("input", searchInputChange);
};
export { searchFn, searchInputChange, vhSearchInit };

View File

@ -12,7 +12,6 @@ declare const ViewImage: any;
const TalkingInit = async (data: any) => {
const talkingDOM = document.querySelector('.vh-container>.vh-talking>main')
if (!talkingDOM) return;
vh.showLoading();
try {
let res = data;
if (typeof data === 'string') {
@ -26,8 +25,11 @@ const TalkingInit = async (data: any) => {
// 灯箱JS初始化======
} catch {
vh.Toast('获取数据失败')
} finally {
vh.hideLoading();
}
}
export default TalkingInit;
// 动态说说初始化
import TALKING_DATA from "../page_data/Talking";
const { api, data } = TALKING_DATA;
export default () => TalkingInit(api || data);

View File

@ -2,7 +2,8 @@ import { LoadScript } from "../utils/index";
// 初始化视频播放器
declare const DPlayer: any;
declare const Hls: any;
const videoInit = async (videoList: any[]) => {
// 初始化视频播放器
export default async (videoList: any[]) => {
const videoDOM: any = document.querySelectorAll(".vh-node.vh-vhVideo");
if (videoDOM.length === 0) return;
// 载入依赖
@ -32,6 +33,4 @@ const videoInit = async (videoList: any[]) => {
});
videoList.push(dp);
});
};
export default videoInit;
};

View File

@ -1,8 +1,10 @@
// 图片懒加载
import LazyLoad from "vanilla-lazyload";
const vhLzImgInit = () => {
document.querySelectorAll("main>.vh-container>:not(aside) img:not(.view-image-container)").forEach((i: any) => {
// 初始化图片懒加载
export default () => {
document.querySelectorAll("main>.vh-container img:not(.view-image-container)").forEach((i: any) => {
// 是否包含data-vh-lz-src
if (!i.hasAttribute("data-vh-lz-src")) {
i.setAttribute("data-vh-lz-src", i.getAttribute("src"));
@ -14,6 +16,4 @@ const vhLzImgInit = () => {
threshold: 0,
data_src: "vh-lz-src"
});
}
export default vhLzImgInit;
}

View File

@ -43,6 +43,16 @@ section.vh-container {
&>time {
color: #9A9A9A;
}
&>span {
&.count {
color: #3FA67F;
}
&.time {
color: #E9B740;
}
}
}
}
}
@ -111,6 +121,7 @@ section.vh-container {
height: max-content;
}
// 文章内容样式开始=========================
h1,
h2,
@ -169,8 +180,8 @@ section.vh-container {
// p标签样式
// ul标签样式
p,
ul,
ol {
ul:not(.vh-vhMusic ul),
ol:not(.vh-vhMusic ol) {
margin: 0;
padding: 0.618rem 0;
font-size: 0.9375rem;
@ -200,9 +211,25 @@ section.vh-container {
}
}
ul,
ol {
ul:not(.vh-vhMusic ul),
ol:not(.vh-vhMusic ol) {
box-sizing: border-box;
padding-left: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
blockquote {
padding: 0.188rem 0.58rem;
border-left: 5px solid #eee;
border-left-color: #929292;
background: #f3f5f7;
&>p {
font-size: 0.875rem;
font-weight: 500;
}
}
/* 全局表格样式 */
@ -223,7 +250,7 @@ section.vh-container {
}
th {
text-align: left;
text-align: center;
background-color: #f8f9fa;
padding: 0.75rem;
border-bottom: solid 1px #EEEEEE;
@ -281,6 +308,40 @@ section.vh-container {
border-radius: 3px;
background: #f7f7f7;
&.note-success {
border-color: #01C4B6;
border-left: 5px solid #01C4B6;
border-left-color: #01C4B6;
background: #01C4B618;
}
&.note-info {
border-color: #3253b4;
border-left: 5px solid #3253b4;
border-left-color: #3253b4;
background: #3253b418;
}
&.note-warning {
border-color: #DD8636;
border-left: 5px solid #DD8636;
border-left-color: #DD8636;
background: #DD863618;
}
&.note-error {
border-color: #DE3C3D;
border-left: 5px solid #DE3C3D;
border-left-color: #DE3C3D;
background: #DE3C3D18;
}
&.note-import {
border-left: 5px solid #B984DF;
border-left-color: #B984DF;
background: #B984DF18;
}
p {
font-size: 0.875rem;
line-height: 2;
@ -363,14 +424,14 @@ section.vh-container {
}
&.btn-info {
border-color: #3275B4;
border-color: #3253b4;
&>span {
color: #3275B4;
color: #3253b4;
}
&::before {
background-color: #3275B4;
background-color: #3253b4;
}
}
@ -397,6 +458,18 @@ section.vh-container {
background-color: #DE3C3D;
}
}
&.btn-import {
border-color: #B984DF;
&>span {
color: #B984DF;
}
&::before {
background-color: #B984DF;
}
}
}
// :::picture

View File

@ -1,7 +1,6 @@
@import url('/assets/font/index.css');
:root {
--vh-animation-delay: 0ms;
--vh-padding-top: calc(66px + 1rem);
--vh-main-max-width: 1388px;
--vh-back-top: calc((calc(100vw - 2rem) - min((100vw - 2rem), var(--vh-main-max-width))) / 2 + 1rem);
@ -18,6 +17,11 @@ blockquote {
margin: 0;
padding: 0;
color: #333;
text-decoration: none;
}
span {
text-decoration: none !important;
}
p {
@ -41,15 +45,12 @@ code {
font-weight: normal;
}
ul,
ol {
display: flex;
flex-direction: column;
gap: 0.5rem;
img {
max-width: 100%;
max-height: 100%;
overflow: hidden;
}
* {
// IOS 点击阴影
-webkit-tap-highlight-color: transparent;
@ -103,26 +104,6 @@ html {
}
}
// 动画效果
.vh-animation {
opacity: 0;
animation: 300ms fade-in-up;
animation-fill-mode: forwards;
}
@keyframes fade-in-up {
0% {
transform: translateY(2rem);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
// 设置图片懒加载样式
main>.vh-container {
img[data-vh-lz-src] {
@ -134,23 +115,163 @@ main>.vh-container {
}
}
// 加载进度条样式
#nprogress {
.bar {
height: 0.28rem !important;
background: #01C4B6 !important;
}
.peg {
box-shadow: 0 0 10px #01C4B6, 0 0 5px #01C4B6 !important;
}
}
@media screen and (max-width: 768px) {
// 手机端 禁止选择
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
// 首次加载效果
.vh-animation.vh-animation-init {
opacity: 0;
animation: 300ms vh-init-show;
animation-fill-mode: forwards;
transition: opacity 0.16s ease-in-out, transform 0.16s ease-in-out;
}
// swup 动画效果
html.swup-enabled,
html.is-changing {
.vh-animation {
&.vh-animation-init {
transform: translateY(0);
opacity: 1;
animation: none;
}
}
}
html.is-animating {
.vh-animation {
&.vh-animation-init {
transform: translateY(0.88rem);
opacity: 0;
}
}
}
// 占位加载
.vh-space-loading {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
grid-column: span 3;
width: 100%;
height: 8.88rem;
overflow: hidden;
&.white {
background: #fff;
box-shadow: 0 3px 8px 6px rgba(7, 17, 27, 0.05);
}
&>span {
background: #000;
position: relative;
width: 5px;
height: 5px;
margin: 3px;
border-radius: 5px;
&:nth-of-type(1) {
-webkit-animation: scale .8s ease infinite;
animation: vh-loading-animation .8s ease infinite
}
&:nth-of-type(2) {
-webkit-animation: scale .8s ease .2s infinite;
animation: vh-loading-animation .8s ease .2s infinite
}
&:nth-of-type(3) {
-webkit-animation: scale .8s ease .4s infinite;
animation: vh-loading-animation .8s ease .4s infinite
}
}
}
// 谷歌广告模块
.vh-ad {
position: relative;
display: block;
width: 100%;
height: max-content;
min-height: 5.88rem;
overflow: hidden !important;
z-index: 1;
&.vh-article-ad {
box-sizing: border-box;
padding: 0.75rem;
margin-top: 1rem;
border-radius: 0.5rem;
background: #fff;
box-shadow: 0 3px 8px 6px rgba(7, 17, 27, 0.05);
overflow: hidden;
&::before {
top: 50%;
}
}
&::before {
position: absolute;
left: 50%;
top: 30%;
transform: translate(-50%, -50%);
content: '广告加载中...';
color: #e3e4e6;
z-index: -1;
}
}
@keyframes vh-init-show {
0% {
transform: translateY(2rem);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes vh-loading-animation {
50% {
height: 25px
}
0% {
height: 5px
}
}
@-webkit-keyframes vh-init-show {
0% {
transform: translateY(2rem);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@-webkit-keyframes vh-loading-animation {
50% {
height: 25px
}
0% {
height: 5px
}
}

View File

@ -108,32 +108,35 @@
}
&>footer {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
gap: 0.58rem;
width: 100%;
&>span {
box-sizing: border-box;
padding: 0 0.5rem;
padding: 0.28rem 0.68rem;
display: flex;
align-items: center;
height: 1.68rem;
width: max-content;
line-height: 1.46rem;
border: 1px solid #49b1f5;
border-radius: 2rem;
color: #49b1f5;
border: 1px solid #3253b4;
border-radius: 0.88rem;
background-color: #fff;
font-size: 0.72rem;
color: #3253b4;
transition: all .2s ease-in-out;
user-select: none;
cursor: pointer;
&:hover {
animation: talking-tag-active 0.16s ease-in-out infinite;
color: #fff;
background-color: #49b1f5;
}
}
}
}
}
}

View File

@ -3,7 +3,7 @@ section.article-list {
position: relative;
padding-bottom: 6rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
grid-template-columns: repeat(3, 1fr);
gap: 1.6rem;
width: 100%;
height: max-content;
@ -12,7 +12,8 @@ section.article-list {
&>.vh-article-item {
box-sizing: border-box;
width: 100%;
height: 100%;
height: initial;
min-height: max-content;
border: solid 1px #eee;
border-radius: 1rem;
background: #fff;
@ -23,7 +24,8 @@ section.article-list {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
height: max-content;
min-height: 100%;
border-radius: 0.618rem;
overflow: hidden;
transition: background 0.16s;
@ -53,6 +55,7 @@ section.article-list {
&>.vh-article-desc {
flex: 1;
box-sizing: border-box;
padding: 1.25rem;
display: flex;
@ -164,4 +167,16 @@ section.article-list {
bottom: 0;
right: 0;
}
}
@media screen and (max-width: 1150px) {
section.article-list {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width: 556px) {
section.article-list {
grid-template-columns: repeat(1, 1fr);
}
}

View File

@ -1,15 +1,24 @@
aside.vh-aside {
flex-shrink: 0;
position: sticky;
top: var(--vh-padding-top);
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 1rem;
width: 288px;
height: max-content;
height: initial;
&>.vh-aside-item {
&>.sticky-aside {
position: sticky;
top: var(--vh-padding-top);
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
height: max-content;
}
.vh-aside-item {
box-sizing: border-box;
padding: 0.75rem;
display: flex;
@ -17,6 +26,7 @@ aside.vh-aside {
align-items: center;
gap: 0.75rem;
width: 100%;
max-width: 100%;
height: max-content;
border-radius: 1rem;
background: #fff;
@ -24,7 +34,6 @@ aside.vh-aside {
box-shadow: 0 3px 8px 6px rgba(7, 17, 27, 0.05);
overflow: hidden;
&>h3 {
padding-bottom: 0.618rem;
display: flex;
@ -50,45 +59,9 @@ aside.vh-aside {
&>.vh-aside-avatar {
position: relative;
width: 6.36rem;
aspect-ratio: 1/1;
height: 6.36rem;
border-radius: 1rem;
cursor: pointer;
overflow: hidden;
&:hover {
&>img {
opacity: 0;
transform: translate(-50%, -100%);
}
&>svg {
opacity: 1;
transform: translate(-50%, -50%);
}
}
&>img,
&>svg {
position: absolute;
left: 50%;
top: 50%;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.28s, opacity 0.18s;
}
&>img {
transform: translate(-50%, -50%);
opacity: 1;
}
&>svg {
width: 60%;
height: 60%;
transform: translate(-50%, 100%);
opacity: 0;
}
}
&>.vh-aside-auther {
@ -211,7 +184,45 @@ aside.vh-aside {
}
}
// 最新文章
// 标签
&.tags {
&>.vh-aside-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
width: 100%;
height: max-content;
overflow: hidden;
&>a {
width: max-content;
height: max-content;
&:hover {
&>span {
background: #EDEEF3;
}
}
&>span {
display: block;
box-sizing: border-box;
padding: 0 0.618rem;
width: max-content;
height: 1.75rem;
border-radius: 0.38rem;
background: #EDEEF388;
font-size: 0.875rem;
font-style: normal;
line-height: 1.75rem;
transition: background 0.16s ease-in-out;
overflow: hidden;
}
}
}
}
// 推荐文章
&.articles {
&>.vh-aside-articles {
display: flex;
@ -293,44 +304,6 @@ aside.vh-aside {
}
}
}
// 标签
&.tags {
&>.vh-aside-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
width: 100%;
height: max-content;
overflow: hidden;
&>a {
width: max-content;
height: max-content;
&:hover {
&>span {
background: #EDEEF3;
}
}
&>span {
display: block;
box-sizing: border-box;
padding: 0 0.618rem;
width: max-content;
height: 1.75rem;
border-radius: 0.38rem;
background: #EDEEF388;
font-size: 0.875rem;
font-style: normal;
line-height: 1.75rem;
transition: background 0.16s ease-in-out;
overflow: hidden;
}
}
}
}
}
}

View File

@ -45,10 +45,10 @@
}
p {
padding: 0.38rem 0;
color: #4c4948;
font-weight: 400;
font-size: 0.875rem;
line-height: 28px;
a {
box-shadow: inset 0 -.12em #60a5fa;
@ -234,8 +234,10 @@
// 内容
.tk-content {
margin: 0;
p {
margin: 0;
padding: 0.18rem 0;
}
img {

View File

@ -25,7 +25,7 @@ const getTags = () => {
// 获取推荐文章 (给文章添加 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) }))
return (recommendList.length ? 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 };

View File

@ -54,10 +54,7 @@ const LoadScript = (
const value = typeof v === "boolean"
? (v ? "" : null) // 布尔值处理为 HTML 标准属性格式
: String(v); // 其他类型转为字符串
if (value !== null) {
script.setAttribute(k, value);
}
if (value !== null) script.setAttribute(k, value);
});
}
script.onload = () => resolve(script);
@ -86,7 +83,6 @@ const $GET = async (url: string, headers: Record<string, string> = {}): Promise<
return res.json();
} catch (error) {
console.error("GET request failed:", error);
throw error; // 抛出错误以便调用者处理
}
};
@ -97,7 +93,6 @@ const $POST = async (url: string, data: Record<string, any>, headers: Record<str
return res.json(); // 解析 JSON 数据
} catch (error) {
console.error("POST request failed:", error);
throw error; // 抛出错误以便调用者处理
}
};

View File

@ -1,17 +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";
// 扩展 Window 接口以包含 swup 属性
declare global {
interface Window {
swup: { hooks: { on: (event: string, handler: EventHandler) => void } };
}
}
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);
// 进入页面时触发
const inRouter = (handler: EventHandler) => {
const setup = () => window.swup.hooks.on("page:view", handler);
window.swup ? setup() : document.addEventListener("swup:enable", setup);
};
// 离开当前页面时触发
const outRouter = (handler: EventHandler) => window.swup ? window.swup.hooks.on("visit:start", handler) : document.addEventListener("swup:enable", () => outRouter(handler));
export default updateRouter;
export { inRouter, outRouter };