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 { defineConfig } from 'astro/config';
|
||||||
import sitemap from "@astrojs/sitemap";
|
import sitemap from "@astrojs/sitemap";
|
||||||
import svelte from "@astrojs/svelte";
|
import svelte from "@astrojs/svelte";
|
||||||
|
import rehypeMermaid from "rehype-mermaid";
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
|
@ -18,4 +19,7 @@ export default defineConfig({
|
||||||
"/": "/zh",
|
"/": "/zh",
|
||||||
},
|
},
|
||||||
integrations: [sitemap(), svelte()],
|
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/sitemap": "^3.7.1",
|
||||||
"@astrojs/svelte": "^8.0.4",
|
"@astrojs/svelte": "^8.0.4",
|
||||||
"astro": "^6.0.8",
|
"astro": "^6.0.8",
|
||||||
|
"playwright": "^1.59.1",
|
||||||
|
"rehype-mermaid": "^3.0.0",
|
||||||
"url": "^0.11.4"
|
"url": "^0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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">
|
<nav class="site-nav" aria-label="Site navigation">
|
||||||
<div class="site-nav-desktop">
|
<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}/about/`}>{t.nav.about}</a>
|
||||||
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
||||||
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
||||||
|
|
@ -17,7 +17,7 @@ const t = getTranslations(lang);
|
||||||
<details class="site-nav-mobile">
|
<details class="site-nav-mobile">
|
||||||
<summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary>
|
<summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary>
|
||||||
<div class="dropdown-menu">
|
<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}/about/`}>{t.nav.about}</a>
|
||||||
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
||||||
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import "@/styles/global.css";
|
import "@/styles/global.css";
|
||||||
import Remark42Count from "@/components/remark42-counter.svelte";
|
import Remark42Counter from "@/components/Remark42Counter.astro";
|
||||||
import { getLangFromUrl } from "@/i18n";
|
import { getLangFromUrl } from "@/i18n";
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url);
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
|
@ -11,10 +11,11 @@ const tags = data.tags;
|
||||||
<li class="post-card">
|
<li class="post-card">
|
||||||
<div class="post-link">
|
<div class="post-link">
|
||||||
<div class="post-text">
|
<div class="post-text">
|
||||||
<a href={data.url} class="post-title">
|
<a href={data.url} class="post-title-link">
|
||||||
{data.title}
|
<h2 class="post-title" transition:name={`post-title-${data.slug}`}>
|
||||||
|
{data.title}
|
||||||
|
</h2>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href={data.url} class="post-description">
|
<a href={data.url} class="post-description">
|
||||||
{data.description}
|
{data.description}
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -32,7 +33,7 @@ const tags = data.tags;
|
||||||
<span class="post-date">{data.date}</span>
|
<span class="post-date">{data.date}</span>
|
||||||
<span class="post-stats">
|
<span class="post-stats">
|
||||||
<span class="post-stat">
|
<span class="post-stat">
|
||||||
💬 <Remark42Count url={data.postPath} client:idle />
|
💬 <Remark42Counter url={data.postPath} />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,7 +134,13 @@ const tags = data.tags;
|
||||||
min-height: calc(1.6em * 4);
|
min-height: calc(1.6em * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-title-link {
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.post-title {
|
.post-title {
|
||||||
|
margin: 0;
|
||||||
font-family:
|
font-family:
|
||||||
system-ui,
|
system-ui,
|
||||||
-apple-system,
|
-apple-system,
|
||||||
|
|
@ -231,8 +238,11 @@ const tags = data.tags;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-title {
|
.post-title-link {
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const filteredPosts = sortedPosts.filter((post: any) => {
|
||||||
return postLang === lang;
|
return postLang === lang;
|
||||||
});
|
});
|
||||||
|
|
||||||
const latestPosts = filteredPosts.slice(0, 5);
|
const latestPosts = filteredPosts.slice(0, 7);
|
||||||
---
|
---
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -38,6 +38,7 @@ const latestPosts = filteredPosts.slice(0, 5);
|
||||||
date={formattedDate}
|
date={formattedDate}
|
||||||
img={post.data.image.url}
|
img={post.data.image.url}
|
||||||
tags={post.data.tags}
|
tags={post.data.tags}
|
||||||
|
slug={slug}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,11 @@ const groupedPosts = filteredPosts.reduce((acc: any[], post: any) => {
|
||||||
<a
|
<a
|
||||||
href={`/${lang}/posts/${slug}/`}
|
href={`/${lang}/posts/${slug}/`}
|
||||||
class="timeline-card"
|
class="timeline-card"
|
||||||
data-astro-reload
|
|
||||||
>
|
>
|
||||||
<h2 class="post-title">
|
<h2
|
||||||
|
class="post-title"
|
||||||
|
transition:name={`post-title-${slug}`}
|
||||||
|
>
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="post-description">
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a class="lang-switch" href={switchHref} data-astro-reload>
|
<a class="lang-switch" href={switchHref}>
|
||||||
{switchLabel}
|
{switchLabel}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 Footer from "@/components/Footer.astro";
|
||||||
import Header from "@/components/Header.astro";
|
import Header from "@/components/Header.astro";
|
||||||
import { ClientRouter } from "astro:transitions";
|
import { ClientRouter } from "astro:transitions";
|
||||||
|
import { slide, fade } from "astro:transitions";
|
||||||
import SEO from "@/components/SEO.astro";
|
import SEO from "@/components/SEO.astro";
|
||||||
|
import FloatingActions from "@/components/FloatingActions.astro";
|
||||||
|
import Spinner from "@/components/Spinner.astro";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pageTitle,
|
pageTitle,
|
||||||
|
|
@ -14,6 +17,7 @@ const {
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<ClientRouter />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<SEO title={pageTitle} description={description} image={image} />
|
<SEO title={pageTitle} description={description} image={image} />
|
||||||
|
|
@ -22,51 +26,15 @@ const {
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>{pageTitle}</title>
|
<title>{pageTitle}</title>
|
||||||
</head>
|
</head>
|
||||||
<div class="floating-actions">
|
<FloatingActions />
|
||||||
<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>
|
|
||||||
<body>
|
<body>
|
||||||
<Header />
|
<Header />
|
||||||
<main class="page-content">
|
<Spinner />
|
||||||
|
<main
|
||||||
|
class="page-content"
|
||||||
|
transition:animate={fade({ duration: "0.05s" })}
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
@ -80,184 +48,4 @@ const {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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>
|
</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 BaseLayout from "./BaseLayout.astro";
|
||||||
import Remark42Embed from "@/components/remark42-embed.svelte";
|
import Remark42Embed from "@/components/Remark42Embed.astro";
|
||||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
import { getLangFromUrl, getTranslations } from "@/i18n";
|
||||||
import "@/styles/global.css";
|
import "@/styles/global.css";
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ const t = getTranslations(lang);
|
||||||
<article class="post-article">
|
<article class="post-article">
|
||||||
<div class="post-header">
|
<div class="post-header">
|
||||||
<div class="post-meta">
|
<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="description"><em>{frontmatter.description}</em></p>
|
||||||
<p class="meta-line">
|
<p class="meta-line">
|
||||||
{t.post.publishedOn}: {frontmatter.pubDate.toLocaleDateString()}
|
{t.post.publishedOn}: {frontmatter.pubDate.toLocaleDateString()}
|
||||||
|
|
@ -71,7 +71,7 @@ const t = getTranslations(lang);
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{comments}</h2>
|
<h2>{comments}</h2>
|
||||||
<Remark42Embed slug={postId} client:load />
|
<Remark42Embed slug={postId} />
|
||||||
</article>
|
</article>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||||
import PostList from "@/components/Posts/PostList.astro";
|
import PostList from "@/components/Posts/PostList.astro";
|
||||||
import Remark42LatestComments from "@/components/remark42-latest-comments.svelte";
|
import Remark42LatestComments from "@/components/remark42-latest-comments.svelte";
|
||||||
|
import LatestComments from "@/components/LatestComments.astro";
|
||||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
import { getLangFromUrl, getTranslations } from "@/i18n";
|
||||||
import "@/styles/global.css";
|
import "@/styles/global.css";
|
||||||
|
|
||||||
|
|
@ -50,7 +51,8 @@ const pageTitle = t.home.title;
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
<Remark42LatestComments lang={lang} client:load />
|
<!-- <Remark42LatestComments lang={lang} client:load /> -->
|
||||||
|
<LatestComments />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -163,4 +163,267 @@ img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
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