正式版1.0✅
This commit is contained in:
parent
60f5296dcb
commit
e7117f0903
146
README.md
146
README.md
@ -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)
|
||||
|
||||

|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- [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
|
||||
# 创建新文章
|
||||
pnpm newpost '文章标题'
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||
@ -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({
|
||||
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],
|
||||
|
||||
15
package.json
15
package.json
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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 });
|
||||
try {
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
await fs.writeFile(path.join(targetDir, `${articleName}.md`), ArticleContent, 'utf8');
|
||||
console.log(`文章 ${articleName} 已创建`);
|
||||
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();
|
||||
@ -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}`}>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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="sticky-aside">
|
||||
<!-- 最新文章块 -->
|
||||
{
|
||||
recommendArticles.length && (
|
||||
<section class="vh-aside-item articles">
|
||||
<h3>推荐文章</h3>
|
||||
<div class="vh-aside-articles">
|
||||
{
|
||||
recommendArticles.map(async i => (
|
||||
{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" />
|
||||
<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>
|
||||
|
||||
@ -15,9 +15,3 @@ import "../styles/components/BackTop.less";
|
||||
</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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
5
src/components/GoogleAd.astro
Normal file
5
src/components/GoogleAd.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
const { slotID, className } = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`vh-ad ${className}`} set:html={slotID} />
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -12,24 +12,3 @@ import "../styles/components/Search.less";
|
||||
<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>
|
||||
|
||||
@ -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>`
|
||||
},
|
||||
}
|
||||
@ -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()
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />)}
|
||||
<!-- 分页 -->
|
||||
|
||||
@ -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, I’m Han 👋</p>
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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 />
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -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初始化======
|
||||
@ -35,4 +37,3 @@ const ArticleInit = () => {
|
||||
});
|
||||
// Pre Code 代码复制功能======
|
||||
}
|
||||
export default ArticleInit;
|
||||
@ -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
8
src/scripts/Comment.ts
Normal 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
7
src/scripts/Footer.ts
Normal 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
24
src/scripts/GoogleAd.ts
Normal 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
11
src/scripts/Header.ts
Normal 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
91
src/scripts/Init.ts
Normal 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");
|
||||
}
|
||||
@ -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)
|
||||
9
src/scripts/MobileSidebar.ts
Normal file
9
src/scripts/MobileSidebar.ts
Normal 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);
|
||||
};
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 };
|
||||
@ -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);
|
||||
@ -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;
|
||||
// 载入依赖
|
||||
@ -33,5 +34,3 @@ const videoInit = async (videoList: any[]) => {
|
||||
videoList.push(dp);
|
||||
});
|
||||
};
|
||||
|
||||
export default videoInit;
|
||||
@ -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"));
|
||||
@ -15,5 +17,3 @@ const vhLzImgInit = () => {
|
||||
data_src: "vh-lz-src"
|
||||
});
|
||||
}
|
||||
|
||||
export default vhLzImgInit;
|
||||
@ -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
|
||||
|
||||
@ -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,19 +115,6 @@ 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;
|
||||
@ -154,3 +122,156 @@ main>.vh-container {
|
||||
-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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -165,3 +168,15 @@ section.article-list {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,24 @@
|
||||
aside.vh-aside {
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 288px;
|
||||
height: initial;
|
||||
|
||||
&>.sticky-aside {
|
||||
position: sticky;
|
||||
top: var(--vh-padding-top);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 288px;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
&>.vh-aside-item {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
p {
|
||||
padding: 0.18rem 0;
|
||||
}
|
||||
|
||||
img {
|
||||
|
||||
@ -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 };
|
||||
@ -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; // 抛出错误以便调用者处理
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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 };
|
||||
Loading…
x
Reference in New Issue
Block a user