diff --git a/README.md b/README.md index f70201a..356dbca 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - [x] 简洁的响应式设计 - [x] 流畅的动画和页面过渡 +- [x] 丝滑的阻尼滚动效果(自定义开启/关闭) - [x] 两列布局 - [x] 阅读时间 - [x] 字数统计 @@ -27,9 +28,12 @@ - [x] 分类 - [x] 归档 - [x] 动态 +- [x] 圈子 - [x] 关于 +- [x] 留言板 - [x] 友情链接 - [x] 推荐文章 +- [x] 置顶文章 - [x] 谷歌广告 - [x] 内置 404 页面 - [x] Sitemap 支持 @@ -114,6 +118,7 @@ date: 文章创建日期 updated: 文章更新日期 cover: "封面图URL (为空默认随机内置封面 /public/assets/images/banner)" recommend: false # 是否推荐文章 +top: false # 是否置顶文章 hide: false # 是否隐藏文章 --- ``` diff --git a/astro.config.mjs b/astro.config.mjs index da24d81..6d039b4 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -14,6 +14,7 @@ import swup from '@swup/astro'; // https://astro.build/config export default defineConfig({ site: 'https://www.vvhan.com', + build: { assets: 'vh_static' }, integrations: [ swup({ theme: false, diff --git a/package.json b/package.json index ddf4302..c6dedcd 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,7 @@ "@astrojs/sitemap": "^3.2.1", "@swup/astro": "^1.5.0", "aplayer": "^1.10.1", - "astro": "^5.4.1", - "lenis": "^1.2.1", + "astro": "^5.4.2", "overlayscrollbars": "^2.11.1", "vanilla-lazyload": "^19.1.3", "vh-plugin": "^1.2.2" diff --git a/src/components/ArticleCard.astro b/src/components/ArticleCard.astro index 2cb3dcc..937e1ae 100644 --- a/src/components/ArticleCard.astro +++ b/src/components/ArticleCard.astro @@ -12,7 +12,7 @@ import "../styles/components/ArticleCard.less"; ---
- +
{post.data.title}
diff --git a/src/content.config.ts b/src/content.config.ts index a2eb10f..9f2d71e 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -14,7 +14,8 @@ const blog = defineCollection({ id: z.union([z.string(), z.number()]), cover: z.string().optional(), recommend: z.boolean().optional(), - hide: z.boolean().optional() + hide: z.boolean().optional(), + top: z.boolean().optional() }), }); diff --git a/src/pages/[...page].astro b/src/pages/[...page].astro index d6e766d..663eafd 100644 --- a/src/pages/[...page].astro +++ b/src/pages/[...page].astro @@ -2,12 +2,14 @@ import { getCollection } from "astro:content"; import type { GetStaticPathsOptions } from "astro"; import setSearchJson from "../utils/vhSearch"; +import moveTopToFirst from "../utils/moveTopToFirst"; export async function getStaticPaths(options: GetStaticPathsOptions) { const { paginate } = options; const posts = (await getCollection("blog")).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); - // 生成 Search JSON 文件 ====== + // 置顶文章功能 + moveTopToFirst(posts); + // 生成 Search JSON 文件 await setSearchJson(posts); - // 生成 Search JSON 文件 ====== // 隐藏的文章不显示 return paginate( posts.filter(i => !i.data.hide), diff --git a/src/scripts/BackTop.ts b/src/scripts/BackTop.ts index e2f9b81..65e8883 100644 --- a/src/scripts/BackTop.ts +++ b/src/scripts/BackTop.ts @@ -15,7 +15,7 @@ const scrollChangeFn = () => { const backTopFn = () => { (window as any).vhlenis && (window as any).vhlenis.stop(); window.scrollTo({ top: 0, behavior: "smooth" }); - (window as any).vhlenis.start(); + (window as any).vhlenis && (window as any).vhlenis.start(); }; // 页面更新,初始化函数====== // 回顶部DOM diff --git a/src/scripts/Friends.ts b/src/scripts/Friends.ts index e64c5a5..3b7f31f 100644 --- a/src/scripts/Friends.ts +++ b/src/scripts/Friends.ts @@ -11,7 +11,7 @@ const FriendsInit = async (data: any) => { if (typeof data === 'string') { res = await $GET(api); } - friendsDOM.innerHTML = res.map((i: any) => ``).join(''); + friendsDOM.innerHTML = res.map((i: any) => ``).join(''); } catch { vh.Toast('获取数据失败') } diff --git a/src/scripts/HanAnalytics.ts b/src/scripts/HanAnalytics.ts new file mode 100644 index 0000000..751db40 --- /dev/null +++ b/src/scripts/HanAnalytics.ts @@ -0,0 +1,9 @@ + +// Han Analytics 统计 +import SITE_INFO from "../config"; +import { LoadScript } from "../utils/index"; + +export default async () => { + const { HanAnalytics } = SITE_INFO; + HanAnalytics.enable && LoadScript(`${HanAnalytics.server}/tracker.min.js`, [{ k: "data-website-id", v: HanAnalytics.siteId }]); +} \ No newline at end of file diff --git a/src/scripts/Init.ts b/src/scripts/Init.ts index 8e66b92..9e5312d 100644 --- a/src/scripts/Init.ts +++ b/src/scripts/Init.ts @@ -1,6 +1,4 @@ import { inRouter, outRouter } from "../utils/updateRouter"; -// 鼠标滚动阻尼效果 -import LenisInit from './Lenis'; // 初始化文章代码块 import codeInit from "../scripts/Code"; // 初始化视频播放器 @@ -30,9 +28,9 @@ 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"; +import HanAnalyticsInit from "../scripts/HanAnalytics"; +// SmoothScroll 滚动优化 +import SmoothScroll from "../scripts/Smoothscroll"; // ============================================================ @@ -40,8 +38,6 @@ import { LoadScript } from "../utils/index"; const videoList: any[] = []; const MusicList: any[] = []; const indexInit = async (only: boolean = true) => { - // 鼠标滚动阻尼效果 - only && LenisInit(); // 预加载搜索数据 only && searchFn(""); // 初始化搜索功能 @@ -52,6 +48,8 @@ const indexInit = async (only: boolean = true) => { only && BackTopInitFn(); // 移动端侧边栏初始化 only && initMobileSidebar(); + // SmoothScroll 滚动优化 + only && SmoothScroll(); // 顶部导航 Current 状态 initLinkCurrent() // 初始化文章代码块 @@ -68,12 +66,12 @@ const indexInit = async (only: boolean = true) => { initTalking(); // Google 广告 GoogleAdInit(); + // Han Analytics 统计 + HanAnalyticsInit(); // 文章视频播放器初始化 videoInit(videoList); // 文章音乐播放器初始化 musicInit(MusicList); - // Han Analytics 统计 - HanAnalytics.enable && LoadScript(`${HanAnalytics.server}/tracker.min.js`, [{ k: "data-website-id", v: HanAnalytics.siteId }]); }; export default () => { diff --git a/src/scripts/Lenis.ts b/src/scripts/Lenis.ts deleted file mode 100644 index 9213453..0000000 --- a/src/scripts/Lenis.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Lenis from "lenis"; - -const IncludeClassName = ['vh-code-box', 'vh-search-list']; -(window as any).vhlenis = new Lenis({ - prevent: (node) => IncludeClassName.some((i: string) => node.className.includes(i)) -}); -const lenisInit = (time: any) => { - (window as any).vhlenis.raf(time) - requestAnimationFrame(lenisInit) -} -(window as any).vhlenis.on('scroll', () => { - if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight) { - (window as any).vhlenis.stop(); - (window as any).vhlenis.start(); - } -}); - -export default () => requestAnimationFrame(lenisInit) \ No newline at end of file diff --git a/src/scripts/Music.ts b/src/scripts/Music.ts index cf0468f..3821b51 100644 --- a/src/scripts/Music.ts +++ b/src/scripts/Music.ts @@ -4,7 +4,6 @@ import { $GET } from '../utils/index' import 'aplayer/dist/APlayer.min.css'; import APlayer from 'aplayer'; -declare const APlayer: any; // 初始化音乐播放器 export default async (MusicList: any[]) => { const musicDOM: any = document.querySelectorAll(".vh-node.vh-vhMusic"); diff --git a/src/scripts/Smoothscroll.ts b/src/scripts/Smoothscroll.ts new file mode 100644 index 0000000..77fbb80 --- /dev/null +++ b/src/scripts/Smoothscroll.ts @@ -0,0 +1,7 @@ +// SmoothScroll 滚动优化 +import { LoadScript } from "../utils/index"; +declare const SmoothScroll: any; +export default async () => { + await LoadScript("/assets/js/smoothscroll.min.js"); + SmoothScroll({ stepSize: 118, animationTime: 666 }) +}; \ No newline at end of file diff --git a/src/styles/Article.less b/src/styles/Article.less index 4dd24f5..a8532bb 100644 --- a/src/styles/Article.less +++ b/src/styles/Article.less @@ -3,7 +3,6 @@ section.vh-container { flex: 1; min-width: 0; - &>header { box-sizing: border-box; padding: 2rem 1rem 1rem; @@ -70,6 +69,14 @@ section.vh-container { box-shadow: 0 12px 8px 6px rgba(7, 17, 27, 0.05); overflow: hidden; + .aplayer-list { + max-height: max-content !important; + + &>ol { + max-height: 566px !important; + } + } + &>.tag-list { box-sizing: border-box; padding-top: 2rem; diff --git a/src/styles/Base.less b/src/styles/Base.less index 0802510..db568d4 100644 --- a/src/styles/Base.less +++ b/src/styles/Base.less @@ -62,6 +62,8 @@ img { // 隐藏滚动条 scrollbar-width: none; -ms-overflow-style: none; + // 平滑滚动 + scroll-behavior: smooth; // 消除边框 outline: none; // 隐藏滚动条 diff --git a/src/styles/Friends.less b/src/styles/Friends.less index 862f8c8..79b4cb8 100644 --- a/src/styles/Friends.less +++ b/src/styles/Friends.less @@ -57,6 +57,7 @@ justify-content: space-between; width: 100%; height: max-content; + overflow: hidden; &>span, &>time { @@ -64,18 +65,33 @@ display: flex; align-items: center; gap: 0.5rem; - width: max-content; height: 1.56rem; font-size: 0.8rem; + overflow: hidden; + + &>em { + font-style: normal; + flex: 1; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } &>img { + flex-shrink: 0; width: 1.56rem; height: 1.56rem; border-radius: 50%; } } + &>span { + flex: 1; + } + &>time { + flex-shrink: 0; + width: max-content; padding: 0.5rem; background: #F7F7F7; border-radius: 0.8rem; diff --git a/src/styles/components/ArticleCard.less b/src/styles/components/ArticleCard.less index 824d8fa..dec9a9a 100644 --- a/src/styles/components/ArticleCard.less +++ b/src/styles/components/ArticleCard.less @@ -20,6 +20,7 @@ section.article-list { overflow: hidden; &>a.vh-article-link { + position: relative; box-sizing: border-box; display: flex; flex-direction: column; @@ -38,7 +39,48 @@ section.article-list { } } + &.active { + &::before { + content: "置顶"; + position: absolute; + top: 1.6rem; + left: 0.66rem; + box-sizing: border-box; + padding: 0.18rem 1rem; + width: max-content; + height: max-content; + font-size: 1rem; + color: #fff; + border-radius: 0.66rem; + transform: rotate(-45deg); + /* 确保元素有半透明背景 */ + background: #00000033; + box-shadow: 0 2rem 1rem rgba(0, 0, 0, 0.1); + z-index: 1; + /* 高斯模糊效果 */ + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + /* 兼容旧版浏览器 */ + } + + &>.vh-article-banner { + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.06); + z-index: 1; + pointer-events: none; + } + } + } + + &>.vh-article-banner { + position: relative; flex-shrink: 0; box-sizing: border-box; width: 100%; diff --git a/src/styles/components/Comment.less b/src/styles/components/Comment.less index 7eac65c..f39386e 100644 --- a/src/styles/components/Comment.less +++ b/src/styles/components/Comment.less @@ -71,6 +71,21 @@ } } + .tk-admin { + + .tk-login { + box-sizing: border-box; + } + + p { + color: #fff; + } + + .tk-admin-warn p { + color: #000; + } + } + // 评论输入框Focus边框颜色 .el-input__inner:focus, .el-textarea__inner:focus { diff --git a/src/type/aplayer.d.ts b/src/type/aplayer.d.ts new file mode 100644 index 0000000..15503fa --- /dev/null +++ b/src/type/aplayer.d.ts @@ -0,0 +1 @@ +declare module 'aplayer'; \ No newline at end of file diff --git a/src/utils/getCover.ts b/src/utils/getCover.ts index f18b856..a40e035 100644 --- a/src/utils/getCover.ts +++ b/src/utils/getCover.ts @@ -59,10 +59,8 @@ async function* createImageIterator(dir: string) { const targetDir = path.resolve(__dirname, '../../public/assets/images/banner/'); // 目标目录 const fileIter = createImageIterator(targetDir); -const getCover = async (filename: string | null | undefined) => { +export default async (filename: string | null | undefined) => { if (filename) return filename; const { value } = await fileIter.next(); return SITE_INFO.Site + `/assets/images/banner/${value}` -} - -export default getCover; \ No newline at end of file +} \ No newline at end of file diff --git a/src/utils/getFileTime.ts b/src/utils/getFileTime.ts deleted file mode 100644 index fd67b50..0000000 --- a/src/utils/getFileTime.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { execSync } from "child_process"; - -const COMMITER_DATE_ISO_FORMAT = "%cI"; - -export default function getFileCreateTime(filePath: string, type: string): Date { - try { - const result = execSync(`git log --pretty=format:"${COMMITER_DATE_ISO_FORMAT}" ${filePath}`).toString(); - if (!result) { - throw new Error('Git command failed to execute'); - } - const commits = result.split(/\r?\n/g); - return new Date(commits[type == 'create' ? commits.length - 1 : 0]); - } catch (e) { - // not committed yet - return new Date(); - } -} \ No newline at end of file diff --git a/src/utils/getPostInfo.ts b/src/utils/getPostInfo.ts index a0a6e46..758a219 100644 --- a/src/utils/getPostInfo.ts +++ b/src/utils/getPostInfo.ts @@ -24,8 +24,8 @@ const getTags = () => { // 获取推荐文章 (给文章添加 recommend: true 字段) const getRecommendArticles = () => { - const recommendList = posts.filter(i => i.data.recommend).slice(0, 6); - 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) })) + const recommendList = posts.filter(i => i.data.recommend); + 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 }; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index a76385e..9a636d5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,7 +11,7 @@ const getDescription = (post: any, num: number = 150) => (post.rendered ? post.r //处理时间 const fmtTime = (time: any, fmt: string = 'MMMM D, YYYY') => dayjs(time).utc().format(fmt) // 处理日期 -const fmtDate = (time: string | Date) => { +const fmtDate = (time: string | Date, hours_status = true) => { const now = dayjs(); const past = dayjs(time); // 计算各时间单位,逐步扣除已计算的部分 @@ -31,9 +31,9 @@ const fmtDate = (time: string | Date) => { years && `${years}年`, months && `${months}月`, days && `${days}天`, - hours && !years && !months && `${hours}小时`, - minutes && !years && !months && !days && `${minutes}分`, - seconds && !years && !months && !days && !hours && `${seconds}秒` + hours_status ? hours && !years && !months && `${hours}小时` : 0, + hours_status ? minutes && !years && !months && !days && `${minutes}分` : '', + hours_status ? seconds && !years && !months && !days && !hours && `${seconds}秒` : '' ].filter(Boolean).join(''); }; diff --git a/src/utils/moveTopToFirst.ts b/src/utils/moveTopToFirst.ts new file mode 100644 index 0000000..c5f583f --- /dev/null +++ b/src/utils/moveTopToFirst.ts @@ -0,0 +1,8 @@ +export default (arr: Array) => { + const index = arr.findIndex((item: any) => item.data.top === true); + if (index !== -1) { + const [item] = arr.splice(index, 1); + arr.unshift(item); + } + return arr; +} \ No newline at end of file diff --git a/src/utils/vhSearch.ts b/src/utils/vhSearch.ts index 4cdb390..93ddc75 100644 --- a/src/utils/vhSearch.ts +++ b/src/utils/vhSearch.ts @@ -2,7 +2,7 @@ import fs from 'fs/promises'; import path from 'path'; import * as cheerio from 'cheerio'; -const setSearchJson = async (posts: any[]) => { +export default async (posts: any[]) => { const searchIndex = posts.map(i => { const $ = cheerio.load(`${i.rendered.html}`); return { @@ -25,6 +25,4 @@ const setSearchJson = async (posts: any[]) => { } catch (error) { console.error('Error writing search index file:', error); } -}; - -export default setSearchJson; \ No newline at end of file +}; \ No newline at end of file