mirror of
https://github.com/ClovertaTheTrilobita/SanYeCao-blog.git
synced 2026-07-03 15:41:26 +00:00
Compare commits
12 commits
c2aa6c2a68
...
c1db660259
| Author | SHA1 | Date | |
|---|---|---|---|
| c1db660259 | |||
| 2f279848b9 | |||
|
|
e12bcc0b24 | ||
| 8dee31ca16 | |||
| 9c8c0e186f | |||
| 74b3b723cf | |||
| 2b2222c740 | |||
| a06f3d524f | |||
| fdd55fb068 | |||
| 07fbc4b731 | |||
| 018ad4ab1e | |||
| 703a63fffc |
20 changed files with 2417 additions and 611 deletions
|
|
@ -2,6 +2,7 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import svelte from "@astrojs/svelte";
|
||||
import rehypeMermaid from "rehype-mermaid";
|
||||
// https://astro.build/config
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
|
|
@ -18,4 +19,7 @@ export default defineConfig({
|
|||
"/": "/zh",
|
||||
},
|
||||
integrations: [sitemap(), svelte()],
|
||||
markdown: {
|
||||
rehypePlugins: [rehypeMermaid],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
1349
package-lock.json
generated
1349
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,8 @@
|
|||
"@astrojs/sitemap": "^3.7.1",
|
||||
"@astrojs/svelte": "^8.0.4",
|
||||
"astro": "^6.0.8",
|
||||
"playwright": "^1.59.1",
|
||||
"rehype-mermaid": "^3.0.0",
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
224
src/components/FloatingActions.astro
Normal file
224
src/components/FloatingActions.astro
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
<div class="floating-actions">
|
||||
<button
|
||||
id="theme-toggle-fab"
|
||||
aria-label="Toggle theme"
|
||||
title="Toggle theme"
|
||||
>
|
||||
<svg
|
||||
class="theme-toggle-icon"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
class="sun"
|
||||
fill-rule="evenodd"
|
||||
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
|
||||
></path>
|
||||
<path
|
||||
class="moon"
|
||||
fill-rule="evenodd"
|
||||
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="back-to-top" aria-label="Back to top" title="Back to top">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M18 15l-6-6-6 6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.floating-actions {
|
||||
position: fixed;
|
||||
right: 1.25rem;
|
||||
bottom: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#theme-toggle-fab,
|
||||
#back-to-top {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: #a7a7a7 1.5px solid;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #222;
|
||||
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
#theme-toggle-fab {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#theme-toggle-fab {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theme-toggle-icon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.theme-toggle-icon .sun,
|
||||
.theme-toggle-icon .moon {
|
||||
transform-origin: center;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle-icon .sun {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
.theme-toggle-icon .moon {
|
||||
opacity: 0;
|
||||
transform: scale(0.75) rotate(-20deg);
|
||||
}
|
||||
|
||||
:global(.dark) .theme-toggle-icon .sun {
|
||||
opacity: 0;
|
||||
transform: scale(0.75) rotate(20deg);
|
||||
}
|
||||
|
||||
:global(.dark) .theme-toggle-icon .moon {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
#theme-toggle-fab,
|
||||
#back-to-top {
|
||||
/* opacity: 0; */
|
||||
visibility: hidden;
|
||||
transform: translateY(150px);
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.27s ease,
|
||||
visibility 0.2s ease;
|
||||
}
|
||||
|
||||
#theme-toggle-fab.show,
|
||||
#back-to-top.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#theme-toggle-fab.show:hover,
|
||||
#back-to-top.show:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
:global(.dark) #theme-toggle-fab,
|
||||
:global(.dark) #back-to-top {
|
||||
background: rgba(34, 34, 34, 0.92);
|
||||
color: #f5f5f5;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
|
||||
border: #515151 2px solid;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.floating-actions {
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
#theme-toggle-fab,
|
||||
#back-to-top {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const initFloatingActions = () => {
|
||||
const backToTop = document.getElementById("back-to-top");
|
||||
const themeToggle = document.getElementById("theme-toggle-fab");
|
||||
|
||||
if (backToTop && backToTop.dataset.bound !== "true") {
|
||||
const toggleBackToTop = () => {
|
||||
if (window.scrollY > 300) {
|
||||
backToTop.classList.add("show");
|
||||
} else {
|
||||
backToTop.classList.remove("show");
|
||||
}
|
||||
};
|
||||
|
||||
backToTop.addEventListener("click", () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", toggleBackToTop, {
|
||||
passive: true,
|
||||
});
|
||||
toggleBackToTop();
|
||||
backToTop.dataset.bound = "true";
|
||||
}
|
||||
|
||||
if (themeToggle && themeToggle.dataset.bound !== "true") {
|
||||
const togglethemeToggle = () => {
|
||||
if (window.scrollY > 300) {
|
||||
themeToggle.classList.add("show");
|
||||
} else {
|
||||
themeToggle.classList.remove("show");
|
||||
}
|
||||
};
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
const applyTheme = (theme: string) => {
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
localStorage.setItem("color-theme", "dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
localStorage.setItem("color-theme", "light");
|
||||
}
|
||||
};
|
||||
|
||||
themeToggle.addEventListener("click", () => {
|
||||
const isDark = root.classList.contains("dark");
|
||||
applyTheme(isDark ? "light" : "dark");
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", togglethemeToggle, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
togglethemeToggle();
|
||||
themeToggle.dataset.bound = "true";
|
||||
}
|
||||
};
|
||||
|
||||
initFloatingActions();
|
||||
document.addEventListener("astro:page-load", initFloatingActions);
|
||||
</script>
|
||||
299
src/components/LatestComments.astro
Normal file
299
src/components/LatestComments.astro
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
---
|
||||
const { lang = "zh" } = Astro.props;
|
||||
const host = import.meta.env.PUBLIC_REMARK42_HOST;
|
||||
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||
const max = 10;
|
||||
const heading = lang === "zh" ? "最新评论" : "Latest comments";
|
||||
---
|
||||
|
||||
<section
|
||||
class="latest-comments"
|
||||
data-lang={lang}
|
||||
data-host={host}
|
||||
data-site-id={siteId}
|
||||
data-max={max}
|
||||
>
|
||||
<h2>{heading}</h2>
|
||||
|
||||
<div class="comments-loading" aria-hidden="true">
|
||||
<div class="loading-card"></div>
|
||||
<div class="loading-card"></div>
|
||||
<div class="loading-card"></div>
|
||||
</div>
|
||||
|
||||
<div class="latest-comments-list" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<script is:inline data-astro-rerun>
|
||||
function escapeHtml(str) {
|
||||
return String(str ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function formatTime(input, lang) {
|
||||
try {
|
||||
const date = new Date(input);
|
||||
return new Intl.DateTimeFormat(lang === "zh" ? "zh-CN" : "en", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function rewritePostUrl(url, lang) {
|
||||
if (!url) return "#";
|
||||
|
||||
if (url.startsWith("/posts/")) {
|
||||
return `/${lang}${url}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const u = new URL(url, window.location.origin);
|
||||
if (u.pathname.startsWith("/posts/")) {
|
||||
return `/${lang}${u.pathname}${u.search}${u.hash}`;
|
||||
}
|
||||
return u.pathname + u.search + u.hash;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function getAvatar(item) {
|
||||
return (
|
||||
item?.user?.picture || item?.user?.photo || item?.user?.avatar || ""
|
||||
);
|
||||
}
|
||||
|
||||
function getUserName(item, lang) {
|
||||
return item?.user?.name || (lang === "zh" ? "匿名用户" : "Anonymous");
|
||||
}
|
||||
|
||||
function normalizePath(url) {
|
||||
if (!url) return "";
|
||||
|
||||
try {
|
||||
const u = new URL(url, window.location.origin);
|
||||
let path = u.pathname;
|
||||
|
||||
if (!path.startsWith("/")) path = "/" + path;
|
||||
if (!path.endsWith("/")) path += "/";
|
||||
|
||||
return path;
|
||||
} catch {
|
||||
let path = String(url);
|
||||
|
||||
path = path.replace(/^https?:\/\/[^/]+/i, "");
|
||||
if (!path.startsWith("/")) path = "/" + path;
|
||||
if (!path.endsWith("/")) path += "/";
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrlCandidates(rawUrl) {
|
||||
const path = normalizePath(rawUrl);
|
||||
if (!path) return [];
|
||||
|
||||
const noSlash = path.endsWith("/") ? path.slice(0, -1) : path;
|
||||
|
||||
const candidates = [
|
||||
path,
|
||||
noSlash,
|
||||
`/zh${path}`,
|
||||
`/zh${noSlash}`,
|
||||
`/en${path}`,
|
||||
`/en${noSlash}`,
|
||||
`${window.location.origin}${path}`,
|
||||
`${window.location.origin}${noSlash}`,
|
||||
];
|
||||
|
||||
return [...new Set(candidates.filter(Boolean))];
|
||||
}
|
||||
|
||||
async function fetchPostComments(host, siteId, rawUrl) {
|
||||
const candidates = buildUrlCandidates(rawUrl);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${host}/api/v1/find?site=${encodeURIComponent(siteId)}&url=${encodeURIComponent(candidate)}&format=plain`,
|
||||
{ credentials: "omit" },
|
||||
);
|
||||
|
||||
if (!res.ok) continue;
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const comments = Array.isArray(data)
|
||||
? data
|
||||
: Array.isArray(data?.comments)
|
||||
? data.comments
|
||||
: [];
|
||||
|
||||
if (comments.length > 0) {
|
||||
return comments;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function enrichReplyTargets(host, siteId, comments) {
|
||||
const urls = [
|
||||
...new Set(
|
||||
comments.map((item) => item?.locator?.url).filter(Boolean),
|
||||
),
|
||||
];
|
||||
|
||||
const postMap = new Map();
|
||||
|
||||
await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
const postComments = await fetchPostComments(host, siteId, url);
|
||||
postMap.set(url, postComments);
|
||||
}),
|
||||
);
|
||||
|
||||
return comments.map((item) => {
|
||||
if (!item?.pid) {
|
||||
return { ...item, replyToName: "" };
|
||||
}
|
||||
|
||||
const postComments = postMap.get(item?.locator?.url) || [];
|
||||
const parent = postComments.find(
|
||||
(c) => String(c?.id) === String(item?.pid),
|
||||
);
|
||||
|
||||
return {
|
||||
...item,
|
||||
replyToName: parent?.user?.name || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function renderComments(root, comments, lang) {
|
||||
const list = root.querySelector(".latest-comments-list");
|
||||
const loading = root.querySelector(".comments-loading");
|
||||
if (!list || !loading) return;
|
||||
|
||||
if (!Array.isArray(comments) || comments.length === 0) {
|
||||
loading.style.display = "none";
|
||||
list.innerHTML = `<p class="latest-comments-empty">${
|
||||
lang === "zh" ? "还没有评论" : "No comments yet"
|
||||
}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = comments
|
||||
.map((item) => {
|
||||
const rawAuthor = getUserName(item, lang);
|
||||
const author = escapeHtml(rawAuthor);
|
||||
const fallbackInitial = escapeHtml(
|
||||
rawAuthor.slice(0, 1).toUpperCase(),
|
||||
);
|
||||
|
||||
const avatar = getAvatar(item);
|
||||
const safeAvatar = escapeHtml(avatar || "");
|
||||
|
||||
const title = escapeHtml(
|
||||
item?.title ||
|
||||
(lang === "zh" ? "未命名文章" : "Untitled post"),
|
||||
);
|
||||
const href = rewritePostUrl(item?.locator?.url || "", lang);
|
||||
|
||||
// Remark42 返回的 text 是处理后的 HTML,适合直接渲染
|
||||
const html = item?.text || "";
|
||||
const time = formatTime(item?.time, lang);
|
||||
|
||||
const replyTo = escapeHtml(item?.replyToName || "");
|
||||
const authorLine = replyTo
|
||||
? `<span class="comment-author">@${author}</span><span class="comment-reply-sep">${
|
||||
lang === "zh" ? "回复" : "replied to"
|
||||
}</span><span class="comment-reply-to">@${replyTo}</span>`
|
||||
: `<span class="comment-author">@${author}</span>`;
|
||||
|
||||
const avatarHtml = avatar
|
||||
? `<img class="comment-avatar-img" src="${safeAvatar}" alt="${author}" loading="lazy" referrerpolicy="no-referrer" />`
|
||||
: `<div class="comment-avatar-fallback" aria-hidden="true">${fallbackInitial}</div>`;
|
||||
|
||||
return `
|
||||
<article class="comment-card">
|
||||
<div class="comment-card-body">
|
||||
<div class="comment-card-info">
|
||||
<div class="comment-avatar">
|
||||
${avatarHtml}
|
||||
</div>
|
||||
|
||||
<div class="comment-meta">
|
||||
<div class="comment-author-row">
|
||||
${authorLine}
|
||||
${time ? `<span class="comment-time">${escapeHtml(time)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment-card-title">
|
||||
<a class="comment-title-link" href="${href}">${title}</a>
|
||||
</div>
|
||||
|
||||
<div class="comment-card-text">${html}</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
loading.style.display = "none";
|
||||
}
|
||||
|
||||
async function loadLatestComments(section) {
|
||||
if (!section) return;
|
||||
|
||||
const host = section.dataset.host;
|
||||
const siteId = section.dataset.siteId;
|
||||
const max = section.dataset.max || "10";
|
||||
const lang = section.dataset.lang || "zh";
|
||||
|
||||
const list = section.querySelector(".latest-comments-list");
|
||||
const loading = section.querySelector(".comments-loading");
|
||||
if (!host || !siteId || !list || !loading) return;
|
||||
|
||||
loading.style.display = "";
|
||||
list.innerHTML = "";
|
||||
|
||||
const url = `${host}/api/v1/last/${encodeURIComponent(max)}?site=${encodeURIComponent(siteId)}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { credentials: "omit" });
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const enriched = await enrichReplyTargets(
|
||||
host,
|
||||
siteId,
|
||||
Array.isArray(data) ? data : [],
|
||||
);
|
||||
renderComments(section, enriched, lang);
|
||||
} catch (err) {
|
||||
loading.style.display = "none";
|
||||
list.innerHTML = `<p class="latest-comments-empty">${
|
||||
lang === "zh"
|
||||
? "最新评论加载失败"
|
||||
: "Failed to load latest comments"
|
||||
}</p>`;
|
||||
console.error("latest comments load failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll(".latest-comments").forEach(loadLatestComments);
|
||||
</script>
|
||||
|
|
@ -7,7 +7,7 @@ const t = getTranslations(lang);
|
|||
|
||||
<nav class="site-nav" aria-label="Site navigation">
|
||||
<div class="site-nav-desktop">
|
||||
<a href={`/${lang}/`} data-astro-reload>{t.nav.home}</a>
|
||||
<a href={`/${lang}/`}>{t.nav.home}</a>
|
||||
<a href={`/${lang}/about/`}>{t.nav.about}</a>
|
||||
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
||||
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
||||
|
|
@ -17,7 +17,7 @@ const t = getTranslations(lang);
|
|||
<details class="site-nav-mobile">
|
||||
<summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary>
|
||||
<div class="dropdown-menu">
|
||||
<a href={`/${lang}/`} data-astro-reload>{t.nav.home}</a>
|
||||
<a href={`/${lang}/`}>{t.nav.home}</a>
|
||||
<a href={`/${lang}/about/`}>{t.nav.about}</a>
|
||||
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
||||
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
import "@/styles/global.css";
|
||||
import Remark42Count from "@/components/remark42-counter.svelte";
|
||||
import Remark42Counter from "@/components/Remark42Counter.astro";
|
||||
import { getLangFromUrl } from "@/i18n";
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
|
|
@ -11,10 +11,11 @@ const tags = data.tags;
|
|||
<li class="post-card">
|
||||
<div class="post-link">
|
||||
<div class="post-text">
|
||||
<a href={data.url} class="post-title">
|
||||
<a href={data.url} class="post-title-link">
|
||||
<h2 class="post-title" transition:name={`post-title-${data.slug}`}>
|
||||
{data.title}
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<a href={data.url} class="post-description">
|
||||
{data.description}
|
||||
</a>
|
||||
|
|
@ -32,7 +33,7 @@ const tags = data.tags;
|
|||
<span class="post-date">{data.date}</span>
|
||||
<span class="post-stats">
|
||||
<span class="post-stat">
|
||||
💬 <Remark42Count url={data.postPath} client:idle />
|
||||
💬 <Remark42Counter url={data.postPath} />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -133,7 +134,13 @@ const tags = data.tags;
|
|||
min-height: calc(1.6em * 4);
|
||||
}
|
||||
|
||||
.post-title-link {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin: 0;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
|
|
@ -231,8 +238,11 @@ const tags = data.tags;
|
|||
min-height: auto;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
.post-title-link {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const filteredPosts = sortedPosts.filter((post: any) => {
|
|||
return postLang === lang;
|
||||
});
|
||||
|
||||
const latestPosts = filteredPosts.slice(0, 5);
|
||||
const latestPosts = filteredPosts.slice(0, 7);
|
||||
---
|
||||
|
||||
<ul>
|
||||
|
|
@ -38,6 +38,7 @@ const latestPosts = filteredPosts.slice(0, 5);
|
|||
date={formattedDate}
|
||||
img={post.data.image.url}
|
||||
tags={post.data.tags}
|
||||
slug={slug}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -51,9 +51,11 @@ const groupedPosts = filteredPosts.reduce((acc: any[], post: any) => {
|
|||
<a
|
||||
href={`/${lang}/posts/${slug}/`}
|
||||
class="timeline-card"
|
||||
data-astro-reload
|
||||
>
|
||||
<h2 class="post-title">
|
||||
<h2
|
||||
class="post-title"
|
||||
transition:name={`post-title-${slug}`}
|
||||
>
|
||||
{post.data.title}
|
||||
</h2>
|
||||
<p class="post-description">
|
||||
|
|
|
|||
66
src/components/Remark42Counter.astro
Normal file
66
src/components/Remark42Counter.astro
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
const { url } = Astro.props;
|
||||
const host = import.meta.env.PUBLIC_REMARK42_HOST;
|
||||
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||
---
|
||||
|
||||
<span
|
||||
class="remark42__counter"
|
||||
data-url={url}
|
||||
data-host={host}
|
||||
data-site-id={siteId}></span>
|
||||
|
||||
<script is:inline data-astro-rerun>
|
||||
function getTheme() {
|
||||
return document.documentElement.classList.contains("dark")
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
function mountRemark42Counters() {
|
||||
const counters = Array.from(
|
||||
document.querySelectorAll(".remark42__counter[data-url]"),
|
||||
);
|
||||
|
||||
if (!counters.length) return;
|
||||
|
||||
const first = counters[0];
|
||||
const host = first.dataset.host;
|
||||
const siteId = first.dataset.siteId;
|
||||
|
||||
if (!host || !siteId) return;
|
||||
|
||||
window.remark_config = {
|
||||
host,
|
||||
site_id: siteId,
|
||||
components: ["counter"],
|
||||
show_rss_subscription: false,
|
||||
theme: getTheme(),
|
||||
};
|
||||
|
||||
for (const el of counters) {
|
||||
el.textContent = "";
|
||||
}
|
||||
|
||||
document.getElementById("remark42-counter-loader")?.remove();
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.id = "remark42-counter-loader";
|
||||
script.defer = true;
|
||||
|
||||
if ("noModule" in script) {
|
||||
script.type = "module";
|
||||
script.src = `${host}/web/counter.mjs?ts=${Date.now()}`;
|
||||
} else {
|
||||
script.async = true;
|
||||
script.src = `${host}/web/counter.js?ts=${Date.now()}`;
|
||||
}
|
||||
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
clearTimeout(window.__remark42CounterMountTimer);
|
||||
window.__remark42CounterMountTimer = setTimeout(() => {
|
||||
mountRemark42Counters();
|
||||
}, 0);
|
||||
</script>
|
||||
82
src/components/Remark42Embed.astro
Normal file
82
src/components/Remark42Embed.astro
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
const { slug } = Astro.props;
|
||||
const pagePath = `/posts/${slug}/`;
|
||||
const host = import.meta.env.PUBLIC_REMARK42_HOST;
|
||||
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||
---
|
||||
|
||||
<div id="remark42"></div>
|
||||
|
||||
<script define:vars={{ pagePath, host, siteId }} is:inline data-astro-rerun>
|
||||
function getTheme() {
|
||||
return document.documentElement.classList.contains("dark")
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
function setRemarkConfig() {
|
||||
window.remark_config = {
|
||||
host,
|
||||
site_id: siteId,
|
||||
url: pagePath,
|
||||
page_id: pagePath,
|
||||
theme: getTheme(),
|
||||
components: ["embed"],
|
||||
show_rss_subscription: false,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureScript() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const existing = document.getElementById("remark42-loader");
|
||||
|
||||
if (existing) {
|
||||
if (window.REMARK42) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
existing.addEventListener("load", () => resolve(true), {
|
||||
once: true,
|
||||
});
|
||||
existing.addEventListener("error", reject, { once: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.id = "remark42-loader";
|
||||
script.defer = true;
|
||||
|
||||
if ("noModule" in script) {
|
||||
script.type = "module";
|
||||
script.src = `${host}/web/embed.mjs`;
|
||||
} else {
|
||||
script.async = true;
|
||||
script.src = `${host}/web/embed.js`;
|
||||
}
|
||||
|
||||
script.onload = () => resolve(true);
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
async function mountRemark42() {
|
||||
const node = document.getElementById("remark42");
|
||||
if (!node) return;
|
||||
|
||||
setRemarkConfig();
|
||||
await ensureScript();
|
||||
|
||||
if (window.REMARK42) {
|
||||
if (typeof window.REMARK42.destroy === "function") {
|
||||
window.REMARK42.destroy();
|
||||
}
|
||||
if (typeof window.REMARK42.createInstance === "function") {
|
||||
node.innerHTML = "";
|
||||
window.REMARK42.createInstance(window.remark_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mountRemark42();
|
||||
</script>
|
||||
95
src/components/Spinner.astro
Normal file
95
src/components/Spinner.astro
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<div id="page-loading-indicator" aria-hidden="true">
|
||||
<span class="page-loading-spinner"></span>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
(() => {
|
||||
let timer;
|
||||
|
||||
const show = () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
const el = document.getElementById("page-loading-indicator");
|
||||
if (el) el.classList.add("is-active");
|
||||
}, 120);
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
clearTimeout(timer);
|
||||
const el = document.getElementById("page-loading-indicator");
|
||||
if (el) el.classList.remove("is-active");
|
||||
};
|
||||
|
||||
document.addEventListener("astro:before-preparation", show);
|
||||
document.addEventListener("astro:page-load", hide);
|
||||
|
||||
hide();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
#page-loading-indicator {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
opacity: 0;
|
||||
transform: scale(1.3);
|
||||
pointer-events: none;
|
||||
/* transition:
|
||||
opacity 160ms ease,
|
||||
transform 160ms ease; */
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
#page-loading-indicator.is-active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.page-loading-spinner {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
box-sizing: border-box;
|
||||
border: 1.5px solid #7da2ff;
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
box-shadow:
|
||||
0 0 0 1px #606060,
|
||||
inset 0 0 0 1px #606060;
|
||||
animation: page-loading-spin-rhythm 2.1s infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
@keyframes page-loading-spin-rhythm {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
animation-timing-function: cubic-bezier(0.25, 0.75, 0.35, 1);
|
||||
}
|
||||
|
||||
52% {
|
||||
transform: rotate(720deg); /* 前两圈,稍微慢一点 */
|
||||
animation-timing-function: cubic-bezier(0.55, 0.08, 0.78, 0.22);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(1080deg); /* 最后一圈更慢 */
|
||||
}
|
||||
}
|
||||
|
||||
.dark .page-loading-spinner,
|
||||
html.dark .page-loading-spinner {
|
||||
border-color: #d8c7a1;
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
@keyframes page-loading-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -34,7 +34,7 @@ const switchHref = "/" + segments.join("/");
|
|||
</svg>
|
||||
</button>
|
||||
|
||||
<a class="lang-switch" href={switchHref} data-astro-reload>
|
||||
<a class="lang-switch" href={switchHref}>
|
||||
{switchLabel}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let url;
|
||||
|
||||
const host = import.meta.env.PUBLIC_REMARK42_HOST;
|
||||
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||
|
||||
onMount(() => {
|
||||
const remark_config = {
|
||||
host,
|
||||
site_id: siteId,
|
||||
components: ["counter"],
|
||||
show_rss_subscription: false,
|
||||
theme: localStorage.getItem("color-theme") ?? "light",
|
||||
};
|
||||
|
||||
window.remark_config = remark_config;
|
||||
|
||||
for (const name of remark_config.components || ["embed"]) {
|
||||
const script = document.createElement("script");
|
||||
let ext = ".js";
|
||||
|
||||
if ("noModule" in script) {
|
||||
script.type = "module";
|
||||
ext = ".mjs";
|
||||
} else {
|
||||
script.async = true;
|
||||
}
|
||||
|
||||
script.defer = true;
|
||||
script.src = `${remark_config.host}/web/${name}${ext}`;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="remark42__counter" data-url={url}></span>
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let slug;
|
||||
|
||||
let pagePath = `/posts/${slug}/`;
|
||||
|
||||
function getTheme() {
|
||||
return document.documentElement.classList.contains("dark")
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
function applyRemark42Theme() {
|
||||
if (window.REMARK42 && typeof window.REMARK42.changeTheme === "function") {
|
||||
window.REMARK42.changeTheme(getTheme());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const scriptId = "remark42-script";
|
||||
|
||||
window.remark_config = {
|
||||
host: import.meta.env.PUBLIC_REMARK42_HOST,
|
||||
site_id: import.meta.env.PUBLIC_REMARK42_SITE_ID,
|
||||
components: ["embed"],
|
||||
show_rss_subscription: false,
|
||||
theme: getTheme(),
|
||||
url: pagePath,
|
||||
page_id: pagePath,
|
||||
};
|
||||
|
||||
if (!document.getElementById(scriptId)) {
|
||||
const script = document.createElement("script");
|
||||
script.id = scriptId;
|
||||
script.async = true;
|
||||
script.innerHTML = `
|
||||
!function(e,n){
|
||||
for(var o=0;o<e.length;o++){
|
||||
var r=n.createElement("script"),c=".js",d=n.head||n.body;
|
||||
"noModule"in r?(r.type="module",c=".mjs"):r.async=!0;
|
||||
r.defer=!0;
|
||||
r.src=window.remark_config.host+"/web/"+e[o]+c;
|
||||
d.appendChild(r);
|
||||
}
|
||||
}(window.remark_config.components||["embed"],document);
|
||||
`;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
const applyWhenReady = setInterval(() => {
|
||||
if (applyRemark42Theme()) {
|
||||
clearInterval(applyWhenReady);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
applyRemark42Theme();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearInterval(applyWhenReady);
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="remark42"></div>
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let lang = "zh";
|
||||
let loaded = false;
|
||||
|
||||
function rewriteLinks() {
|
||||
const links = document.querySelectorAll(
|
||||
".latest-comments .comment__title-link",
|
||||
);
|
||||
|
||||
links.forEach((link) => {
|
||||
const href = link.getAttribute("href");
|
||||
if (!href) return;
|
||||
|
||||
if (href.startsWith("/posts/")) {
|
||||
link.setAttribute("href", `/${lang}${href}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.remark_config = {
|
||||
host: import.meta.env.PUBLIC_REMARK42_HOST,
|
||||
site_id: import.meta.env.PUBLIC_REMARK42_SITE_ID,
|
||||
components: ["last-comments"],
|
||||
theme: localStorage.getItem("color-theme") ?? "light",
|
||||
max_last_comments: 10,
|
||||
no_footer: true,
|
||||
};
|
||||
|
||||
const loaderId = "remark42-last-comments-loader";
|
||||
if (!document.getElementById(loaderId)) {
|
||||
const init = document.createElement("script");
|
||||
init.id = loaderId;
|
||||
init.innerHTML = `
|
||||
!function(e,n){
|
||||
for(var o=0;o<e.length;o++){
|
||||
var r=n.createElement("script"),c=".js",d=n.head||n.body;
|
||||
"noModule"in r?(r.type="module",c=".mjs"):r.async=!0;
|
||||
r.defer=!0;
|
||||
r.src=window.remark_config.host+"/web/"+e[o]+c;
|
||||
d.appendChild(r);
|
||||
}
|
||||
}(window.remark_config.components||["last-comments"],document);
|
||||
`;
|
||||
document.body.appendChild(init);
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const root = document.querySelector(".remark42__last-comments");
|
||||
if (root && root.children.length > 0) {
|
||||
rewriteLinks();
|
||||
loaded = true;
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="latest-comments">
|
||||
<h2>最新评论</h2>
|
||||
|
||||
{#if !loaded}
|
||||
<div class="comments-loading" aria-hidden="true">
|
||||
<div class="loading-card"></div>
|
||||
<div class="loading-card"></div>
|
||||
<div class="loading-card"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="remark42__last-comments" data-max="10"></div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.loading-card {
|
||||
height: 92px;
|
||||
margin: 0 0 14px 0;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px dashed #aeb8c2;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(160, 175, 190, 0.06) 25%,
|
||||
rgba(160, 175, 190, 0.16) 50%,
|
||||
rgba(160, 175, 190, 0.06) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.latest-comments {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.latest-comments h2 {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.latest-comments :global(.remark42__last-comments) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.latest-comments :global(article.comment.list-comments__item) {
|
||||
display: block;
|
||||
margin: 0 0 14px 0;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px dashed #aeb8c2;
|
||||
background: rgba(160, 175, 190, 0.06);
|
||||
}
|
||||
|
||||
.latest-comments :global(article.comment.list-comments__item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.latest-comments
|
||||
:global(article.comment.list-comments__item) {
|
||||
background: #2a3138;
|
||||
border-color: #7f8c97;
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__body) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
grid-template-areas:
|
||||
"info title"
|
||||
"text title";
|
||||
gap: 0.35rem 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__title) {
|
||||
grid-area: title;
|
||||
text-align: right;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__title-link) {
|
||||
color: #6f8090;
|
||||
text-decoration: none;
|
||||
font-size: 0.87rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__title-link:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__info) {
|
||||
grid-area: info;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: inherit;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
font-style: italic;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
"PingFang SC",
|
||||
"Hiragino Sans GB",
|
||||
"Microsoft YaHei",
|
||||
"Noto Sans CJK SC",
|
||||
"Source Han Sans SC",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__info)::before {
|
||||
content: "@";
|
||||
margin-right: 0.08em;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__text) {
|
||||
grid-area: text;
|
||||
margin: 0;
|
||||
color: #555;
|
||||
line-height: 1.7;
|
||||
font-size: 1.03rem;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
"PingFang SC",
|
||||
"Hiragino Sans GB",
|
||||
"Microsoft YaHei",
|
||||
"Noto Sans CJK SC",
|
||||
"Source Han Sans SC",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.latest-comments
|
||||
:global(article.comment.list-comments__item) {
|
||||
border-color: #7f8c97;
|
||||
background: rgba(180, 190, 200, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .latest-comments :global(.comment__title-link) {
|
||||
color: #c8d2dc;
|
||||
}
|
||||
|
||||
:global(.dark) .latest-comments :global(.comment__text) {
|
||||
color: #d3d7db;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.latest-comments :global(.comment__body) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"info"
|
||||
"title"
|
||||
"text";
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__title) {
|
||||
text-align: left;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.latest-comments :global(.comment__title-link) {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
import Footer from "@/components/Footer.astro";
|
||||
import Header from "@/components/Header.astro";
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
import { slide, fade } from "astro:transitions";
|
||||
import SEO from "@/components/SEO.astro";
|
||||
import FloatingActions from "@/components/FloatingActions.astro";
|
||||
import Spinner from "@/components/Spinner.astro";
|
||||
|
||||
const {
|
||||
pageTitle,
|
||||
|
|
@ -14,6 +17,7 @@ const {
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<ClientRouter />
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<SEO title={pageTitle} description={description} image={image} />
|
||||
|
|
@ -22,51 +26,15 @@ const {
|
|||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{pageTitle}</title>
|
||||
</head>
|
||||
<div class="floating-actions">
|
||||
<button
|
||||
id="theme-toggle-fab"
|
||||
aria-label="Toggle theme"
|
||||
title="Toggle theme"
|
||||
>
|
||||
<svg
|
||||
class="theme-toggle-icon"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
class="sun"
|
||||
fill-rule="evenodd"
|
||||
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
|
||||
></path>
|
||||
<path
|
||||
class="moon"
|
||||
fill-rule="evenodd"
|
||||
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<FloatingActions />
|
||||
|
||||
<button id="back-to-top" aria-label="Back to top" title="Back to top">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M18 15l-6-6-6 6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<body>
|
||||
<Header />
|
||||
<main class="page-content">
|
||||
<Spinner />
|
||||
<main
|
||||
class="page-content"
|
||||
transition:animate={fade({ duration: "0.05s" })}
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
|
|
@ -80,184 +48,4 @@ const {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.floating-actions {
|
||||
position: fixed;
|
||||
right: 1.25rem;
|
||||
bottom: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#theme-toggle-fab,
|
||||
#back-to-top {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: #a7a7a7 1.5px solid;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #222;
|
||||
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
#theme-toggle-fab {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#theme-toggle-fab {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theme-toggle-icon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.theme-toggle-icon .sun,
|
||||
.theme-toggle-icon .moon {
|
||||
transform-origin: center;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle-icon .sun {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
.theme-toggle-icon .moon {
|
||||
opacity: 0;
|
||||
transform: scale(0.75) rotate(-20deg);
|
||||
}
|
||||
|
||||
:global(.dark) .theme-toggle-icon .sun {
|
||||
opacity: 0;
|
||||
transform: scale(0.75) rotate(20deg);
|
||||
}
|
||||
|
||||
:global(.dark) .theme-toggle-icon .moon {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
#theme-toggle-fab,
|
||||
#back-to-top {
|
||||
/* opacity: 0; */
|
||||
visibility: hidden;
|
||||
transform: translateY(150px);
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.27s ease,
|
||||
visibility 0.2s ease;
|
||||
}
|
||||
|
||||
#theme-toggle-fab.show,
|
||||
#back-to-top.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#theme-toggle-fab.show:hover,
|
||||
#back-to-top.show:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
:global(.dark) #theme-toggle-fab,
|
||||
:global(.dark) #back-to-top {
|
||||
background: rgba(34, 34, 34, 0.92);
|
||||
color: #f5f5f5;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
|
||||
border: #515151 2px solid;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.floating-actions {
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
#theme-toggle-fab,
|
||||
#back-to-top {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const initFloatingActions = () => {
|
||||
const backToTop = document.getElementById("back-to-top");
|
||||
const themeToggle = document.getElementById("theme-toggle-fab");
|
||||
|
||||
if (backToTop && backToTop.dataset.bound !== "true") {
|
||||
const toggleBackToTop = () => {
|
||||
if (window.scrollY > 300) {
|
||||
backToTop.classList.add("show");
|
||||
} else {
|
||||
backToTop.classList.remove("show");
|
||||
}
|
||||
};
|
||||
|
||||
backToTop.addEventListener("click", () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", toggleBackToTop, {
|
||||
passive: true,
|
||||
});
|
||||
toggleBackToTop();
|
||||
backToTop.dataset.bound = "true";
|
||||
}
|
||||
|
||||
if (themeToggle && themeToggle.dataset.bound !== "true") {
|
||||
const togglethemeToggle = () => {
|
||||
if (window.scrollY > 300) {
|
||||
themeToggle.classList.add("show");
|
||||
} else {
|
||||
themeToggle.classList.remove("show");
|
||||
}
|
||||
};
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
const applyTheme = (theme: string) => {
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
localStorage.setItem("color-theme", "dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
localStorage.setItem("color-theme", "light");
|
||||
}
|
||||
};
|
||||
|
||||
themeToggle.addEventListener("click", () => {
|
||||
const isDark = root.classList.contains("dark");
|
||||
applyTheme(isDark ? "light" : "dark");
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", togglethemeToggle, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
togglethemeToggle();
|
||||
themeToggle.dataset.bound = "true";
|
||||
}
|
||||
};
|
||||
|
||||
initFloatingActions();
|
||||
document.addEventListener("astro:page-load", initFloatingActions);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import Remark42Embed from "@/components/remark42-embed.svelte";
|
||||
import Remark42Embed from "@/components/Remark42Embed.astro";
|
||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
||||
import "@/styles/global.css";
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ const t = getTranslations(lang);
|
|||
<article class="post-article">
|
||||
<div class="post-header">
|
||||
<div class="post-meta">
|
||||
<h1 class="post-title">{frontmatter.title}</h1>
|
||||
<h1 class="post-title" transition:name={`post-title-${postId}`} >{frontmatter.title}</h1>
|
||||
<p class="description"><em>{frontmatter.description}</em></p>
|
||||
<p class="meta-line">
|
||||
{t.post.publishedOn}: {frontmatter.pubDate.toLocaleDateString()}
|
||||
|
|
@ -71,7 +71,7 @@ const t = getTranslations(lang);
|
|||
</div>
|
||||
|
||||
<h2>{comments}</h2>
|
||||
<Remark42Embed slug={postId} client:load />
|
||||
<Remark42Embed slug={postId} />
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import PostList from "@/components/Posts/PostList.astro";
|
||||
import Remark42LatestComments from "@/components/remark42-latest-comments.svelte";
|
||||
import LatestComments from "@/components/LatestComments.astro";
|
||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
||||
import "@/styles/global.css";
|
||||
|
||||
|
|
@ -50,7 +51,8 @@ const pageTitle = t.home.title;
|
|||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<Remark42LatestComments lang={lang} client:load />
|
||||
<!-- <Remark42LatestComments lang={lang} client:load /> -->
|
||||
<LatestComments />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -164,3 +164,266 @@ img {
|
|||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.latest-comments {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.latest-comments h2 {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.loading-card {
|
||||
height: 92px;
|
||||
margin: 0 0 14px 0;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px dashed #aeb8c2;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(160, 175, 190, 0.06) 25%,
|
||||
rgba(160, 175, 190, 0.16) 50%,
|
||||
rgba(160, 175, 190, 0.06) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.latest-comments-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.comment-card {
|
||||
display: block;
|
||||
margin: 0 0 14px 0;
|
||||
padding: 0.6rem 1rem;
|
||||
border: 1.5px dashed #aeb8c2;
|
||||
background-color: #f3f5f7;
|
||||
}
|
||||
|
||||
.comment-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-card-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
grid-template-areas:
|
||||
"info title"
|
||||
"text title";
|
||||
gap: 0.2rem 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.comment-card-info {
|
||||
grid-area: info;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comment-avatar-img,
|
||||
.comment-avatar-fallback {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.comment-avatar-img {
|
||||
border: 1px dashed #aeb8c2;
|
||||
background: #f3f5f7;
|
||||
}
|
||||
|
||||
.comment-avatar-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed #aeb8c2;
|
||||
background: rgba(160, 175, 190, 0.12);
|
||||
color: #5e6b77;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-author-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.6rem;
|
||||
min-width: 0;
|
||||
line-height: 1.3;
|
||||
font-style: italic;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
"PingFang SC",
|
||||
"Hiragino Sans GB",
|
||||
"Microsoft YaHei",
|
||||
"Noto Sans CJK SC",
|
||||
"Source Han Sans SC",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 400;
|
||||
font-size: 0.8rem;
|
||||
color: gray;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
color: #7a7a7a;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.comment-card-title {
|
||||
grid-area: title;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
text-align: right;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.comment-title-link {
|
||||
color: #6f8090;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.comment-title-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.comment-card-text {
|
||||
grid-area: text;
|
||||
margin: 0;
|
||||
color: #555;
|
||||
line-height: 1.5;
|
||||
font-size: 0.93rem;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
"PingFang SC",
|
||||
"Hiragino Sans GB",
|
||||
"Microsoft YaHei",
|
||||
"Noto Sans CJK SC",
|
||||
"Source Han Sans SC",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.comment-card-text :global(p),
|
||||
.comment-card-text p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comment-card-text :global(img),
|
||||
.comment-card-text img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.latest-comments-empty {
|
||||
color: #777;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.comment-reply-sep {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.latest-comments .comment-reply-to {
|
||||
font-weight: 400;
|
||||
font-size: 0.8rem;
|
||||
color: gray;
|
||||
opacity: 0.9;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
html.dark .latest-comments .comment-card,
|
||||
.dark .latest-comments .comment-card {
|
||||
border-color: #7f8c97;
|
||||
background-color: #252525;
|
||||
}
|
||||
|
||||
html.dark .comment-title-link {
|
||||
color: #c8d2dc;
|
||||
}
|
||||
|
||||
html.dark .comment-card-text {
|
||||
color: #d3d7db;
|
||||
}
|
||||
|
||||
html.dark .comment-time {
|
||||
color: #a8b0b7;
|
||||
}
|
||||
|
||||
html.dark .latest-comments .comment-avatar-fallback,
|
||||
.dark .latest-comments .comment-avatar-fallback {
|
||||
border-color: #7f8c97;
|
||||
background-color: #3a444d;
|
||||
color: #dbe3ea;
|
||||
}
|
||||
|
||||
:global(.dark) .comment-avatar-fallback {
|
||||
background: rgba(180, 190, 200, 0.12);
|
||||
color: #dbe3ea;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.comment-card-body {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"title"
|
||||
"info"
|
||||
"text";
|
||||
}
|
||||
|
||||
.comment-card-title {
|
||||
text-align: left;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.comment-title-link {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue