mirror of
https://github.com/ClovertaTheTrilobita/SanYeCao-blog.git
synced 2026-07-05 00:21:27 +00:00
Compare commits
68 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f47501d79d | ||
| e6fb5a4519 | |||
|
|
715d6c9b7b | ||
|
|
1bb9d56e03 | ||
|
|
5673fc7ddb | ||
|
|
7067e43cb4 | ||
|
|
1901b50062 | ||
|
|
94cbaf7707 | ||
| c9d1bacec4 | |||
| 6e84ef4884 | |||
| 06999dae80 | |||
| eeff9fa4f6 | |||
| 9d4c8d9f76 | |||
| b60b8d8e61 | |||
| 664fa20b0a | |||
| 04aca48a3b | |||
|
|
9b8f3794c8 | ||
| f54acb3ac3 | |||
| eabd15fc74 | |||
| db6deb3205 | |||
|
|
c69e72cd2f | ||
| ae259e84e6 | |||
| f86ac62e04 | |||
| d30148c021 | |||
| 9c53ea922e | |||
|
|
9b017117d8 | ||
| 7292affc04 | |||
| 18ddbb82f1 | |||
| 0541250822 | |||
| 5b4ba442ed | |||
| b10aec8d15 | |||
| 2f48907035 | |||
|
|
0936ea2c58 | ||
| fabe058bfe | |||
| d9ae38197c | |||
| 804bfbd080 | |||
| 1e389aeb44 | |||
| 7dd21ea510 | |||
| 8fe898a281 | |||
| 9db3347970 | |||
|
|
55c446a4e4 | ||
| 27badbee83 | |||
| 1160e375d4 | |||
| 9973a78d4a | |||
| 965bde4bed | |||
| 8c4eae8a96 | |||
| e96ccb6841 | |||
| cce78260c3 | |||
|
|
d43a1d61db | ||
| 20d185adb7 | |||
| d76f371c72 | |||
| 7ccdcad866 | |||
| c2ed2992d9 | |||
| 0f3b22b303 | |||
| 3e461eda7a | |||
| 16d7d4275b | |||
| f397d14fe2 | |||
| c95b9c978a | |||
| 4b1726ac00 | |||
| 983ef3bfb5 | |||
| 9b1e1f6b93 | |||
| 0a239f53c2 | |||
| f44d976330 | |||
|
|
e3b38c20f0 | ||
|
|
8168e1fed2 | ||
| 69e0e4e666 | |||
| 07a42ea123 | |||
| fc005b4944 |
45 changed files with 2549 additions and 4430 deletions
6
.github/workflows/deploy-blog.yml
vendored
6
.github/workflows/deploy-blog.yml
vendored
|
|
@ -46,6 +46,9 @@ jobs:
|
||||||
needs: sync-blog-content
|
needs: sync-blog-content
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://blog.cloverta.top
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||||
|
|
@ -105,6 +108,9 @@ jobs:
|
||||||
manual-deploy:
|
manual-deploy:
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://blog.cloverta.top
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ 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";
|
import rehypeMermaid from "rehype-mermaid";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
|
@ -24,6 +26,10 @@ export default defineConfig({
|
||||||
type: 'shiki',
|
type: 'shiki',
|
||||||
excludeLangs: ['mermaid', 'math'],
|
excludeLangs: ['mermaid', 'math'],
|
||||||
},
|
},
|
||||||
rehypePlugins: [rehypeMermaid],
|
rehypePlugins: [
|
||||||
|
rehypeMermaid,
|
||||||
|
rehypeKatex,
|
||||||
|
],
|
||||||
|
remarkPlugins: [remarkMath],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import Fontmin from "fontmin";
|
|
||||||
|
|
||||||
function getFiles(dir) {
|
|
||||||
const results = [];
|
|
||||||
const list = fs.readdirSync(dir);
|
|
||||||
|
|
||||||
for (const file of list) {
|
|
||||||
const filePath = path.join(dir, file);
|
|
||||||
const stat = fs.statSync(filePath);
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
results.push(...getFiles(filePath));
|
|
||||||
} else {
|
|
||||||
results.push(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanDirectory(dir) {
|
|
||||||
let set = new Set();
|
|
||||||
const files = getFiles(dir);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const ignoredExtensions = [
|
|
||||||
".ttf",
|
|
||||||
".otf",
|
|
||||||
".woff",
|
|
||||||
".woff2",
|
|
||||||
".eot",
|
|
||||||
".png",
|
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".webp",
|
|
||||||
".gif",
|
|
||||||
".ico",
|
|
||||||
".pdf",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (ignoredExtensions.some((ext) => file.endsWith(ext))) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(file, "utf8");
|
|
||||||
const currentSet = new Set(content);
|
|
||||||
set = new Set([...set, ...currentSet]);
|
|
||||||
} catch {
|
|
||||||
// 跳过二进制等不可读文件
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
function subsetFont(src, text) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const fontmin = new Fontmin()
|
|
||||||
.src(src)
|
|
||||||
.use(
|
|
||||||
Fontmin.glyph({
|
|
||||||
text,
|
|
||||||
hinting: false,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.use(Fontmin.ttf2woff2())
|
|
||||||
.dest("public/fonts/subset");
|
|
||||||
|
|
||||||
fontmin.run((err) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const baseChars =
|
|
||||||
"首页文章标签关于作者评论发布于切换主题,。!?:“”‘’()《》【】、—…·-_/\\'\"()[]{}<>:;.!? ";
|
|
||||||
const scanned = Array.from(scanDirectory("src")).join("");
|
|
||||||
const chars = Array.from(new Set((scanned + baseChars).split(""))).join("");
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
subsetFont("public/fonts/MapleMono-CN-Regular.ttf", chars),
|
|
||||||
subsetFont("public/fonts/MapleMono-CN-Bold.ttf", chars),
|
|
||||||
subsetFont("public/fonts/MapleMono-CN-Italic.ttf", chars),
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(`中文子集字体生成完成,共收集 ${chars.length} 个字符`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
4754
package-lock.json
generated
4754
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -6,23 +6,23 @@
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "PUBLIC_GIT_COMMIT=$(git rev-parse HEAD) astro dev",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"subset-font": "node build/fontmin.js",
|
"build": "PUBLIC_GIT_COMMIT=$(git rev-parse HEAD) astro build"
|
||||||
"build": "npm run subset-font && astro build"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/rss": "^4.0.17",
|
"@astrojs/rss": "^4.0.17",
|
||||||
"@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",
|
||||||
|
"katex": "^0.17.0",
|
||||||
"playwright": "^1.59.1",
|
"playwright": "^1.59.1",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-mermaid": "^3.0.0",
|
"rehype-mermaid": "^3.0.0",
|
||||||
"url": "^0.11.4"
|
"remark-math": "^6.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0"
|
||||||
"fontmin": "^1.1.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
public/images/email.svg
Normal file
1
public/images/email.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M957.1 183.2c-0.2-0.4-0.3-0.8-0.5-1.2-0.7-1.7-1.6-3.3-2.5-4.8-0.3-0.5-0.7-1.1-1-1.6-0.5-0.8-1.1-1.6-1.6-2.3-0.2-0.3-0.4-0.6-0.7-0.9-0.4-0.6-0.9-1.1-1.4-1.7-0.5-0.5-1-1.1-1.5-1.6-2.3-2.4-4.9-4.6-7.7-6.4-0.7-0.4-1.4-0.9-2.1-1.3-2.8-1.7-5.9-3-9-4l-0.6-0.2c-0.5-0.2-1.1-0.3-1.7-0.4-3.7-1-7.6-1.5-11.4-1.5H106.7l-0.9 0.1h-0.2C82.2 157 64 176.6 64.1 200.1c0 12.2 5 23.9 13.8 32.3l404.8 404.8c18 17.6 46.7 17.7 64.8 0.2l323.3-323.3v468.6h-717V648.2c0-24.7-20.1-44.8-44.8-44.8-24.7 0-44.8 20.1-44.8 44.8v179.3c0 24.7 20.1 44.8 44.8 44.8h806.6c24.7 0 44.8-20.1 44.8-44.8V200.1c-0.1-5.8-1.2-11.5-3.3-16.9z m-442 359.7l-298-298h596l-298 298z" fill="" /><path d="M108.9 513.8c24.7 0 44.8-20.1 44.8-44.8v-44.8c0-24.7-20.1-44.8-44.8-44.8-24.7 0-44.8 20.1-44.8 44.8V469c0 24.7 20 44.8 44.8 44.8z" fill="" /></svg>
|
||||||
|
After Width: | Height: | Size: 994 B |
1
public/images/github-outline-7.svg
Normal file
1
public/images/github-outline-7.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg class="svg-icon" style="width: 1em;height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M692.906667 938.666667a42.666667 42.666667 0 0 1-42.666667-42.666667v-110.933333a91.733333 91.733333 0 0 0-23.04-70.826667 42.666667 42.666667 0 0 1 26.026667-71.253333C757.333333 630.613333 853.333333 597.333333 853.333333 416.853333a170.666667 170.666667 0 0 0-28.586666-94.72 117.333333 117.333333 0 0 1-17.493334-87.893333 158.293333 158.293333 0 0 0 0-60.16 326.4 326.4 0 0 0-89.173333 46.506667 42.666667 42.666667 0 0 1-35.84 6.4 433.066667 433.066667 0 0 0-235.52 0 42.666667 42.666667 0 0 1-35.84-6.4 315.733333 315.733333 0 0 0-90.026667-46.506667 150.186667 150.186667 0 0 0 0 60.16 121.173333 121.173333 0 0 1-18.346666 88.746667 173.653333 173.653333 0 0 0-28.586667 95.146666c0 165.973333 80.213333 210.346667 200.533333 225.706667a42.666667 42.666667 0 0 1 34.986667 28.16 42.666667 42.666667 0 0 1-8.96 42.666667 87.893333 87.893333 0 0 0-23.466667 66.56V896a42.666667 42.666667 0 0 1-85.333333 0v-24.32a256 256 0 0 1-224.853333-89.173333 166.4 166.4 0 0 0-49.493334-37.546667 42.666667 42.666667 0 1 1 21.333334-82.773333 210.346667 210.346667 0 0 1 85.333333 58.026666c42.666667 42.666667 85.333333 80.213333 166.4 64.853334a165.973333 165.973333 0 0 1 9.813333-67.413334c-87.893333-22.186667-213.333333-85.333333-213.333333-298.666666a256 256 0 0 1 42.666667-142.08 36.266667 36.266667 0 0 0 5.546666-26.453334 242.773333 242.773333 0 0 1 14.08-136.96 42.666667 42.666667 0 0 1 26.88-24.32c14.506667-4.266667 66.56-12.8 165.12 51.2a518.826667 518.826667 0 0 1 242.773334 0c98.56-64 150.613333-55.893333 164.693333-51.2a42.666667 42.666667 0 0 1 26.88 24.32 243.626667 243.626667 0 0 1 14.08 137.386667 32 32 0 0 0 4.693333 24.32 256 256 0 0 1 42.666667 142.506667c0 216.32-124.586667 279.04-213.333333 298.666666a182.613333 182.613333 0 0 1 9.386666 71.253334V896a42.666667 42.666667 0 0 1-40.106666 42.666667z" /></svg>
|
||||||
|
After Width: | Height: | Size: 2 KiB |
47
src/components/BuildHashBlocks.astro
Normal file
47
src/components/BuildHashBlocks.astro
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
const rawHash = (import.meta.env.PUBLIC_GIT_COMMIT || "")
|
||||||
|
.replace(/[^a-fA-F0-9]/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
const safeHash = rawHash || "0000000000000000000000000000000000000000";
|
||||||
|
|
||||||
|
const blockCount = 6;
|
||||||
|
const hexPerBlock = 6;
|
||||||
|
|
||||||
|
const blocks: string[] = Array.from({ length: blockCount }, (_, i) => {
|
||||||
|
const start = i * hexPerBlock;
|
||||||
|
const part = safeHash.slice(start, start + hexPerBlock).padEnd(6, "0");
|
||||||
|
return `#${part}`;
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="build-hash"
|
||||||
|
title={`Build ${safeHash}`}
|
||||||
|
aria-label={`Build ${safeHash}`}
|
||||||
|
>
|
||||||
|
<div class="build-hint">$build.onCommit.hash =</div>
|
||||||
|
{
|
||||||
|
blocks.map((color: string) => (
|
||||||
|
<span class="build-block" style={`background:${color}`} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.build-hint {
|
||||||
|
margin-right: 5px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.build-hash {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
/* vertical-align: middle; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-block {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,22 +1,85 @@
|
||||||
---
|
---
|
||||||
import { getLangFromUrl, getTranslations, type Lang } from "@/i18n";
|
import { getLangFromUrl, getTranslations, type Lang } from "@/i18n";
|
||||||
|
import BuildHashBlocks from "@/components/BuildHashBlocks.astro";
|
||||||
|
const commit = import.meta.env.PUBLIC_GIT_COMMIT || "unknown";
|
||||||
const lang = getLangFromUrl(Astro.url);
|
const lang = getLangFromUrl(Astro.url);
|
||||||
const t = getTranslations(lang);
|
const t = getTranslations(lang);
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p set:html={t.footer.githubIntro} />
|
<a
|
||||||
<p set:html={t.footer.repoIntro} />
|
href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog"
|
||||||
|
class="github"
|
||||||
|
aria-label="GitHub repository"></a>
|
||||||
|
<a href="mailto:cloverta@petalmail.com" class="email" aria-label="Email"></a>
|
||||||
|
<p class="footer-content-pc">
|
||||||
|
© 2026 ClovertaTheTrilobita · <a
|
||||||
|
href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog"
|
||||||
|
>Source code</a
|
||||||
|
> licensed under MIT. Content rights reserved.
|
||||||
|
<!-- <code class="build-commit">{commit.slice(0, 7)}</code> -->
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="footer-content-mobile">
|
||||||
|
<p>© 2026 ClovertaTheTrilobita</p>
|
||||||
|
<p>Content rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BuildHashBlocks />
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.github {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: #1f2328;
|
||||||
|
-webkit-mask: url("/images/github-outline-7.svg") no-repeat center / contain;
|
||||||
|
mask: url("/images/github-outline-7.svg") no-repeat center / contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
transition:
|
||||||
|
transform 0.18s ease,
|
||||||
|
opacity 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: #1f2328;
|
||||||
|
-webkit-mask: url("/images/email.svg") no-repeat center / contain;
|
||||||
|
mask: url("/images/email.svg") no-repeat center / contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transition:
|
||||||
|
transform 0.18s ease,
|
||||||
|
opacity 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.email:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.dark .github) {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.dark .email) {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 1.5rem 0 0;
|
padding: 1.5rem 0 0;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer::before {
|
.footer::before {
|
||||||
|
|
@ -27,22 +90,22 @@ const t = getTranslations(lang);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
-45deg,
|
-45deg,
|
||||||
#e96b6b 0 14px,
|
var(--deep-red) 0 14px,
|
||||||
transparent 14px 28px,
|
transparent 14px 28px,
|
||||||
#7da2ff 28px 42px,
|
var(--deep-blue) 28px 42px,
|
||||||
transparent 42px 56px
|
transparent 42px 56px
|
||||||
);
|
);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer p {
|
.footer-content-mobile {
|
||||||
margin: 0.4rem 0;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer a {
|
.footer p {
|
||||||
color: #7fb3ff;
|
margin: 0.4rem 0;
|
||||||
font-weight: 700;
|
font-size: 0.8rem;
|
||||||
text-decoration: underline;
|
/* display: inline-flex; */
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .footer::before {
|
:global(.dark) .footer::before {
|
||||||
|
|
@ -54,4 +117,14 @@ const t = getTranslations(lang);
|
||||||
transparent 42px 56px
|
transparent 42px 56px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.footer-content-pc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content-mobile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ const links = await getCollection("friends");
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.friendly-link-list {
|
.friendly-link-list {
|
||||||
|
max-width: 770px;
|
||||||
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
|
||||||
108
src/components/FriendlyLinks/FriendsDescription.astro
Normal file
108
src/components/FriendlyLinks/FriendsDescription.astro
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
---
|
||||||
|
import { getLangFromUrl, getTranslations } from "@/i18n";
|
||||||
|
import "@/styles/global.css";
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const t = getTranslations(lang);
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="description">
|
||||||
|
<div class="image-wrap">
|
||||||
|
<img
|
||||||
|
src="https://files.seeusercontent.com/2026/04/17/nOg4/7tx5GS3nAtOXM577XE9VOe0LUU8.webp"
|
||||||
|
alt="nichijo.webp"
|
||||||
|
/>
|
||||||
|
<p class="caption">{t.friends.imgDesc}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>{t.friends.title}</h1>
|
||||||
|
<div class="content">
|
||||||
|
{t.friends.content.map((line: string) => <p set:html={line} />)}
|
||||||
|
</div>
|
||||||
|
<div class="friend-code-block">
|
||||||
|
<pre><code>---
|
||||||
|
name: 'CLoverta的博客'
|
||||||
|
description: "欢迎光临,请进门左转"
|
||||||
|
url: "https://blog.cloverta.top"
|
||||||
|
avatar: 'https://s2.loli.net/2025/11/22/tiDKuzdqycx1v9B.png'
|
||||||
|
---</code></pre>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="player">
|
||||||
|
<iframe
|
||||||
|
data-testid="embed-iframe"
|
||||||
|
style="border-radius:2px"
|
||||||
|
src="https://open.spotify.com/embed/track/2vuSiplO7RnGkoyw2gH96M?utm_source=generator"
|
||||||
|
width="450px"
|
||||||
|
height="80"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen=""
|
||||||
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
|
loading="lazy"></iframe>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.player {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
line-height: 1.8;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-code-block {
|
||||||
|
background: #fbf5f2;
|
||||||
|
border: 1px solid #8fa1ad;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-code-block pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-code-block code {
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrap {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrap img {
|
||||||
|
width: calc((3.8em * 4) * 16 / 5);
|
||||||
|
height: calc(3.4em * 4);
|
||||||
|
aspect-ratio: 16 / 5;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 2px dashed gray;
|
||||||
|
filter: sepia(0.3) saturate(1.08) hue-rotate(-12deg) brightness(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrap .caption {
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
text-align: right;
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.description img {
|
||||||
|
height: calc(2.6em * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -26,6 +26,20 @@ const t = getTranslations(lang);
|
||||||
</details>
|
</details>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const nav = document.querySelector(".site-nav-mobile");
|
||||||
|
|
||||||
|
if (!nav) return;
|
||||||
|
|
||||||
|
const target = event.target as Node;
|
||||||
|
|
||||||
|
if (!nav.contains(target)) {
|
||||||
|
nav.removeAttribute("open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.site-nav {
|
.site-nav {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
|
@ -42,9 +56,9 @@ const t = getTranslations(lang);
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
-45deg,
|
-45deg,
|
||||||
#e96b6b 0 14px,
|
var(--deep-red) 0 14px,
|
||||||
transparent 14px 28px,
|
transparent 14px 28px,
|
||||||
#7da2ff 28px 42px,
|
var(--deep-blue) 28px 42px,
|
||||||
transparent 42px 56px
|
transparent 42px 56px
|
||||||
);
|
);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
||||||
118
src/components/Posts/Dialog.astro
Normal file
118
src/components/Posts/Dialog.astro
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
<dialog id="image-preview-dialog" class="image-preview-dialog">
|
||||||
|
<button class="image-preview-close" aria-label="关闭图片预览">×</button>
|
||||||
|
|
||||||
|
<img id="image-preview-img" alt="" />
|
||||||
|
|
||||||
|
<a
|
||||||
|
id="image-preview-download"
|
||||||
|
class="image-preview-download"
|
||||||
|
href="#"
|
||||||
|
aria-label="下载图片"
|
||||||
|
title="下载图片"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="26"
|
||||||
|
height="26"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 3v12m0 0 5-5m-5 5-5-5M5 21h14"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</dialog>
|
||||||
|
<script is:inline>
|
||||||
|
function initImagePreview() {
|
||||||
|
const dialog = document.getElementById("image-preview-dialog");
|
||||||
|
const previewImg = document.getElementById("image-preview-img");
|
||||||
|
const closeBtn = dialog?.querySelector(".image-preview-close");
|
||||||
|
const downloadBtn = document.getElementById("image-preview-download");
|
||||||
|
|
||||||
|
if (!dialog || !previewImg || !closeBtn || !downloadBtn) return;
|
||||||
|
|
||||||
|
let currentImageUrl = "";
|
||||||
|
|
||||||
|
function getFileNameFromUrl(url) {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(url, window.location.href).pathname;
|
||||||
|
return pathname.split("/").pop() || "image";
|
||||||
|
} catch {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage(url) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "omit",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch image");
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = blobUrl;
|
||||||
|
a.download = getFileNameFromUrl(url);
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("article img").forEach((img) => {
|
||||||
|
if (img.dataset.previewBound === "true") return;
|
||||||
|
img.dataset.previewBound = "true";
|
||||||
|
|
||||||
|
img.addEventListener("click", () => {
|
||||||
|
const imageUrl = img.currentSrc || img.src;
|
||||||
|
|
||||||
|
currentImageUrl = imageUrl;
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
previewImg.alt = img.alt || "";
|
||||||
|
|
||||||
|
dialog.showModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadBtn.addEventListener("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!currentImageUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadImage(currentImageUrl);
|
||||||
|
} catch (error) {
|
||||||
|
// 如果 fetch 因为跨域失败,就退回到直接打开图片
|
||||||
|
window.open(currentImageUrl, "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function closePreview() {
|
||||||
|
dialog.close();
|
||||||
|
previewImg.removeAttribute("src");
|
||||||
|
currentImageUrl = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBtn.addEventListener("click", closePreview);
|
||||||
|
|
||||||
|
dialog.addEventListener("click", (event) => {
|
||||||
|
if (event.target === dialog) {
|
||||||
|
closePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", initImagePreview);
|
||||||
|
document.addEventListener("astro:page-load", initImagePreview);
|
||||||
|
</script>
|
||||||
|
|
@ -12,14 +12,14 @@ const tags = data.tags;
|
||||||
<div class="post-link">
|
<div class="post-link">
|
||||||
<div class="post-text">
|
<div class="post-text">
|
||||||
<a href={data.url} class="post-title-link">
|
<a href={data.url} class="post-title-link">
|
||||||
<h2 class="post-title" transition:name={`post-title-${data.slug}`}>
|
<h2 class="post-title" transition:name={`post-title-${data.postId}`}>
|
||||||
{data.title}
|
{data.title}
|
||||||
</h2>
|
</h2>
|
||||||
</a>
|
</a>
|
||||||
<a href={data.url} class="post-description">
|
<a href={data.url} class="post-description">
|
||||||
{data.description}
|
{data.description}
|
||||||
</a>
|
</a>
|
||||||
<div class="tags">
|
<div class="tags" transition:name={`post-tags-${data.postId}`}>
|
||||||
{
|
{
|
||||||
tags.map((tag: string) => (
|
tags.map((tag: string) => (
|
||||||
<p class="tag">
|
<p class="tag">
|
||||||
|
|
@ -40,7 +40,13 @@ const tags = data.tags;
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href={data.url} class="post-image-link">
|
<a href={data.url} class="post-image-link">
|
||||||
<img src={data.img} alt={data.title} class="post-image" loading="lazy" />
|
<img
|
||||||
|
src={data.img}
|
||||||
|
alt={data.title}
|
||||||
|
class="post-image"
|
||||||
|
loading="lazy"
|
||||||
|
transition:name={`post-image-${data.postId}`}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -110,8 +116,8 @@ const tags = data.tags;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 1rem 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-bottom: 0.2rem;
|
padding-bottom: 0.9rem;
|
||||||
padding-top: 0.2rem;
|
padding-top: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-link {
|
.post-link {
|
||||||
|
|
@ -167,7 +173,7 @@ const tags = data.tags;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
font-style: italic;
|
/* font-style: italic; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-meta-row {
|
.post-meta-row {
|
||||||
|
|
@ -206,7 +212,7 @@ const tags = data.tags;
|
||||||
aspect-ratio: 16 / 10;
|
aspect-ratio: 16 / 10;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
border: 2px #94a0ab dashed;
|
border: 1.5px #94a0ab dashed;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
@ -256,7 +262,7 @@ const tags = data.tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-image {
|
.post-image {
|
||||||
border: none;
|
/* border: none; */
|
||||||
grid-area: image;
|
grid-area: image;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(145px * 10 / 16);
|
height: calc(145px * 10 / 16);
|
||||||
|
|
@ -270,5 +276,10 @@ const tags = data.tags;
|
||||||
width: 145px;
|
width: 145px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-card {
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ const latestPosts = filteredPosts.slice(0, 7);
|
||||||
img={post.data.image.url}
|
img={post.data.image.url}
|
||||||
tags={post.data.tags}
|
tags={post.data.tags}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
|
postId={post.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
403
src/components/Posts/PostMenu.astro
Normal file
403
src/components/Posts/PostMenu.astro
Normal file
|
|
@ -0,0 +1,403 @@
|
||||||
|
---
|
||||||
|
interface Heading {
|
||||||
|
depth: number;
|
||||||
|
slug: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
headings: Heading[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { headings = [] } = Astro.props;
|
||||||
|
const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
tocHeadings.length > 0 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
class="post-menu-mobile-toggle"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="post-menu-panel"
|
||||||
|
aria-label="目录"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="post-menu-mobile-preview"
|
||||||
|
data-post-menu-preview
|
||||||
|
data-default-text={tocHeadings[0]?.text ?? "本文目录"}
|
||||||
|
>
|
||||||
|
{tocHeadings[0]?.text ?? "本文目录"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="post-menu-mobile-icon" aria-hidden="true">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 6.5H19M5 12H19M5 17.5H19"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<circle cx="3.5" cy="6.5" r="1" fill="currentColor" />
|
||||||
|
<circle cx="3.5" cy="12" r="1" fill="currentColor" />
|
||||||
|
<circle cx="3.5" cy="17.5" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<aside class="post-menu" id="post-menu-panel">
|
||||||
|
<nav aria-label="Table of contents">
|
||||||
|
<p class="post-menu-title">目录</p>
|
||||||
|
<ul class="post-menu-list">
|
||||||
|
{tocHeadings.map((heading) => (
|
||||||
|
<li class={`post-menu-item depth-${heading.depth}`}>
|
||||||
|
<a
|
||||||
|
href={`#${heading.slug}`}
|
||||||
|
data-heading-link
|
||||||
|
data-heading-text={heading.text}
|
||||||
|
>
|
||||||
|
{heading.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
:where(h1, h2, h3, h4, h5, h6) {
|
||||||
|
scroll-margin-top: 4rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.post-menu {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-title {
|
||||||
|
margin: 0 0 0.7rem;
|
||||||
|
padding-top: 0.65rem;
|
||||||
|
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(65, 65, 65, 0.8) 0,
|
||||||
|
rgba(65, 65, 65, 0.8) 8px,
|
||||||
|
transparent 8px,
|
||||||
|
transparent 14px
|
||||||
|
);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
background-size: 14px 2px;
|
||||||
|
background-position: top left;
|
||||||
|
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-list {
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 0.9rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-list::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0.15rem;
|
||||||
|
top: 0.25rem;
|
||||||
|
bottom: 0.25rem;
|
||||||
|
width: 1px;
|
||||||
|
background: rgba(128, 128, 128, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-item {
|
||||||
|
position: relative;
|
||||||
|
margin: 0.42rem 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-item.depth-3 {
|
||||||
|
padding-left: 0.85rem;
|
||||||
|
opacity: 0.82;
|
||||||
|
font-size: 0.94em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-mobile-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.post-menu {
|
||||||
|
--content-width: 90ch;
|
||||||
|
--menu-width: clamp(
|
||||||
|
140px,
|
||||||
|
calc((100vw - 1200px) * 0.42 + 140px),
|
||||||
|
260px
|
||||||
|
);
|
||||||
|
--menu-gap: 0rem;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: 8.5rem;
|
||||||
|
width: var(--menu-width);
|
||||||
|
|
||||||
|
left: calc(
|
||||||
|
50vw - var(--content-width) / 2 - var(--menu-gap) -
|
||||||
|
var(--menu-width)
|
||||||
|
);
|
||||||
|
|
||||||
|
max-height: calc(100vh - 10rem);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.2rem 0.2rem 0.2rem 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.post-menu-mobile-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
position: fixed;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.8rem;
|
||||||
|
z-index: 30;
|
||||||
|
max-width: min(78vw, 22rem);
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border: 1.5px solid var(--deep-blue);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(calc(-100% - 0.6rem));
|
||||||
|
pointer-events: none;
|
||||||
|
transition:
|
||||||
|
transform 0.28s ease,
|
||||||
|
opacity 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-mobile-toggle.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(calc(env(safe-area-inset-top) + 0.4rem));
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-mobile-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
opacity: 0.78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-mobile-preview {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10rem;
|
||||||
|
/* height: 1rem; */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: calc(env(safe-area-inset-top) + 4.2rem);
|
||||||
|
right: 0.8rem;
|
||||||
|
z-index: 29;
|
||||||
|
width: min(88vw, 24rem);
|
||||||
|
max-height: min(65vh, 32rem);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
border: 1.5px solid var(--deep-blue);
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu-title {
|
||||||
|
padding-top: 0;
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-menu.is-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.dark) .post-menu-mobile-toggle,
|
||||||
|
:global(html.dark) .post-menu {
|
||||||
|
background: var(--background-color-dark);
|
||||||
|
color: var(--text-color-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(() => {
|
||||||
|
const setupPostMenu = () => {
|
||||||
|
const btn = document.querySelector(".post-menu-mobile-toggle");
|
||||||
|
const panel = document.querySelector("#post-menu-panel");
|
||||||
|
const preview = document.querySelector("[data-post-menu-preview]");
|
||||||
|
const headingLinks = Array.from(
|
||||||
|
document.querySelectorAll("[data-heading-link]"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!btn || !panel || !preview || headingLinks.length === 0) return;
|
||||||
|
if (btn.dataset.bound === "true") return;
|
||||||
|
btn.dataset.bound = "true";
|
||||||
|
|
||||||
|
const defaultText =
|
||||||
|
preview.getAttribute("data-default-text") || "本文目录";
|
||||||
|
|
||||||
|
const getHeadingElements = () =>
|
||||||
|
headingLinks
|
||||||
|
.map((link) => {
|
||||||
|
const href = link.getAttribute("href");
|
||||||
|
if (!href || !href.startsWith("#")) return null;
|
||||||
|
|
||||||
|
const el = document.getElementById(href.slice(1));
|
||||||
|
if (!el) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
link,
|
||||||
|
el,
|
||||||
|
text:
|
||||||
|
link.getAttribute("data-heading-text") ||
|
||||||
|
el.textContent ||
|
||||||
|
defaultText,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
let headingItems = getHeadingElements();
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
panel.classList.remove("is-open");
|
||||||
|
btn.setAttribute("aria-expanded", "false");
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (!(target instanceof Node)) return;
|
||||||
|
|
||||||
|
const clickedInsidePanel = panel.contains(target);
|
||||||
|
const clickedButton = btn.contains(target);
|
||||||
|
|
||||||
|
if (!clickedInsidePanel && !clickedButton) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateVisibility = () => {
|
||||||
|
if (window.innerWidth >= 1200) {
|
||||||
|
btn.classList.remove("is-visible");
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShow = window.scrollY > 80;
|
||||||
|
btn.classList.toggle("is-visible", shouldShow);
|
||||||
|
|
||||||
|
if (!shouldShow) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCurrentHeading = () => {
|
||||||
|
if (window.innerWidth >= 1200) return;
|
||||||
|
|
||||||
|
headingItems = getHeadingElements();
|
||||||
|
|
||||||
|
let current = null;
|
||||||
|
const triggerLine = 140;
|
||||||
|
|
||||||
|
for (const item of headingItems) {
|
||||||
|
const rect = item.el.getBoundingClientRect();
|
||||||
|
if (rect.top <= triggerLine) {
|
||||||
|
current = item;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preview.textContent = current ? current.text : defaultText;
|
||||||
|
|
||||||
|
headingLinks.forEach((link) =>
|
||||||
|
link.classList.remove("is-current"),
|
||||||
|
);
|
||||||
|
if (current) current.link.classList.add("is-current");
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
const expanded = btn.getAttribute("aria-expanded") === "true";
|
||||||
|
btn.setAttribute("aria-expanded", String(!expanded));
|
||||||
|
panel.classList.toggle("is-open", !expanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.addEventListener("click", toggleMenu);
|
||||||
|
|
||||||
|
panel.addEventListener("click", (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (!(target instanceof Element)) return;
|
||||||
|
|
||||||
|
const link = target.closest("a[data-heading-link]");
|
||||||
|
if (!link) return;
|
||||||
|
|
||||||
|
const href = link.getAttribute("href");
|
||||||
|
if (!href || !href.startsWith("#")) return;
|
||||||
|
|
||||||
|
const el = document.getElementById(href.slice(1));
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
el.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start",
|
||||||
|
});
|
||||||
|
|
||||||
|
history.replaceState(null, "", href);
|
||||||
|
closeMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
updateVisibility();
|
||||||
|
updateCurrentHeading();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
window.addEventListener("resize", onScroll);
|
||||||
|
|
||||||
|
updateVisibility();
|
||||||
|
updateCurrentHeading();
|
||||||
|
};
|
||||||
|
|
||||||
|
setupPostMenu();
|
||||||
|
document.addEventListener("astro:page-load", setupPostMenu);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
130
src/components/Posts/PostNav.astro
Normal file
130
src/components/Posts/PostNav.astro
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import type { Lang } from "@/i18n";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
post: CollectionEntry<"blog">;
|
||||||
|
lang: Lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post, lang } = Astro.props;
|
||||||
|
|
||||||
|
const allPosts = await getCollection("blog");
|
||||||
|
|
||||||
|
// 只保留当前语言
|
||||||
|
const sameLangPosts = allPosts.filter((p) => {
|
||||||
|
const [postLang] = p.id.split("/");
|
||||||
|
return postLang === lang;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按日期倒序:越新越靠前
|
||||||
|
const sortedPosts = [...sameLangPosts].sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 找当前文章位置
|
||||||
|
const currentIndex = sortedPosts.findIndex((p) => p.id === post.id);
|
||||||
|
|
||||||
|
// 更旧的一篇
|
||||||
|
const prevPost =
|
||||||
|
currentIndex < sortedPosts.length - 1
|
||||||
|
? sortedPosts[currentIndex + 1]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 更新的一篇
|
||||||
|
const nextPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null;
|
||||||
|
|
||||||
|
const getSlugFromId = (id: string) => id.split("/").slice(1).join("/");
|
||||||
|
|
||||||
|
const prevSlug = prevPost ? getSlugFromId(prevPost.id) : null;
|
||||||
|
const nextSlug = nextPost ? getSlugFromId(nextPost.id) : null;
|
||||||
|
---
|
||||||
|
|
||||||
|
<nav class="post-nav" aria-label="Post navigation">
|
||||||
|
<div class="post-nav-item post-nav-prev">
|
||||||
|
{
|
||||||
|
nextPost && nextSlug && (
|
||||||
|
<a href={`/${lang}/posts/${nextSlug}/`}>
|
||||||
|
<span class="post-nav-label">
|
||||||
|
{lang === "zh" ? "上一篇" : "Previous"}
|
||||||
|
</span>
|
||||||
|
<span class="post-nav-title">{nextPost.data.title}</span>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-nav-item post-nav-next">
|
||||||
|
{
|
||||||
|
prevPost && prevSlug && (
|
||||||
|
<a href={`/${lang}/posts/${prevSlug}/`}>
|
||||||
|
<span class="post-nav-label">
|
||||||
|
{lang === "zh" ? "下一篇" : "Next"}
|
||||||
|
</span>
|
||||||
|
<span class="post-nav-title">{prevPost.data.title}</span>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.post-nav {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-top: 1px dashed rgba(128, 128, 128, 0.5);
|
||||||
|
border-bottom: 1px dashed rgba(128, 128, 128, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav-item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav-next {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.post-nav-title {
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav a:hover .post-nav-title {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav a {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
/* color: inherit; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.65;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.post-nav {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-nav-next {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -54,7 +54,7 @@ const groupedPosts = filteredPosts.reduce((acc: any[], post: any) => {
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
class="post-title"
|
class="post-title"
|
||||||
transition:name={`post-title-${slug}`}
|
transition:name={`post-title-${post.id}`}
|
||||||
>
|
>
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -147,7 +147,8 @@ const groupedPosts = filteredPosts.reduce((acc: any[], post: any) => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-style: italic;
|
font-weight: 400;
|
||||||
|
/* font-style: italic; */
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="remark42"></div>
|
<div id="remark42"></div>
|
||||||
|
|
||||||
<script define:vars={{ pagePath, host, siteId }} is:inline data-astro-rerun>
|
<script define:vars={{ pagePath, host, siteId }} is:inline data-astro-rerun>
|
||||||
function getTheme() {
|
function getTheme() {
|
||||||
return document.documentElement.classList.contains("dark")
|
return document.documentElement.classList.contains("dark")
|
||||||
|
|
@ -35,6 +34,7 @@ const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||||
resolve(true);
|
resolve(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
existing.addEventListener("load", () => resolve(true), {
|
existing.addEventListener("load", () => resolve(true), {
|
||||||
once: true,
|
once: true,
|
||||||
});
|
});
|
||||||
|
|
@ -67,16 +67,42 @@ const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||||
setRemarkConfig();
|
setRemarkConfig();
|
||||||
await ensureScript();
|
await ensureScript();
|
||||||
|
|
||||||
if (window.REMARK42) {
|
if (!window.REMARK42) return;
|
||||||
if (typeof window.REMARK42.destroy === "function") {
|
|
||||||
window.REMARK42.destroy();
|
if (typeof window.REMARK42.destroy === "function") {
|
||||||
}
|
window.REMARK42.destroy();
|
||||||
if (typeof window.REMARK42.createInstance === "function") {
|
}
|
||||||
node.innerHTML = "";
|
|
||||||
window.REMARK42.createInstance(window.remark_config);
|
if (typeof window.REMARK42.createInstance === "function") {
|
||||||
}
|
node.innerHTML = "";
|
||||||
|
window.REMARK42.createInstance(window.remark_config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupRemark42ThemeObserver() {
|
||||||
|
if (window.__remark42ThemeObserver) {
|
||||||
|
window.__remark42ThemeObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastTheme = getTheme();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const currentTheme = getTheme();
|
||||||
|
|
||||||
|
if (currentTheme === lastTheme) return;
|
||||||
|
|
||||||
|
lastTheme = currentTheme;
|
||||||
|
mountRemark42();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
|
||||||
|
window.__remark42ThemeObserver = observer;
|
||||||
|
}
|
||||||
|
|
||||||
mountRemark42();
|
mountRemark42();
|
||||||
|
setupRemark42ThemeObserver();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -70,15 +70,15 @@ const switchHref = "/" + segments.join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-switch {
|
.lang-switch {
|
||||||
color: black; !important
|
color: black;
|
||||||
font-weight: 700; !important
|
font-weight: 700;
|
||||||
font-size: 0.95rem; !important
|
font-size: 0.95rem;
|
||||||
text-decoration: none; !important
|
text-decoration: none;
|
||||||
line-height: 1; !important
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-switch:hover {
|
.lang-switch:hover {
|
||||||
text-decoration: underline; !important
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sun {
|
.sun {
|
||||||
|
|
@ -104,37 +104,37 @@ const switchHref = "/" + segments.join("/");
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
function applyTheme() {
|
function applyTheme() {
|
||||||
const localStorageTheme = localStorage?.getItem("theme") ?? "";
|
const localStorageTheme = localStorage?.getItem("theme") ?? "";
|
||||||
let theme = "light";
|
let theme = "light";
|
||||||
|
|
||||||
if (["dark", "light"].includes(localStorageTheme)) {
|
if (["dark", "light"].includes(localStorageTheme)) {
|
||||||
theme = localStorageTheme;
|
theme = localStorageTheme;
|
||||||
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
theme = "dark";
|
theme = "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||||
|
window.localStorage.setItem("theme", theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
function bindThemeToggle() {
|
||||||
window.localStorage.setItem("theme", theme);
|
const button = document.getElementById("themeToggle");
|
||||||
}
|
if (!button) return;
|
||||||
|
|
||||||
function bindThemeToggle() {
|
button.onclick = () => {
|
||||||
const button = document.getElementById("themeToggle");
|
const element = document.documentElement;
|
||||||
if (!button) return;
|
element.classList.toggle("dark");
|
||||||
|
|
||||||
button.onclick = () => {
|
const isDark = element.classList.contains("dark");
|
||||||
const element = document.documentElement;
|
localStorage.setItem("theme", isDark ? "dark" : "light");
|
||||||
element.classList.toggle("dark");
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const isDark = element.classList.contains("dark");
|
|
||||||
localStorage.setItem("theme", isDark ? "dark" : "light");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTheme();
|
|
||||||
bindThemeToggle();
|
|
||||||
|
|
||||||
document.addEventListener("astro:after-swap", () => {
|
|
||||||
applyTheme();
|
applyTheme();
|
||||||
bindThemeToggle();
|
bindThemeToggle();
|
||||||
});
|
|
||||||
|
document.addEventListener("astro:after-swap", () => {
|
||||||
|
applyTheme();
|
||||||
|
bindThemeToggle();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ export default {
|
||||||
writtenBy: "Written by",
|
writtenBy: "Written by",
|
||||||
comments: "Comments",
|
comments: "Comments",
|
||||||
publishedOn: "Published on",
|
publishedOn: "Published on",
|
||||||
|
commentsDesc: [
|
||||||
|
"Feel free to leave your thoughts here. 💭💡",
|
||||||
|
"After signing in, you can click the “Subscribe by Email” button at the bottom right of the text box to receive notifications of new interactions via email."
|
||||||
|
]
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
toggle: "Toggle theme",
|
toggle: "Toggle theme",
|
||||||
|
|
@ -25,7 +29,8 @@ export default {
|
||||||
content: [
|
content: [
|
||||||
"These few lines speak what the heart would say; Ink and paper end, but thoughts still stay",
|
"These few lines speak what the heart would say; Ink and paper end, but thoughts still stay",
|
||||||
"Welcome to Cloverta's blog.",
|
"Welcome to Cloverta's blog.",
|
||||||
]
|
],
|
||||||
|
latestTitle: "Latest Posts"
|
||||||
},
|
},
|
||||||
about: {
|
about: {
|
||||||
title: "About Me, and This Blog",
|
title: "About Me, and This Blog",
|
||||||
|
|
@ -41,9 +46,10 @@ export default {
|
||||||
"The design of this blog was inspired by:",
|
"The design of this blog was inspired by:",
|
||||||
'· <a href="https://ex-tasty.com/">極限風味</a>',
|
'· <a href="https://ex-tasty.com/">極限風味</a>',
|
||||||
'· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>',
|
'· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>',
|
||||||
|
'· <a href="https://www.kokosa.icu/">Kokosa\'s Notebook</a>',
|
||||||
"· And some other websites whose names I have unfortunately forgotten",
|
"· And some other websites whose names I have unfortunately forgotten",
|
||||||
"Thank you for your ideas and passion!",
|
"Thank you for your ideas and passion!",
|
||||||
"In addition, this blog is fully open source. You can find its source code through the link in the footer.",
|
"In addition, this blog is open source under MIT License. You can find its source code through the link in the footer.",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
|
|
@ -53,5 +59,15 @@ export default {
|
||||||
footer: {
|
footer: {
|
||||||
githubIntro: 'See more on <a href="https://github.com/ClovertaTheTrilobita">Github</a>!',
|
githubIntro: 'See more on <a href="https://github.com/ClovertaTheTrilobita">Github</a>!',
|
||||||
repoIntro: 'This blog is fully open source at <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>'
|
repoIntro: 'This blog is fully open source at <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>'
|
||||||
|
},
|
||||||
|
friends: {
|
||||||
|
title: "Friends",
|
||||||
|
content: [
|
||||||
|
"Thank you for making it this far.",
|
||||||
|
"These are my friends, and you're very welcome to exchange links with me too!",
|
||||||
|
'You can send your link information to <a href="mailto:cloverta@petalmail.com">my email</a>.',
|
||||||
|
"The format is as follows:"
|
||||||
|
],
|
||||||
|
imgDesc: "Our everyday lives may, in fact, be a series of miracles."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -15,6 +15,10 @@ export default {
|
||||||
writtenBy: "作者",
|
writtenBy: "作者",
|
||||||
comments: "评论",
|
comments: "评论",
|
||||||
publishedOn: "发布于",
|
publishedOn: "发布于",
|
||||||
|
commentsDesc: [
|
||||||
|
"欢迎在这里留下你的想法。💭💡",
|
||||||
|
"你可以在登录评论区后,点击文本框右下角的「Subscribe by Email」以通过邮件接收最新的互动通知。"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
toggle: "切换主题",
|
toggle: "切换主题",
|
||||||
|
|
@ -25,7 +29,8 @@ export default {
|
||||||
content: [
|
content: [
|
||||||
"见字如晤,展信舒颜。楮墨有限,不尽欲言。",
|
"见字如晤,展信舒颜。楮墨有限,不尽欲言。",
|
||||||
"欢迎来到三叶的博客。",
|
"欢迎来到三叶的博客。",
|
||||||
]
|
],
|
||||||
|
latestTitle: "最新文章"
|
||||||
},
|
},
|
||||||
about: {
|
about: {
|
||||||
title: "关于我,和这个博客",
|
title: "关于我,和这个博客",
|
||||||
|
|
@ -41,9 +46,10 @@ export default {
|
||||||
"这个博客在设计理念上参考了:",
|
"这个博客在设计理念上参考了:",
|
||||||
'· <a href="https://ex-tasty.com/">極限風味</a>',
|
'· <a href="https://ex-tasty.com/">極限風味</a>',
|
||||||
'· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>',
|
'· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>',
|
||||||
|
'· <a href="https://www.kokosa.icu/">Kokosa\'s Notebook</a>',
|
||||||
"· 还有一些已经忘记名字的网站",
|
"· 还有一些已经忘记名字的网站",
|
||||||
"谢谢你们的想法和热情!",
|
"谢谢你们的想法和热情!",
|
||||||
"此外,这个博客完全开源,你可以从页脚的链接处获取它的源代码。",
|
"此外,这个博客的源码使用 MIT 协议开源,你可以从页脚的链接处获取它的源代码。",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
|
|
@ -53,5 +59,15 @@ export default {
|
||||||
footer: {
|
footer: {
|
||||||
githubIntro: '在 <a href="https://github.com/ClovertaTheTrilobita">Github</a> 查看更多!',
|
githubIntro: '在 <a href="https://github.com/ClovertaTheTrilobita">Github</a> 查看更多!',
|
||||||
repoIntro: '这个博客完全开源于 <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>'
|
repoIntro: '这个博客完全开源于 <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>'
|
||||||
|
},
|
||||||
|
friends: {
|
||||||
|
title: "友链",
|
||||||
|
content: [
|
||||||
|
"感谢你能看到这里。",
|
||||||
|
"这里是我的朋友们,也非常欢迎你来一起交换友链!",
|
||||||
|
'你可以把友链信息发送到<a href="mailto:cloverta@petalmail.com">我的邮箱</a>。',
|
||||||
|
"友链的格式如下:"
|
||||||
|
],
|
||||||
|
imgDesc: "——我们日复一日度过的日常,也许就是接连发生的奇迹"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
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 { fade } from "astro:transitions";
|
||||||
|
import { getLangFromUrl } from "@/i18n";
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
const htmlLang = lang === "zh" ? "zh-CN" : "en-US";
|
||||||
import SEO from "@/components/SEO.astro";
|
import SEO from "@/components/SEO.astro";
|
||||||
import FloatingActions from "@/components/FloatingActions.astro";
|
import FloatingActions from "@/components/FloatingActions.astro";
|
||||||
import Spinner from "@/components/Spinner.astro";
|
import Spinner from "@/components/Spinner.astro";
|
||||||
|
|
@ -15,7 +20,7 @@ const {
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang={htmlLang}>
|
||||||
<head>
|
<head>
|
||||||
<ClientRouter />
|
<ClientRouter />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
@ -23,6 +28,16 @@ const {
|
||||||
<SEO title={pageTitle} description={description} image={image} />
|
<SEO title={pageTitle} description={description} image={image} />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>{pageTitle}</title>
|
<title>{pageTitle}</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from "./BaseLayout.astro";
|
import BaseLayout from "./BaseLayout.astro";
|
||||||
import Remark42Embed from "@/components/Remark42Embed.astro";
|
import Remark42Embed from "@/components/Remark42Embed.astro";
|
||||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
import PostNav from "@/components/Posts/PostNav.astro";
|
||||||
|
import PostMenu from "@/components/Posts/PostMenu.astro";
|
||||||
|
import Dialog from "@/components/Posts/Dialog.astro";
|
||||||
|
import { fade } from "astro:transitions";
|
||||||
|
import { getTranslations } from "@/i18n";
|
||||||
import "@/styles/global.css";
|
import "@/styles/global.css";
|
||||||
|
|
||||||
const { frontmatter, lang, postId } = Astro.props;
|
const { post, frontmatter, lang, slug, postId, headings } = Astro.props;
|
||||||
const comments = lang === "zh" ? "评论区" : "comments";
|
const comments = lang === "zh" ? "评论区" : "comments";
|
||||||
const t = getTranslations(lang);
|
const t = getTranslations(lang);
|
||||||
---
|
---
|
||||||
|
|
@ -14,7 +18,7 @@ const t = getTranslations(lang);
|
||||||
description={frontmatter.description}
|
description={frontmatter.description}
|
||||||
image={frontmatter.image?.url}
|
image={frontmatter.image?.url}
|
||||||
>
|
>
|
||||||
<div id="reading-progress" aria-hidden="true"></div>
|
<div id="reading-progress" aria-hidden="true"></div>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="back-button"
|
class="back-button"
|
||||||
|
|
@ -36,17 +40,24 @@ const t = getTranslations(lang);
|
||||||
<path d="M15 18l-6-6 6-6"></path>
|
<path d="M15 18l-6-6 6-6"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<PostMenu headings={headings} />
|
||||||
|
|
||||||
<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" transition:name={`post-title-${postId}`} >{frontmatter.title}</h1>
|
<h1 class="post-title" transition:name={`post-title-${postId}`}>
|
||||||
<p class="description"><em>{frontmatter.description}</em></p>
|
{frontmatter.title}
|
||||||
|
</h1>
|
||||||
|
<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()}
|
||||||
</p>
|
</p>
|
||||||
<p class="meta-line">{t.post.writtenBy}: {frontmatter.author}</p>
|
<p class="meta-line">{t.post.writtenBy}: {frontmatter.author}</p>
|
||||||
|
|
||||||
<div class="tags">
|
<div class="tags" transition:name={`post-tags-${postId}`}>
|
||||||
{
|
{
|
||||||
frontmatter.tags.map((tag: string) => (
|
frontmatter.tags.map((tag: string) => (
|
||||||
<p class="tag">
|
<p class="tag">
|
||||||
|
|
@ -61,17 +72,23 @@ const t = getTranslations(lang);
|
||||||
src={frontmatter.image.url}
|
src={frontmatter.image.url}
|
||||||
alt={frontmatter.image.alt}
|
alt={frontmatter.image.alt}
|
||||||
class="post-cover"
|
class="post-cover"
|
||||||
|
transition:name={`post-image-${postId}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="post-divider"></div>
|
<div class="post-divider"></div>
|
||||||
|
|
||||||
<div class="post-content">
|
<div class="post-content" transition:animate={fade({ duration: "0.2s" })}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog />
|
||||||
|
|
||||||
|
<PostNav post={post} lang={lang} />
|
||||||
|
|
||||||
<h2>{comments}</h2>
|
<h2>{comments}</h2>
|
||||||
<Remark42Embed slug={postId} />
|
{t.post.commentsDesc.map((line: string) => <p set:html={line} />)}
|
||||||
|
<Remark42Embed slug={slug} />
|
||||||
</article>
|
</article>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
|
@ -131,7 +148,7 @@ const t = getTranslations(lang);
|
||||||
backButton.dataset.bound = "true";
|
backButton.dataset.bound = "true";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const updateProgress = () => {
|
const updateProgress = () => {
|
||||||
const doc = document.documentElement;
|
const doc = document.documentElement;
|
||||||
const scrollTop = window.scrollY || doc.scrollTop;
|
const scrollTop = window.scrollY || doc.scrollTop;
|
||||||
const scrollHeight = doc.scrollHeight - window.innerHeight;
|
const scrollHeight = doc.scrollHeight - window.innerHeight;
|
||||||
|
|
@ -152,7 +169,7 @@ const t = getTranslations(lang);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#reading-progress {
|
#reading-progress {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
@ -175,7 +192,7 @@ const t = getTranslations(lang);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: #285ee9 1.5px solid;
|
border: #536493 1.5px solid;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: rgba(255, 255, 255, 0.92);
|
||||||
color: #222;
|
color: #222;
|
||||||
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
|
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
|
||||||
|
|
@ -186,8 +203,8 @@ const t = getTranslations(lang);
|
||||||
/* opacity: 0; */
|
/* opacity: 0; */
|
||||||
transition:
|
transition:
|
||||||
opacity 0.2s ease,
|
opacity 0.2s ease,
|
||||||
transform 0.27s ease,
|
transform 0.27s ease,
|
||||||
visibility 0.2s ease;
|
visibility 0.2s ease,
|
||||||
box-shadow 0.2s ease,
|
box-shadow 0.2s ease,
|
||||||
background 0.2s ease,
|
background 0.2s ease,
|
||||||
color 0.2s ease;
|
color 0.2s ease;
|
||||||
|
|
@ -234,6 +251,7 @@ const t = getTranslations(lang);
|
||||||
"Noto Sans CJK SC",
|
"Noto Sans CJK SC",
|
||||||
"Source Han Sans SC",
|
"Source Han Sans SC",
|
||||||
sans-serif;
|
sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-content :global(code),
|
.post-content :global(code),
|
||||||
|
|
@ -284,10 +302,11 @@ const t = getTranslations(lang);
|
||||||
.post-cover {
|
.post-cover {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: auto;
|
height: auto;
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 10;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
border-radius: 0.4rem;
|
/* border-radius: 0.4rem; */
|
||||||
|
border: 1.5px #94a0ab dashed;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -346,6 +365,91 @@ const t = getTranslations(lang);
|
||||||
.post-cover {
|
.post-cover {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
details {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: 2px solid #1e3a5f;
|
||||||
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
|
background: rgba(128, 128, 128, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.8rem 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary::before {
|
||||||
|
content: "›";
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 展开内容 */
|
||||||
|
details > :not(summary) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 为列表序号预留空间 */
|
||||||
|
details > ol,
|
||||||
|
details > ul {
|
||||||
|
padding: 0.6rem 1rem 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
details li {
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-left: 4px solid var(--deep-red);
|
||||||
|
background: rgba(128, 128, 128, 0.1);
|
||||||
|
border-radius: 0 0.4rem 0.4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote p + p {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 块级公式:超出屏幕时只滚动公式,不撑宽整篇文章 */
|
||||||
|
.katex-display {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 避免 KaTeX 内部元素强行把父容器撑宽 */
|
||||||
|
.katex-display > .katex {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||||
import FriendlyLinkList from "@/components/FriendlyLinks/FriendlyLinkList.astro";
|
import FriendlyLinkList from "@/components/FriendlyLinks/FriendlyLinkList.astro";
|
||||||
|
import FriendsDescription from "@/components/FriendlyLinks/FriendsDescription.astro";
|
||||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
import { getLangFromUrl, getTranslations } from "@/i18n";
|
||||||
import "@/styles/global.css";
|
import "@/styles/global.css";
|
||||||
|
|
||||||
|
|
@ -14,5 +15,16 @@ const headerTitle = lang === "zh" ? "友情链接" : "Friends";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}`>
|
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}`>
|
||||||
|
<FriendsDescription />
|
||||||
|
<div class="section-divider"></div>
|
||||||
<FriendlyLinkList />
|
<FriendlyLinkList />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.section-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #cdd2d8;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,14 @@ const lang = getLangFromUrl(Astro.url);
|
||||||
const t = getTranslations(lang);
|
const t = getTranslations(lang);
|
||||||
const headerTitle = lang === "zh" ? "Cloverta的博客" : "Cloverta's blog";
|
const headerTitle = lang === "zh" ? "Cloverta的博客" : "Cloverta's blog";
|
||||||
const pageTitle = t.home.title;
|
const pageTitle = t.home.title;
|
||||||
|
const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`>
|
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`>
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
<span>{pageTitle}</span>
|
<span>{pageTitle}</span>
|
||||||
<a
|
<a
|
||||||
href={`/rss.xml`}
|
href={rssLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label="RSS Feed"
|
aria-label="RSS Feed"
|
||||||
|
|
@ -33,7 +34,7 @@ const pageTitle = t.home.title;
|
||||||
width="33"
|
width="33"
|
||||||
height="33"
|
height="33"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
style="margin-left: -15px; margin-bottom: -5px; color:#3f50e5"
|
style="margin-left: 0px; margin-bottom: -5px; color:#3f50e5"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M6.18 17.82a1.64 1.64 0 1 1 0 3.28 1.64 1.64 0 0 1 0-3.28ZM3 10.44v2.25c4.56 0 8.27 3.71 8.27 8.27h2.25C13.52 15.16 8.84 10.44 3 10.44Zm0-4.54v2.25c7.06 0 12.81 5.75 12.81 12.81h2.25C18.06 12.66 11.3 5.9 3 5.9Z"
|
d="M6.18 17.82a1.64 1.64 0 1 1 0 3.28 1.64 1.64 0 0 1 0-3.28ZM3 10.44v2.25c4.56 0 8.27 3.71 8.27 8.27h2.25C13.52 15.16 8.84 10.44 3 10.44Zm0-4.54v2.25c7.06 0 12.81 5.75 12.81 12.81h2.25C18.06 12.66 11.3 5.9 3 5.9Z"
|
||||||
|
|
@ -47,6 +48,8 @@ const pageTitle = t.home.title;
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
|
<h2>{t.home.latestTitle}</h2>
|
||||||
|
|
||||||
<PostList />
|
<PostList />
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
|
|
@ -66,4 +69,8 @@ const pageTitle = t.home.title;
|
||||||
:global(.dark) .section-divider {
|
:global(.dark) .section-divider {
|
||||||
background: #7f8b97;
|
background: #7f8b97;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,19 @@ export async function getStaticPaths() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { post, lang } = Astro.props;
|
const { post, lang } = Astro.props;
|
||||||
const { Content } = await render(post);
|
const { Content, headings } = await render(post);
|
||||||
|
|
||||||
const [postLang, ...slugParts] = post.id.split("/");
|
const [postLang, ...slugParts] = post.id.split("/");
|
||||||
const slug = slugParts.join("/");
|
const slug = slugParts.join("/");
|
||||||
---
|
---
|
||||||
|
|
||||||
<MarkdownPostLayout frontmatter={post.data} lang={lang} postId={slug}>
|
<MarkdownPostLayout
|
||||||
|
post={post}
|
||||||
|
frontmatter={post.data}
|
||||||
|
lang={lang}
|
||||||
|
slug={slug}
|
||||||
|
postId={post.id}
|
||||||
|
headings={headings}
|
||||||
|
>
|
||||||
<Content />
|
<Content />
|
||||||
</MarkdownPostLayout>
|
</MarkdownPostLayout>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const t = getTranslations(lang);
|
||||||
<BaseLayout pageTitle={String(tag)}>
|
<BaseLayout pageTitle={String(tag)}>
|
||||||
<p class="tag-heading">
|
<p class="tag-heading">
|
||||||
{lang === "zh" ? "带有标签" : "Posts tagged with"}{" "}
|
{lang === "zh" ? "带有标签" : "Posts tagged with"}{" "}
|
||||||
<span class="tag-chip">{tag}</span>
|
<span class="tag-chip" transition:name={`post-tags-${tag}`}>{tag}</span>
|
||||||
{lang === "zh" ? "的文章" : ""}
|
{lang === "zh" ? "的文章" : ""}
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -54,6 +54,7 @@ const t = getTranslations(lang);
|
||||||
date={formattedDate}
|
date={formattedDate}
|
||||||
img={post.data.image.url}
|
img={post.data.image.url}
|
||||||
tags={post.data.tags}
|
tags={post.data.tags}
|
||||||
|
postId={post.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@ const tags = [...new Set(allPosts.map((post: any) => post.data.tags).flat())];
|
||||||
const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
|
const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}` `>
|
<BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}`>
|
||||||
<h1>{t.tags.title}</h1>
|
<h1>{t.tags.title}</h1>
|
||||||
<p>{t.tags.description}</p>
|
<p>{t.tags.description}</p>
|
||||||
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
{
|
{
|
||||||
tags.map((tag) => (
|
tags.map((tag) => (
|
||||||
<p class="tag">
|
<p class="tag" transition:name={`post-tags-${tag}`}>
|
||||||
<a href={`/${lang}/tags/${tag}`}>{tag}</a>
|
<a href={`/${lang}/tags/${tag}`}>{tag}</a>
|
||||||
</p>
|
</p>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
35
src/pages/en/rss.xml.js
Normal file
35
src/pages/en/rss.xml.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import rss from '@astrojs/rss';
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
export async function GET(context) {
|
||||||
|
const posts = await getCollection('blog');
|
||||||
|
|
||||||
|
const enPosts = posts
|
||||||
|
.filter((post) => post.id.startsWith('en/'))
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.data.pubDate).getTime() -
|
||||||
|
new Date(a.data.pubDate).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
title: "Cloverta's Blog",
|
||||||
|
description:
|
||||||
|
"Discover more here. Welcome to Cloverta's blog 🥳",
|
||||||
|
site: context.site,
|
||||||
|
|
||||||
|
items: enPosts.map((post) => {
|
||||||
|
const [, ...slugParts] = post.id.split('/');
|
||||||
|
const slug = slugParts.join('/');
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: post.data.title,
|
||||||
|
pubDate: post.data.pubDate,
|
||||||
|
description: post.data.description,
|
||||||
|
link: `/en/posts/${slug}/`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
customData: '<language>en-US</language>',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,34 @@
|
||||||
import rss from '@astrojs/rss';
|
import rss from '@astrojs/rss';
|
||||||
import { pagesGlobToRssItems } from '@astrojs/rss';
|
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
export async function GET(context) {
|
export async function GET(context) {
|
||||||
const posts = await getCollection("blog");
|
const posts = await getCollection('blog');
|
||||||
|
|
||||||
|
const zhPosts = posts
|
||||||
|
.filter((post) => post.id.startsWith('zh/'))
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.data.pubDate).getTime() -
|
||||||
|
new Date(a.data.pubDate).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
return rss({
|
return rss({
|
||||||
title: 'Cloverta的博客',
|
title: 'Cloverta的博客',
|
||||||
description: '在这里,发现更多(雾)欢迎来到三叶的博客🥳',
|
description: '在这里,发现更多(雾)欢迎来到三叶的博客🥳',
|
||||||
site: context.site,
|
site: context.site,
|
||||||
items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')),
|
|
||||||
items: posts.map((post) => {
|
|
||||||
const [postLang, ...slugParts] = post.id.split("/");
|
|
||||||
const slug = slugParts.join("/");
|
|
||||||
|
|
||||||
return ({
|
items: zhPosts.map((post) => {
|
||||||
|
const [, ...slugParts] = post.id.split('/');
|
||||||
|
const slug = slugParts.join('/');
|
||||||
|
|
||||||
|
return {
|
||||||
title: post.data.title,
|
title: post.data.title,
|
||||||
pubDate: post.data.pubDate,
|
pubDate: post.data.pubDate,
|
||||||
description: post.data.description,
|
description: post.data.description,
|
||||||
link: `/${postLang}/posts/${slug}/`,
|
link: `/zh/posts/${slug}/`,
|
||||||
})
|
};
|
||||||
}),
|
}),
|
||||||
customData: `<language>en-us</language>`,
|
|
||||||
})
|
customData: '<language>zh-CN</language>',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
95
src/styles/dialog.css
Normal file
95
src/styles/dialog.css
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
.image-preview-dialog {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
max-width: min(96vw, 1200px);
|
||||||
|
max-height: 96vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-dialog::backdrop {
|
||||||
|
background: rgba(0, 0, 0, 0.72);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-dialog img {
|
||||||
|
display: block;
|
||||||
|
max-width: 96vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
object-fit: contain;
|
||||||
|
box-shadow: 0 20px 80px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-close:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-close:focus-visible {
|
||||||
|
outline: 2px solid rgba(125, 162, 255, 0.9);
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-close {
|
||||||
|
position: fixed;
|
||||||
|
top: 1.25rem;
|
||||||
|
right: 1.25rem;
|
||||||
|
z-index: 1;
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-color-dark);
|
||||||
|
font-size: 2.6rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .image-preview-close:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* 给文章图片一个可点击提示 */
|
||||||
|
.prose img,
|
||||||
|
.markdown-body img,
|
||||||
|
article img {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-download {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: max(1.5rem, env(safe-area-inset-bottom));
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0.4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.88;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-download:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-download:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-download:focus-visible {
|
||||||
|
outline: 2px solid rgba(125, 162, 255, 0.9);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,9 @@
|
||||||
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400.css");
|
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400.css");
|
||||||
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400-italic.css");
|
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400-italic.css");
|
||||||
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/700.css");
|
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/700.css");
|
||||||
|
@import "./latest-comments.css";
|
||||||
@font-face {
|
@import "./variables.css";
|
||||||
font-family: "Maple Mono CN";
|
@import "./dialog.css";
|
||||||
src: url("/fonts/subset/MapleMono-CN-Regular.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Maple Mono CN";
|
|
||||||
src: url("/fonts/subset/MapleMono-CN-Italic.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Maple Mono CN";
|
|
||||||
src: url("/fonts/subset/MapleMono-CN-Bold.woff2") format("woff2");
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
@ -57,9 +36,18 @@ article svg[id^="mermaid-"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: "Maple Mono", "Maple Mono CN", monospace;
|
/* font-family: "Maple Mono", "Maple Mono CN", monospace; */
|
||||||
background-color: #ffffff;
|
font-family:
|
||||||
color: #1f2328;
|
"Noto Serif SC",
|
||||||
|
"Source Han Serif SC",
|
||||||
|
"Songti SC",
|
||||||
|
"STSong",
|
||||||
|
"SimSun",
|
||||||
|
serif;
|
||||||
|
font-weight: 500;
|
||||||
|
/* background-color: #ffffff; */
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
@ -86,13 +74,14 @@ body::after {
|
||||||
/* 按需调整大小 */
|
/* 按需调整大小 */
|
||||||
height: 400px;
|
height: 400px;
|
||||||
/* 按需调整大小 */
|
/* 按需调整大小 */
|
||||||
background-image: url("https://files.seeusercontent.com/2026/03/30/4Xfr/bc7e804dc2c8ecaf407c9d665414ff72.webp");
|
background-image: url("https://files.seeusercontent.com/2026/04/16/1sXb/touhou___kirisame_marisa__2__by_.webp");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: right bottom;
|
background-position: right bottom;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark body::after {
|
html.dark body::after {
|
||||||
|
|
@ -101,7 +90,7 @@ html.dark body::after {
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
body::after {
|
body::after {
|
||||||
opacity: 0.1;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,10 +102,6 @@ html.dark body::after {
|
||||||
body {
|
body {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::after {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -132,7 +117,7 @@ h1 {
|
||||||
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #416bd6;
|
color: #3D74B6;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
@ -178,265 +163,110 @@ img {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-comments {
|
/* 避免 Markdown 内容撑宽正文 */
|
||||||
margin-top: 1.5rem;
|
.post-article,
|
||||||
}
|
.post-content {
|
||||||
|
|
||||||
.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;
|
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%;
|
max-width: 100%;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-comments-empty {
|
/* 块级公式过长时独立横向滚动 */
|
||||||
color: #777;
|
.post-content .katex-display {
|
||||||
font-style: italic;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-reply-sep {
|
.post-content .katex-display > .katex {
|
||||||
font-size: 0.8rem;
|
display: inline-block;
|
||||||
opacity: 0.7;
|
min-width: max-content;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-comments .comment-reply-to {
|
/* =========================
|
||||||
font-weight: 400;
|
Markdown details 下拉栏
|
||||||
font-size: 0.8rem;
|
========================= */
|
||||||
color: gray;
|
|
||||||
opacity: 0.9;
|
.post-content details {
|
||||||
overflow-wrap: anywhere;
|
min-width: 0;
|
||||||
word-break: break-word;
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
border-top: 2px solid #1e3a5f;
|
||||||
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
|
background: rgba(128, 128, 128, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .latest-comments .comment-card,
|
.post-content details summary {
|
||||||
.dark .latest-comments .comment-card {
|
display: flex;
|
||||||
border-color: #7f8c97;
|
align-items: center;
|
||||||
background-color: #252525;
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.8rem 0.9rem;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .comment-title-link {
|
.post-content details summary::-webkit-details-marker {
|
||||||
color: #c8d2dc;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .comment-card-text {
|
.post-content details summary::before {
|
||||||
color: #d3d7db;
|
content: "›";
|
||||||
|
display: inline-block;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .comment-time {
|
.post-content details[open] summary::before {
|
||||||
color: #a8b0b7;
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .latest-comments .comment-avatar-fallback,
|
.post-content details > :not(summary) {
|
||||||
.dark .latest-comments .comment-avatar-fallback {
|
min-width: 0;
|
||||||
border-color: #7f8c97;
|
max-width: 100%;
|
||||||
background-color: #3a444d;
|
box-sizing: border-box;
|
||||||
color: #dbe3ea;
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .comment-avatar-fallback {
|
.post-content details > ol,
|
||||||
background: rgba(180, 190, 200, 0.12);
|
.post-content details > ul {
|
||||||
color: #dbe3ea;
|
padding: 0.6rem 1rem 1rem 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
.post-content details li {
|
||||||
.comment-card-body {
|
margin: 0.8rem 0;
|
||||||
grid-template-columns: 1fr;
|
}
|
||||||
grid-template-areas:
|
|
||||||
"title"
|
blockquote {
|
||||||
"info"
|
margin: 1.25rem 0;
|
||||||
"text";
|
padding: 0.75rem 1rem;
|
||||||
}
|
border-left: 4px solid var(--deep-red);
|
||||||
|
background: rgba(128, 128, 128, 0.1);
|
||||||
.comment-card-title {
|
border-radius: 0 0.4rem 0.4rem 0;
|
||||||
text-align: left;
|
}
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
}
|
blockquote p {
|
||||||
|
margin: 0;
|
||||||
.comment-title-link {
|
}
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
blockquote p + p {
|
||||||
overflow: hidden;
|
margin-top: 0.75rem;
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
263
src/styles/latest-comments.css
Normal file
263
src/styles/latest-comments.css
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
.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: 2px dashed #aeb8c2;
|
||||||
|
background-color: #fbf5f2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/styles/variables.css
Normal file
9
src/styles/variables.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
:root {
|
||||||
|
--deep-blue: #536493;
|
||||||
|
--deep-red: #ef5a6f;
|
||||||
|
--background-color: #f9f2ed;
|
||||||
|
--text-color: #0E2F56;
|
||||||
|
|
||||||
|
--background-color-dark: #1e1e1e;
|
||||||
|
--text-color-dark: #e6e6e6;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue