mirror of
https://github.com/Ad-closeNN/blog-fuwari.git
synced 2026-05-31 01:00:04 -04:00
feat(ui): PostCard 详情页新增浏览量显示
This commit is contained in:
+3
-1
@@ -28,4 +28,6 @@ yarn.lock
|
|||||||
|
|
||||||
src/content/.obsidian
|
src/content/.obsidian
|
||||||
|
|
||||||
.playwright-mcp
|
.playwright-mcp
|
||||||
|
.serena
|
||||||
|
.claude
|
||||||
Vendored
+23
-3
@@ -1,9 +1,26 @@
|
|||||||
import type { AstroIntegration } from "@swup/astro";
|
export {};
|
||||||
|
|
||||||
|
type SwupHookHandler = (...args: unknown[]) => void;
|
||||||
|
|
||||||
|
type SwupLike = {
|
||||||
|
hooks: {
|
||||||
|
on: (eventName: string, handler: SwupHookHandler) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type BlogPhotoSwipeState = {
|
||||||
|
lightbox: {
|
||||||
|
destroy: () => void;
|
||||||
|
} | null;
|
||||||
|
hookRegistered: boolean;
|
||||||
|
pageLoadRegistered: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
// type from '@swup/astro' is incorrect
|
swup?: SwupLike;
|
||||||
swup: AstroIntegration;
|
__blogPhotoSwipe?: BlogPhotoSwipeState;
|
||||||
|
dataLayer?: unknown[];
|
||||||
pagefind: {
|
pagefind: {
|
||||||
search: (query: string) => Promise<{
|
search: (query: string) => Promise<{
|
||||||
results: Array<{
|
results: Array<{
|
||||||
@@ -14,6 +31,9 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare function gtag(...args: unknown[]): void;
|
||||||
|
declare let dataLayer: unknown[];
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
url: string;
|
url: string;
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
+81
-24
@@ -482,8 +482,8 @@ function showBanner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 在显示Banner前,先移除所有已加载的onload-animation类,防止动画重复触发
|
// 在显示Banner前,先移除所有已加载的onload-animation类,防止动画重复触发
|
||||||
const animatedElements = document.querySelectorAll('.onload-animation');
|
const animatedElements = document.querySelectorAll<HTMLElement>('.onload-animation');
|
||||||
animatedElements.forEach(el => {
|
animatedElements.forEach((el) => {
|
||||||
el.style.animation = 'none';
|
el.style.animation = 'none';
|
||||||
el.style.opacity = '1';
|
el.style.opacity = '1';
|
||||||
});
|
});
|
||||||
@@ -635,52 +635,109 @@ window.onresize = () => {
|
|||||||
import PhotoSwipeLightbox from "photoswipe/lightbox"
|
import PhotoSwipeLightbox from "photoswipe/lightbox"
|
||||||
import "photoswipe/style.css"
|
import "photoswipe/style.css"
|
||||||
|
|
||||||
let lightbox: PhotoSwipeLightbox
|
const zoomTargetSelector = ".custom-md img, #post-cover img"
|
||||||
let pswp = import("photoswipe")
|
const pswpModule = import("photoswipe")
|
||||||
|
|
||||||
|
type PhotoSwipeState = {
|
||||||
|
lightbox: PhotoSwipeLightbox | null
|
||||||
|
hookRegistered: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const photoSwipeState = (window.__blogPhotoSwipe ??= {
|
||||||
|
lightbox: null,
|
||||||
|
hookRegistered: false,
|
||||||
|
pageLoadRegistered: false,
|
||||||
|
}) as PhotoSwipeState & { pageLoadRegistered: boolean }
|
||||||
|
|
||||||
|
function getZoomTargets() {
|
||||||
|
return Array.from(document.querySelectorAll<HTMLImageElement>(zoomTargetSelector))
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPhotoSwipe() {
|
||||||
|
photoSwipeState.lightbox?.destroy()
|
||||||
|
photoSwipeState.lightbox = null
|
||||||
|
}
|
||||||
|
|
||||||
function createPhotoSwipe() {
|
function createPhotoSwipe() {
|
||||||
lightbox = new PhotoSwipeLightbox({
|
const lightbox = new PhotoSwipeLightbox({
|
||||||
gallery: ".custom-md img, #post-cover img",
|
gallery: "body",
|
||||||
pswpModule: () => pswp,
|
children: zoomTargetSelector,
|
||||||
|
pswpModule: () => pswpModule,
|
||||||
closeSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></svg>',
|
closeSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></svg>',
|
||||||
zoomSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M340-540h-40q-17 0-28.5-11.5T260-580q0-17 11.5-28.5T300-620h40v-40q0-17 11.5-28.5T380-700q17 0 28.5 11.5T420-660v40h40q17 0 28.5 11.5T500-580q0 17-11.5 28.5T460-540h-40v40q0 17-11.5 28.5T380-460q-17 0-28.5-11.5T340-500v-40Zm40 220q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>',
|
zoomSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M340-540h-40q-17 0-28.5-11.5T260-580q0-17 11.5-28.5T300-620h40v-40q0-17 11.5-28.5T380-700q17 0 28.5 11.5T420-660v40h40q17 0 28.5 11.5T500-580q0 17-11.5 28.5T460-540h-40v40q0 17-11.5 28.5T380-460q-17 0-28.5-11.5T340-500v-40Zm40 220q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>',
|
||||||
padding: { top: 20, bottom: 20, left: 20, right: 20 },
|
padding: { top: 20, bottom: 20, left: 20, right: 20 },
|
||||||
|
showHideAnimationType: "fade",
|
||||||
|
showAnimationDuration: 160,
|
||||||
|
hideAnimationDuration: 140,
|
||||||
wheelToZoom: true,
|
wheelToZoom: true,
|
||||||
arrowPrev: false,
|
arrowPrev: false,
|
||||||
arrowNext: false,
|
arrowNext: false,
|
||||||
imageClickAction: 'close',
|
imageClickAction: "close",
|
||||||
tapAction: 'close',
|
tapAction: "close",
|
||||||
doubleTapAction: 'zoom',
|
doubleTapAction: "zoom",
|
||||||
})
|
})
|
||||||
|
|
||||||
lightbox.addFilter("domItemData", (itemData, element) => {
|
lightbox.addFilter("domItemData", (itemData, element) => {
|
||||||
if (element instanceof HTMLImageElement) {
|
if (element instanceof HTMLImageElement) {
|
||||||
itemData.src = element.src
|
const width = element.naturalWidth || element.width || window.innerWidth
|
||||||
|
const height = element.naturalHeight || element.height || window.innerHeight
|
||||||
|
const src = element.currentSrc || element.src
|
||||||
|
|
||||||
itemData.w = Number(element.naturalWidth || window.innerWidth)
|
itemData.src = src
|
||||||
itemData.h = Number(element.naturalHeight || window.innerHeight)
|
itemData.w = Number(width)
|
||||||
|
itemData.h = Number(height)
|
||||||
itemData.msrc = element.src
|
itemData.msrc = src
|
||||||
}
|
}
|
||||||
|
|
||||||
return itemData
|
return itemData
|
||||||
})
|
})
|
||||||
|
|
||||||
lightbox.init()
|
lightbox.init()
|
||||||
|
photoSwipeState.lightbox = lightbox
|
||||||
}
|
}
|
||||||
|
|
||||||
const setup = () => {
|
function initPhotoSwipe() {
|
||||||
window.swup.hooks.on("page:view", () => {
|
if (photoSwipeState.lightbox || getZoomTargets().length === 0) {
|
||||||
if (lightbox) {
|
return
|
||||||
lightbox.destroy()
|
}
|
||||||
}
|
createPhotoSwipe()
|
||||||
createPhotoSwipe()
|
}
|
||||||
|
|
||||||
|
function reinitPhotoSwipe() {
|
||||||
|
destroyPhotoSwipe()
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
initPhotoSwipe()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.swup) {
|
function registerPhotoSwipeHook() {
|
||||||
setup()
|
if (photoSwipeState.hookRegistered || !window.swup?.hooks) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.swup.hooks.on("page:view", reinitPhotoSwipe)
|
||||||
|
photoSwipeState.hookRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleInitialPhotoSwipe() {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
initPhotoSwipe()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", scheduleInitialPhotoSwipe, { once: true })
|
||||||
} else {
|
} else {
|
||||||
document.addEventListener("swup:enable", setup)
|
scheduleInitialPhotoSwipe()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!photoSwipeState.pageLoadRegistered) {
|
||||||
|
document.addEventListener("astro:page-load", scheduleInitialPhotoSwipe)
|
||||||
|
photoSwipeState.pageLoadRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.swup?.hooks) {
|
||||||
|
registerPhotoSwipeHook()
|
||||||
|
} else {
|
||||||
|
document.addEventListener("swup:enable", registerPhotoSwipeHook, { once: true })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface Props {
|
|||||||
|
|
||||||
const { entry }: Props = Astro.props;
|
const { entry }: Props = Astro.props;
|
||||||
const { Content, headings, remarkPluginFrontmatter } = await render(entry);
|
const { Content, headings, remarkPluginFrontmatter } = await render(entry);
|
||||||
|
const postSlug = entry.id;
|
||||||
|
|
||||||
const jsonLd = {
|
const jsonLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -62,7 +63,7 @@ const customcover = entry.data.customcover;
|
|||||||
{}
|
{}
|
||||||
]}>
|
]}>
|
||||||
<!-- word count and reading time -->
|
<!-- word count and reading time -->
|
||||||
<div class="flex flex-row text-black/30 dark:text-white/30 gap-5 mb-3 transition onload-animation">
|
<div class="flex flex-row flex-wrap text-black/30 dark:text-white/30 gap-5 mb-3 transition onload-animation">
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
||||||
<Icon name="material-symbols:notes-rounded"></Icon>
|
<Icon name="material-symbols:notes-rounded"></Icon>
|
||||||
@@ -77,6 +78,12 @@ const customcover = entry.data.customcover;
|
|||||||
大约 {remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
|
大约 {remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
||||||
|
<Icon name="material-symbols:visibility-outline-rounded"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm" id="post-top-page-views">加载中...</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- title -->
|
<!-- title -->
|
||||||
@@ -164,6 +171,43 @@ const customcover = entry.data.customcover;
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script define:vars={{ slug: postSlug }}>
|
||||||
|
async function fetchTopPageViews() {
|
||||||
|
const displayElement = document.getElementById('post-top-page-views');
|
||||||
|
if (!displayElement || displayElement.dataset.umamiState === 'loading' || displayElement.dataset.umamiState === 'loaded') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayElement.dataset.umamiState = 'loading';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const umamiStore = window['__blogUmami'];
|
||||||
|
const statsData = await umamiStore?.getStats(`post:${slug}`, (websiteId) => {
|
||||||
|
const currentTimestamp = Date.now();
|
||||||
|
return `https://umami.adclosenn.top/api/websites/${websiteId}/stats?startAt=0&endAt=${currentTimestamp}&unit=hour&timezone=${encodeURIComponent('Asia/Shanghai')}&url=%2Fposts%2F${slug}%2F&compare=false`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!statsData) {
|
||||||
|
throw new Error('统计功能未启用');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageViews = statsData.pageviews?.value || 0;
|
||||||
|
displayElement.textContent = `浏览量 ${pageViews}`;
|
||||||
|
displayElement.dataset.umamiState = 'loaded';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching top page views:', error);
|
||||||
|
displayElement.textContent = '统计不可用';
|
||||||
|
displayElement.dataset.umamiState = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', fetchTopPageViews);
|
||||||
|
} else {
|
||||||
|
fetchTopPageViews();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- 评论区 -->
|
<!-- 评论区 -->
|
||||||
<script src="https://giscus.app/client.js"
|
<script src="https://giscus.app/client.js"
|
||||||
data-repo="Ad-closeNN/blog-friends"
|
data-repo="Ad-closeNN/blog-friends"
|
||||||
|
|||||||
Reference in New Issue
Block a user