mirror of
https://github.com/ClovertaTheTrilobita/SanYeCao-blog.git
synced 2026-07-05 00:21:27 +00:00
Compare commits
103 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 | |||
| 7566503db7 | |||
| d7fa430317 | |||
| f4425d8680 | |||
| 10cad307eb | |||
| 975ecf9185 | |||
| ee6112b3f8 | |||
| 131a5f8c7c | |||
| b2b7ecb34a | |||
| bdb1560dfb | |||
| daa458f813 | |||
|
|
0ae9e746a8 | ||
| 490c66f6b7 | |||
| f53b492431 | |||
| c1db660259 | |||
| 2f279848b9 | |||
|
|
e12bcc0b24 | ||
| 8dee31ca16 | |||
| 9c8c0e186f | |||
| 74b3b723cf | |||
| 2b2222c740 | |||
| a06f3d524f | |||
| fdd55fb068 | |||
| 07fbc4b731 | |||
| 018ad4ab1e | |||
| 703a63fffc | |||
| c2aa6c2a68 | |||
| c626bdbb07 | |||
|
|
6ac21acc63 | ||
| 6b804c3519 | |||
| 141c15f557 | |||
| 04d81bd3a5 | |||
| cd7e72caee | |||
|
|
11ddcb4ae0 | ||
| 0775eb7927 | |||
| 08bdca532a |
54 changed files with 4663 additions and 4541 deletions
135
.github/workflows/deploy-blog.yml
vendored
135
.github/workflows/deploy-blog.yml
vendored
|
|
@ -1,44 +1,54 @@
|
||||||
name: Deploy Astro Blog
|
name: Sync and Deploy Astro Blog
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- blog-content
|
- master
|
||||||
paths:
|
|
||||||
- 'src/blog/**'
|
|
||||||
- 'src/friends/**'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
- 'astro.config.mjs'
|
|
||||||
- 'astro.config.ts'
|
|
||||||
- 'public/**'
|
|
||||||
- '.github/workflows/deploy-blog.yml'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
discussion:
|
|
||||||
types: [created, category_changed]
|
|
||||||
|
|
||||||
discussion_comment:
|
|
||||||
types: [created, edited, deleted]
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: deploy-blog
|
group: deploy-blog
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
sync-blog-content:
|
||||||
if: >
|
if: github.event_name == 'push'
|
||||||
github.event_name == 'push' ||
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
(
|
|
||||||
github.event_name == 'discussion' &&
|
|
||||||
github.event.discussion.category.slug == 'comments'
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
github.event_name == 'discussion_comment' &&
|
|
||||||
github.event.discussion.category.slug == 'comments'
|
|
||||||
)
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Checkout blog-content branch
|
||||||
|
run: |
|
||||||
|
git checkout blog-content
|
||||||
|
git pull origin blog-content
|
||||||
|
|
||||||
|
- name: Merge master into blog-content
|
||||||
|
run: |
|
||||||
|
git merge origin/master --no-edit
|
||||||
|
|
||||||
|
- name: Push blog-content
|
||||||
|
run: |
|
||||||
|
git push origin blog-content
|
||||||
|
|
||||||
|
build-and-deploy:
|
||||||
|
needs: sync-blog-content
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://blog.cloverta.top
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||||
|
|
@ -48,7 +58,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout blog-content branch
|
- name: Checkout blog-content branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: blog-content
|
ref: blog-content
|
||||||
|
|
||||||
|
|
@ -61,6 +71,71 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install chromium --with-deps
|
||||||
|
|
||||||
|
- name: Create .env file
|
||||||
|
run: |
|
||||||
|
cat > .env <<'EOF'
|
||||||
|
${{ secrets.ENV_FILE }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Build Astro site
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Install rsync and ssh client
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y rsync openssh-client
|
||||||
|
|
||||||
|
- name: Setup SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
|
chmod 644 ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
run: |
|
||||||
|
rsync -avz --delete \
|
||||||
|
--omit-dir-times \
|
||||||
|
--no-perms \
|
||||||
|
--no-owner \
|
||||||
|
--no-group \
|
||||||
|
-e "ssh -p ${DEPLOY_PORT}" \
|
||||||
|
dist/ \
|
||||||
|
${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/
|
||||||
|
|
||||||
|
manual-deploy:
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://blog.cloverta.top
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||||
|
DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
|
||||||
|
DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
|
||||||
|
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout blog-content branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: blog-content
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install chromium --with-deps
|
||||||
|
|
||||||
- name: Create .env file
|
- name: Create .env file
|
||||||
run: |
|
run: |
|
||||||
cat > .env <<'EOF'
|
cat > .env <<'EOF'
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,10 @@ For installation and deployment details, please refer to the official documentat
|
||||||
|
|
||||||
For details, see: <b>[ChangeLog-en.md](./docs/ChangeLog-en.md)</b>
|
For details, see: <b>[ChangeLog-en.md](./docs/ChangeLog-en.md)</b>
|
||||||
|
|
||||||
|
### 6. Older Versions
|
||||||
|
|
||||||
|
Older versions of GISCUS are available on the <b>[deprecated/giscus](https://github.com/ClovertaTheTrilobita/SanYeCao-blog/tree/deprecated/giscus)</b> branch, but they haven’t been updated in a while and may contain numerous issues.
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## ⚖️ License
|
## ⚖️ License
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# SanYeCao-Blog
|
# SanYeCao-Blog
|
||||||
|
|
||||||
[中文](README.md) | [English](README-en.md)
|
<p align="right">[<a href="./README.md">中文</a> | <a href="./README-en.md">English</a>]</p>
|
||||||
|
|
||||||
<p align="center"><br>
|
<p align="center"><br>
|
||||||
✨三叶草Blog✨<br>
|
✨三叶草Blog✨<br>
|
||||||
|
|
@ -88,6 +88,10 @@
|
||||||
|
|
||||||
详情请看:<b>[ChangeLog.md](./docs/ChangeLog.md)</b>
|
详情请看:<b>[ChangeLog.md](./docs/ChangeLog.md)</b>
|
||||||
|
|
||||||
|
### 6. 旧版本
|
||||||
|
|
||||||
|
支持GISCUS的旧版本在 <b>[deprecated/giscus](https://github.com/ClovertaTheTrilobita/SanYeCao-blog/tree/deprecated/giscus)</b> 分支上,但是它有些时候没更新了,可能会有诸多问题。
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## ⚖️ 许可
|
## ⚖️ 许可
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import sitemap from "@astrojs/sitemap";
|
import sitemap from "@astrojs/sitemap";
|
||||||
import svelte from "@astrojs/svelte";
|
import svelte from "@astrojs/svelte";
|
||||||
|
import rehypeMermaid from "rehype-mermaid";
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
@ -18,4 +21,15 @@ export default defineConfig({
|
||||||
"/": "/zh",
|
"/": "/zh",
|
||||||
},
|
},
|
||||||
integrations: [sitemap(), svelte()],
|
integrations: [sitemap(), svelte()],
|
||||||
|
markdown: {
|
||||||
|
syntaxHighlight: {
|
||||||
|
type: 'shiki',
|
||||||
|
excludeLangs: ['mermaid', 'math'],
|
||||||
|
},
|
||||||
|
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);
|
|
||||||
});
|
|
||||||
5525
package-lock.json
generated
5525
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
|
@ -6,21 +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",
|
||||||
"url": "^0.11.4"
|
"katex": "^0.17.0",
|
||||||
|
"playwright": "^1.59.1",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"rehype-mermaid": "^3.0.0",
|
||||||
|
"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>
|
||||||
224
src/components/FloatingActions.astro
Normal file
224
src/components/FloatingActions.astro
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
<div class="floating-actions">
|
||||||
|
<button
|
||||||
|
id="theme-toggle-fab"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
title="Toggle theme"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="theme-toggle-icon"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
class="sun"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
class="moon"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="back-to-top" aria-label="Back to top" title="Back to top">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M18 15l-6-6-6 6"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.floating-actions {
|
||||||
|
position: fixed;
|
||||||
|
right: 1.25rem;
|
||||||
|
bottom: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-toggle-fab,
|
||||||
|
#back-to-top {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border: #a7a7a7 1.5px solid;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: #222;
|
||||||
|
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-toggle-fab {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-toggle-fab {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-icon {
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-icon .sun,
|
||||||
|
.theme-toggle-icon .moon {
|
||||||
|
transform-origin: center;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-icon .sun {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-icon .moon {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.75) rotate(-20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .theme-toggle-icon .sun {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.75) rotate(20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .theme-toggle-icon .moon {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-toggle-fab,
|
||||||
|
#back-to-top {
|
||||||
|
/* opacity: 0; */
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(150px);
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.27s ease,
|
||||||
|
visibility 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-toggle-fab.show,
|
||||||
|
#back-to-top.show {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-toggle-fab.show:hover,
|
||||||
|
#back-to-top.show:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) #theme-toggle-fab,
|
||||||
|
:global(.dark) #back-to-top {
|
||||||
|
background: rgba(34, 34, 34, 0.92);
|
||||||
|
color: #f5f5f5;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
|
||||||
|
border: #515151 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.floating-actions {
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-toggle-fab,
|
||||||
|
#back-to-top {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const initFloatingActions = () => {
|
||||||
|
const backToTop = document.getElementById("back-to-top");
|
||||||
|
const themeToggle = document.getElementById("theme-toggle-fab");
|
||||||
|
|
||||||
|
if (backToTop && backToTop.dataset.bound !== "true") {
|
||||||
|
const toggleBackToTop = () => {
|
||||||
|
if (window.scrollY > 300) {
|
||||||
|
backToTop.classList.add("show");
|
||||||
|
} else {
|
||||||
|
backToTop.classList.remove("show");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
backToTop.addEventListener("click", () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("scroll", toggleBackToTop, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
toggleBackToTop();
|
||||||
|
backToTop.dataset.bound = "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeToggle && themeToggle.dataset.bound !== "true") {
|
||||||
|
const togglethemeToggle = () => {
|
||||||
|
if (window.scrollY > 300) {
|
||||||
|
themeToggle.classList.add("show");
|
||||||
|
} else {
|
||||||
|
themeToggle.classList.remove("show");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
const applyTheme = (theme: string) => {
|
||||||
|
if (theme === "dark") {
|
||||||
|
root.classList.add("dark");
|
||||||
|
localStorage.setItem("color-theme", "dark");
|
||||||
|
} else {
|
||||||
|
root.classList.remove("dark");
|
||||||
|
localStorage.setItem("color-theme", "light");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
themeToggle.addEventListener("click", () => {
|
||||||
|
const isDark = root.classList.contains("dark");
|
||||||
|
applyTheme(isDark ? "light" : "dark");
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("scroll", togglethemeToggle, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
togglethemeToggle();
|
||||||
|
themeToggle.dataset.bound = "true";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initFloatingActions();
|
||||||
|
document.addEventListener("astro:page-load", initFloatingActions);
|
||||||
|
</script>
|
||||||
|
|
@ -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>
|
||||||
299
src/components/LatestComments.astro
Normal file
299
src/components/LatestComments.astro
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
---
|
||||||
|
const { lang = "zh" } = Astro.props;
|
||||||
|
const host = import.meta.env.PUBLIC_REMARK42_HOST;
|
||||||
|
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||||
|
const max = 10;
|
||||||
|
const heading = lang === "zh" ? "最新评论" : "Latest comments";
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="latest-comments"
|
||||||
|
data-lang={lang}
|
||||||
|
data-host={host}
|
||||||
|
data-site-id={siteId}
|
||||||
|
data-max={max}
|
||||||
|
>
|
||||||
|
<h2>{heading}</h2>
|
||||||
|
|
||||||
|
<div class="comments-loading" aria-hidden="true">
|
||||||
|
<div class="loading-card"></div>
|
||||||
|
<div class="loading-card"></div>
|
||||||
|
<div class="loading-card"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="latest-comments-list" aria-live="polite"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script is:inline data-astro-rerun>
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str ?? "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(input, lang) {
|
||||||
|
try {
|
||||||
|
const date = new Date(input);
|
||||||
|
return new Intl.DateTimeFormat(lang === "zh" ? "zh-CN" : "en", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(date);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewritePostUrl(url, lang) {
|
||||||
|
if (!url) return "#";
|
||||||
|
|
||||||
|
if (url.startsWith("/posts/")) {
|
||||||
|
return `/${lang}${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const u = new URL(url, window.location.origin);
|
||||||
|
if (u.pathname.startsWith("/posts/")) {
|
||||||
|
return `/${lang}${u.pathname}${u.search}${u.hash}`;
|
||||||
|
}
|
||||||
|
return u.pathname + u.search + u.hash;
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvatar(item) {
|
||||||
|
return (
|
||||||
|
item?.user?.picture || item?.user?.photo || item?.user?.avatar || ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserName(item, lang) {
|
||||||
|
return item?.user?.name || (lang === "zh" ? "匿名用户" : "Anonymous");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(url) {
|
||||||
|
if (!url) return "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const u = new URL(url, window.location.origin);
|
||||||
|
let path = u.pathname;
|
||||||
|
|
||||||
|
if (!path.startsWith("/")) path = "/" + path;
|
||||||
|
if (!path.endsWith("/")) path += "/";
|
||||||
|
|
||||||
|
return path;
|
||||||
|
} catch {
|
||||||
|
let path = String(url);
|
||||||
|
|
||||||
|
path = path.replace(/^https?:\/\/[^/]+/i, "");
|
||||||
|
if (!path.startsWith("/")) path = "/" + path;
|
||||||
|
if (!path.endsWith("/")) path += "/";
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrlCandidates(rawUrl) {
|
||||||
|
const path = normalizePath(rawUrl);
|
||||||
|
if (!path) return [];
|
||||||
|
|
||||||
|
const noSlash = path.endsWith("/") ? path.slice(0, -1) : path;
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
path,
|
||||||
|
noSlash,
|
||||||
|
`/zh${path}`,
|
||||||
|
`/zh${noSlash}`,
|
||||||
|
`/en${path}`,
|
||||||
|
`/en${noSlash}`,
|
||||||
|
`${window.location.origin}${path}`,
|
||||||
|
`${window.location.origin}${noSlash}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...new Set(candidates.filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPostComments(host, siteId, rawUrl) {
|
||||||
|
const candidates = buildUrlCandidates(rawUrl);
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${host}/api/v1/find?site=${encodeURIComponent(siteId)}&url=${encodeURIComponent(candidate)}&format=plain`,
|
||||||
|
{ credentials: "omit" },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) continue;
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const comments = Array.isArray(data)
|
||||||
|
? data
|
||||||
|
: Array.isArray(data?.comments)
|
||||||
|
? data.comments
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (comments.length > 0) {
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enrichReplyTargets(host, siteId, comments) {
|
||||||
|
const urls = [
|
||||||
|
...new Set(
|
||||||
|
comments.map((item) => item?.locator?.url).filter(Boolean),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const postMap = new Map();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
urls.map(async (url) => {
|
||||||
|
const postComments = await fetchPostComments(host, siteId, url);
|
||||||
|
postMap.set(url, postComments);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return comments.map((item) => {
|
||||||
|
if (!item?.pid) {
|
||||||
|
return { ...item, replyToName: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const postComments = postMap.get(item?.locator?.url) || [];
|
||||||
|
const parent = postComments.find(
|
||||||
|
(c) => String(c?.id) === String(item?.pid),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
replyToName: parent?.user?.name || "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComments(root, comments, lang) {
|
||||||
|
const list = root.querySelector(".latest-comments-list");
|
||||||
|
const loading = root.querySelector(".comments-loading");
|
||||||
|
if (!list || !loading) return;
|
||||||
|
|
||||||
|
if (!Array.isArray(comments) || comments.length === 0) {
|
||||||
|
loading.style.display = "none";
|
||||||
|
list.innerHTML = `<p class="latest-comments-empty">${
|
||||||
|
lang === "zh" ? "还没有评论" : "No comments yet"
|
||||||
|
}</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = comments
|
||||||
|
.map((item) => {
|
||||||
|
const rawAuthor = getUserName(item, lang);
|
||||||
|
const author = escapeHtml(rawAuthor);
|
||||||
|
const fallbackInitial = escapeHtml(
|
||||||
|
rawAuthor.slice(0, 1).toUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const avatar = getAvatar(item);
|
||||||
|
const safeAvatar = escapeHtml(avatar || "");
|
||||||
|
|
||||||
|
const title = escapeHtml(
|
||||||
|
item?.title ||
|
||||||
|
(lang === "zh" ? "未命名文章" : "Untitled post"),
|
||||||
|
);
|
||||||
|
const href = rewritePostUrl(item?.locator?.url || "", lang);
|
||||||
|
|
||||||
|
// Remark42 返回的 text 是处理后的 HTML,适合直接渲染
|
||||||
|
const html = item?.text || "";
|
||||||
|
const time = formatTime(item?.time, lang);
|
||||||
|
|
||||||
|
const replyTo = escapeHtml(item?.replyToName || "");
|
||||||
|
const authorLine = replyTo
|
||||||
|
? `<span class="comment-author">@${author}</span><span class="comment-reply-sep">${
|
||||||
|
lang === "zh" ? "回复" : "replied to"
|
||||||
|
}</span><span class="comment-reply-to">@${replyTo}</span>`
|
||||||
|
: `<span class="comment-author">@${author}</span>`;
|
||||||
|
|
||||||
|
const avatarHtml = avatar
|
||||||
|
? `<img class="comment-avatar-img" src="${safeAvatar}" alt="${author}" loading="lazy" referrerpolicy="no-referrer" />`
|
||||||
|
: `<div class="comment-avatar-fallback" aria-hidden="true">${fallbackInitial}</div>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<article class="comment-card">
|
||||||
|
<div class="comment-card-body">
|
||||||
|
<div class="comment-card-info">
|
||||||
|
<div class="comment-avatar">
|
||||||
|
${avatarHtml}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comment-meta">
|
||||||
|
<div class="comment-author-row">
|
||||||
|
${authorLine}
|
||||||
|
${time ? `<span class="comment-time">${escapeHtml(time)}</span>` : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comment-card-title">
|
||||||
|
<a class="comment-title-link" href="${href}">${title}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comment-card-text">${html}</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
loading.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLatestComments(section) {
|
||||||
|
if (!section) return;
|
||||||
|
|
||||||
|
const host = section.dataset.host;
|
||||||
|
const siteId = section.dataset.siteId;
|
||||||
|
const max = section.dataset.max || "10";
|
||||||
|
const lang = section.dataset.lang || "zh";
|
||||||
|
|
||||||
|
const list = section.querySelector(".latest-comments-list");
|
||||||
|
const loading = section.querySelector(".comments-loading");
|
||||||
|
if (!host || !siteId || !list || !loading) return;
|
||||||
|
|
||||||
|
loading.style.display = "";
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
const url = `${host}/api/v1/last/${encodeURIComponent(max)}?site=${encodeURIComponent(siteId)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { credentials: "omit" });
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const enriched = await enrichReplyTargets(
|
||||||
|
host,
|
||||||
|
siteId,
|
||||||
|
Array.isArray(data) ? data : [],
|
||||||
|
);
|
||||||
|
renderComments(section, enriched, lang);
|
||||||
|
} catch (err) {
|
||||||
|
loading.style.display = "none";
|
||||||
|
list.innerHTML = `<p class="latest-comments-empty">${
|
||||||
|
lang === "zh"
|
||||||
|
? "最新评论加载失败"
|
||||||
|
: "Failed to load latest comments"
|
||||||
|
}</p>`;
|
||||||
|
console.error("latest comments load failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(".latest-comments").forEach(loadLatestComments);
|
||||||
|
</script>
|
||||||
|
|
@ -7,7 +7,7 @@ const t = getTranslations(lang);
|
||||||
|
|
||||||
<nav class="site-nav" aria-label="Site navigation">
|
<nav class="site-nav" aria-label="Site navigation">
|
||||||
<div class="site-nav-desktop">
|
<div class="site-nav-desktop">
|
||||||
<a href={`/${lang}/`} data-astro-reload>{t.nav.home}</a>
|
<a href={`/${lang}/`}>{t.nav.home}</a>
|
||||||
<a href={`/${lang}/about/`}>{t.nav.about}</a>
|
<a href={`/${lang}/about/`}>{t.nav.about}</a>
|
||||||
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
||||||
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
||||||
|
|
@ -17,7 +17,7 @@ const t = getTranslations(lang);
|
||||||
<details class="site-nav-mobile">
|
<details class="site-nav-mobile">
|
||||||
<summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary>
|
<summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<a href={`/${lang}/`} data-astro-reload>{t.nav.home}</a>
|
<a href={`/${lang}/`}>{t.nav.home}</a>
|
||||||
<a href={`/${lang}/about/`}>{t.nav.about}</a>
|
<a href={`/${lang}/about/`}>{t.nav.about}</a>
|
||||||
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
|
||||||
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -143,7 +157,7 @@ const t = getTranslations(lang);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 700px) {
|
||||||
.site-nav-desktop {
|
.site-nav-desktop {
|
||||||
display: none;
|
display: 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>
|
||||||
|
|
@ -1,38 +1,123 @@
|
||||||
---
|
---
|
||||||
import "@/styles/global.css";
|
import "@/styles/global.css";
|
||||||
import Remark42Count from "@/components/remark42-counter.svelte";
|
import Remark42Counter from "@/components/Remark42Counter.astro";
|
||||||
|
import { getLangFromUrl } from "@/i18n";
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
const data = Astro.props;
|
const data = Astro.props;
|
||||||
|
const tags = data.tags;
|
||||||
---
|
---
|
||||||
|
|
||||||
<li class="post-card">
|
<li class="post-card">
|
||||||
<a href={data.url} class="post-link" data-astro-reload>
|
<div class="post-link">
|
||||||
<div class="post-text">
|
<div class="post-text">
|
||||||
<span class="post-title">{data.title}</span>
|
<a href={data.url} class="post-title-link">
|
||||||
<span class="post-description">{data.description}</span>
|
<h2 class="post-title" transition:name={`post-title-${data.postId}`}>
|
||||||
|
{data.title}
|
||||||
|
</h2>
|
||||||
|
</a>
|
||||||
|
<a href={data.url} class="post-description">
|
||||||
|
{data.description}
|
||||||
|
</a>
|
||||||
|
<div class="tags" transition:name={`post-tags-${data.postId}`}>
|
||||||
|
{
|
||||||
|
tags.map((tag: string) => (
|
||||||
|
<p class="tag">
|
||||||
|
<a href={`/${lang}/tags/${tag}`}>{tag}</a>
|
||||||
|
</p>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="post-meta-row">
|
<div class="post-meta-row">
|
||||||
<span class="post-date">{data.date}</span>
|
<span class="post-date">{data.date}</span>
|
||||||
<span class="post-stats">
|
<span class="post-stats">
|
||||||
<span class="post-stat">
|
<span class="post-stat">
|
||||||
💬 <Remark42Count url={data.postPath} client:idle />
|
💬 <Remark42Counter url={data.postPath} />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src={data.img} alt={data.title} class="post-image" loading="lazy" />
|
<a href={data.url} class="post-image-link">
|
||||||
|
<img
|
||||||
|
src={data.img}
|
||||||
|
alt={data.title}
|
||||||
|
class="post-image"
|
||||||
|
loading="lazy"
|
||||||
|
transition:name={`post-image-${data.postId}`}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
"Open Sans",
|
||||||
|
"Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.22em 0.65em;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
border: 1px dashed #8b6b4a;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag a {
|
||||||
|
color: #6f4e37;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .tag {
|
||||||
|
border-color: #d8c7a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .tag a {
|
||||||
|
color: #e6d8b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .tag {
|
||||||
|
border-color: #d8c7a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .tag a {
|
||||||
|
color: #e6d8b8;
|
||||||
|
}
|
||||||
|
|
||||||
.post-card {
|
.post-card {
|
||||||
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 {
|
||||||
|
|
@ -47,6 +132,7 @@ const data = Astro.props;
|
||||||
.post-text {
|
.post-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-width: 40rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -54,10 +140,15 @@ const data = Astro.props;
|
||||||
min-height: calc(1.6em * 4);
|
min-height: calc(1.6em * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-title-link {
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.post-title {
|
.post-title {
|
||||||
|
margin: 0;
|
||||||
font-family:
|
font-family:
|
||||||
system-ui,
|
system-ui,
|
||||||
sans-serif,
|
|
||||||
-apple-system,
|
-apple-system,
|
||||||
BlinkMacSystemFont,
|
BlinkMacSystemFont,
|
||||||
"Segoe UI",
|
"Segoe UI",
|
||||||
|
|
@ -66,9 +157,9 @@ const data = Astro.props;
|
||||||
Ubuntu,
|
Ubuntu,
|
||||||
Cantarell,
|
Cantarell,
|
||||||
"Open Sans",
|
"Open Sans",
|
||||||
"Helvetica Neue";
|
"Helvetica Neue",
|
||||||
color: inherit;
|
sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 500;
|
||||||
font-size: 1.34rem;
|
font-size: 1.34rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
|
@ -79,8 +170,10 @@ const data = Astro.props;
|
||||||
|
|
||||||
.post-description {
|
.post-description {
|
||||||
color: black;
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: italic;
|
font-size: 0.92rem;
|
||||||
|
/* font-style: italic; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-meta-row {
|
.post-meta-row {
|
||||||
|
|
@ -114,12 +207,12 @@ const data = Astro.props;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-image {
|
.post-image {
|
||||||
width: calc((1.6em * 4) * 16 / 9);
|
width: calc((1.6em * 4) * 16 / 10);
|
||||||
height: calc(1.6em * 4);
|
height: calc(1.6em * 4);
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 10;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
border-radius: 0.5rem;
|
border: 1.5px #94a0ab dashed;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
@ -132,13 +225,14 @@ const data = Astro.props;
|
||||||
color: #aab7c4;
|
color: #aab7c4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 600px) {
|
||||||
.post-link {
|
.post-link {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 120px;
|
grid-template-columns: 1fr 145px;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"title title"
|
"title title"
|
||||||
"desc image"
|
"desc image"
|
||||||
|
"tags image"
|
||||||
"meta image";
|
"meta image";
|
||||||
column-gap: 0.75rem;
|
column-gap: 0.75rem;
|
||||||
row-gap: 0.2rem;
|
row-gap: 0.2rem;
|
||||||
|
|
@ -150,8 +244,11 @@ const data = Astro.props;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-title {
|
.post-title-link {
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,9 +262,24 @@ const data = Astro.props;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-image {
|
.post-image {
|
||||||
|
/* border: none; */
|
||||||
grid-area: image;
|
grid-area: image;
|
||||||
width: 120px;
|
width: 100%;
|
||||||
height: calc(120px * 9 / 16);
|
height: calc(145px * 10 / 16);
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-link {
|
||||||
|
grid-area: image;
|
||||||
|
align-self: center;
|
||||||
|
display: block;
|
||||||
|
width: 145px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card {
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
padding-top: 0.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const filteredPosts = sortedPosts.filter((post: any) => {
|
||||||
return postLang === lang;
|
return postLang === lang;
|
||||||
});
|
});
|
||||||
|
|
||||||
const latestPosts = filteredPosts.slice(0, 5);
|
const latestPosts = filteredPosts.slice(0, 7);
|
||||||
---
|
---
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -37,6 +37,9 @@ const latestPosts = filteredPosts.slice(0, 5);
|
||||||
description={post.data.description}
|
description={post.data.description}
|
||||||
date={formattedDate}
|
date={formattedDate}
|
||||||
img={post.data.image.url}
|
img={post.data.image.url}
|
||||||
|
tags={post.data.tags}
|
||||||
|
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>
|
||||||
|
|
@ -51,9 +51,11 @@ const groupedPosts = filteredPosts.reduce((acc: any[], post: any) => {
|
||||||
<a
|
<a
|
||||||
href={`/${lang}/posts/${slug}/`}
|
href={`/${lang}/posts/${slug}/`}
|
||||||
class="timeline-card"
|
class="timeline-card"
|
||||||
data-astro-reload
|
|
||||||
>
|
>
|
||||||
<h2 class="post-title">
|
<h2
|
||||||
|
class="post-title"
|
||||||
|
transition:name={`post-title-${post.id}`}
|
||||||
|
>
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="post-description">
|
<p class="post-description">
|
||||||
|
|
@ -145,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;
|
||||||
|
|
|
||||||
66
src/components/Remark42Counter.astro
Normal file
66
src/components/Remark42Counter.astro
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
---
|
||||||
|
const { url } = Astro.props;
|
||||||
|
const host = import.meta.env.PUBLIC_REMARK42_HOST;
|
||||||
|
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||||
|
---
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="remark42__counter"
|
||||||
|
data-url={url}
|
||||||
|
data-host={host}
|
||||||
|
data-site-id={siteId}></span>
|
||||||
|
|
||||||
|
<script is:inline data-astro-rerun>
|
||||||
|
function getTheme() {
|
||||||
|
return document.documentElement.classList.contains("dark")
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountRemark42Counters() {
|
||||||
|
const counters = Array.from(
|
||||||
|
document.querySelectorAll(".remark42__counter[data-url]"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!counters.length) return;
|
||||||
|
|
||||||
|
const first = counters[0];
|
||||||
|
const host = first.dataset.host;
|
||||||
|
const siteId = first.dataset.siteId;
|
||||||
|
|
||||||
|
if (!host || !siteId) return;
|
||||||
|
|
||||||
|
window.remark_config = {
|
||||||
|
host,
|
||||||
|
site_id: siteId,
|
||||||
|
components: ["counter"],
|
||||||
|
show_rss_subscription: false,
|
||||||
|
theme: getTheme(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const el of counters) {
|
||||||
|
el.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("remark42-counter-loader")?.remove();
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.id = "remark42-counter-loader";
|
||||||
|
script.defer = true;
|
||||||
|
|
||||||
|
if ("noModule" in script) {
|
||||||
|
script.type = "module";
|
||||||
|
script.src = `${host}/web/counter.mjs?ts=${Date.now()}`;
|
||||||
|
} else {
|
||||||
|
script.async = true;
|
||||||
|
script.src = `${host}/web/counter.js?ts=${Date.now()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(window.__remark42CounterMountTimer);
|
||||||
|
window.__remark42CounterMountTimer = setTimeout(() => {
|
||||||
|
mountRemark42Counters();
|
||||||
|
}, 0);
|
||||||
|
</script>
|
||||||
108
src/components/Remark42Embed.astro
Normal file
108
src/components/Remark42Embed.astro
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
---
|
||||||
|
const { slug } = Astro.props;
|
||||||
|
const pagePath = `/posts/${slug}/`;
|
||||||
|
const host = import.meta.env.PUBLIC_REMARK42_HOST;
|
||||||
|
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id="remark42"></div>
|
||||||
|
<script define:vars={{ pagePath, host, siteId }} is:inline data-astro-rerun>
|
||||||
|
function getTheme() {
|
||||||
|
return document.documentElement.classList.contains("dark")
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRemarkConfig() {
|
||||||
|
window.remark_config = {
|
||||||
|
host,
|
||||||
|
site_id: siteId,
|
||||||
|
url: pagePath,
|
||||||
|
page_id: pagePath,
|
||||||
|
theme: getTheme(),
|
||||||
|
components: ["embed"],
|
||||||
|
show_rss_subscription: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureScript() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const existing = document.getElementById("remark42-loader");
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (window.REMARK42) {
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.addEventListener("load", () => resolve(true), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
existing.addEventListener("error", reject, { once: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.id = "remark42-loader";
|
||||||
|
script.defer = true;
|
||||||
|
|
||||||
|
if ("noModule" in script) {
|
||||||
|
script.type = "module";
|
||||||
|
script.src = `${host}/web/embed.mjs`;
|
||||||
|
} else {
|
||||||
|
script.async = true;
|
||||||
|
script.src = `${host}/web/embed.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
script.onload = () => resolve(true);
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountRemark42() {
|
||||||
|
const node = document.getElementById("remark42");
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
setRemarkConfig();
|
||||||
|
await ensureScript();
|
||||||
|
|
||||||
|
if (!window.REMARK42) return;
|
||||||
|
|
||||||
|
if (typeof window.REMARK42.destroy === "function") {
|
||||||
|
window.REMARK42.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
setupRemark42ThemeObserver();
|
||||||
|
</script>
|
||||||
91
src/components/Spinner.astro
Normal file
91
src/components/Spinner.astro
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<div id="page-loading-indicator" aria-hidden="true" transition:persist>
|
||||||
|
<span class="page-loading-spinner"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(() => {
|
||||||
|
let timer;
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
const el = document.getElementById("page-loading-indicator");
|
||||||
|
if (el) el.classList.add("is-active");
|
||||||
|
}, 120);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
const el = document.getElementById("page-loading-indicator");
|
||||||
|
if (el) el.classList.remove("is-active");
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("astro:before-preparation", show);
|
||||||
|
document.addEventListener("astro:page-load", hide);
|
||||||
|
|
||||||
|
hide();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
#page-loading-indicator {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transform: scale(1.3) translateY(-70px);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: transform 160ms linear;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-loading-indicator.is-active {
|
||||||
|
transform: scale(1) translateY(0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-loading-spinner {
|
||||||
|
width: 1.4rem;
|
||||||
|
height: 1.4rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1.5px solid #7da2ff;
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px #606060,
|
||||||
|
inset 0 0 0 1px #606060;
|
||||||
|
animation: page-loading-spin-rhythm 2.1s infinite;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes page-loading-spin-rhythm {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
animation-timing-function: cubic-bezier(0.25, 0.75, 0.35, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
52% {
|
||||||
|
transform: rotate(720deg); /* 前两圈,稍微慢一点 */
|
||||||
|
animation-timing-function: cubic-bezier(0.55, 0.08, 0.78, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(1080deg); /* 最后一圈更慢 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .page-loading-spinner,
|
||||||
|
html.dark .page-loading-spinner {
|
||||||
|
border-color: #d8c7a1;
|
||||||
|
border-top-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes page-loading-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -34,7 +34,7 @@ const switchHref = "/" + segments.join("/");
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a class="lang-switch" href={switchHref} data-astro-reload>
|
<a class="lang-switch" href={switchHref}>
|
||||||
{switchLabel}
|
{switchLabel}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
export let url;
|
|
||||||
|
|
||||||
const host = import.meta.env.PUBLIC_REMARK42_HOST;
|
|
||||||
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const remark_config = {
|
|
||||||
host,
|
|
||||||
site_id: siteId,
|
|
||||||
components: ["counter"],
|
|
||||||
show_rss_subscription: false,
|
|
||||||
theme: localStorage.getItem("color-theme") ?? "light",
|
|
||||||
};
|
|
||||||
|
|
||||||
window.remark_config = remark_config;
|
|
||||||
|
|
||||||
for (const name of remark_config.components || ["embed"]) {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
let ext = ".js";
|
|
||||||
|
|
||||||
if ("noModule" in script) {
|
|
||||||
script.type = "module";
|
|
||||||
ext = ".mjs";
|
|
||||||
} else {
|
|
||||||
script.async = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
script.defer = true;
|
|
||||||
script.src = `${remark_config.host}/web/${name}${ext}`;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span class="remark42__counter" data-url={url}></span>
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
export let slug;
|
|
||||||
|
|
||||||
let pagePath = `/posts/${slug}/`;
|
|
||||||
|
|
||||||
function getTheme() {
|
|
||||||
return document.documentElement.classList.contains("dark")
|
|
||||||
? "dark"
|
|
||||||
: "light";
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyRemark42Theme() {
|
|
||||||
if (window.REMARK42 && typeof window.REMARK42.changeTheme === "function") {
|
|
||||||
window.REMARK42.changeTheme(getTheme());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const scriptId = "remark42-script";
|
|
||||||
|
|
||||||
window.remark_config = {
|
|
||||||
host: import.meta.env.PUBLIC_REMARK42_HOST,
|
|
||||||
site_id: import.meta.env.PUBLIC_REMARK42_SITE_ID,
|
|
||||||
components: ["embed"],
|
|
||||||
show_rss_subscription: false,
|
|
||||||
theme: getTheme(),
|
|
||||||
url: pagePath,
|
|
||||||
page_id: pagePath,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!document.getElementById(scriptId)) {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.id = scriptId;
|
|
||||||
script.async = true;
|
|
||||||
script.innerHTML = `
|
|
||||||
!function(e,n){
|
|
||||||
for(var o=0;o<e.length;o++){
|
|
||||||
var r=n.createElement("script"),c=".js",d=n.head||n.body;
|
|
||||||
"noModule"in r?(r.type="module",c=".mjs"):r.async=!0;
|
|
||||||
r.defer=!0;
|
|
||||||
r.src=window.remark_config.host+"/web/"+e[o]+c;
|
|
||||||
d.appendChild(r);
|
|
||||||
}
|
|
||||||
}(window.remark_config.components||["embed"],document);
|
|
||||||
`;
|
|
||||||
document.body.appendChild(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyWhenReady = setInterval(() => {
|
|
||||||
if (applyRemark42Theme()) {
|
|
||||||
clearInterval(applyWhenReady);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
applyRemark42Theme();
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["class"],
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(applyWhenReady);
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="remark42"></div>
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
export let lang = "zh";
|
|
||||||
let loaded = false;
|
|
||||||
|
|
||||||
function rewriteLinks() {
|
|
||||||
const links = document.querySelectorAll(
|
|
||||||
".latest-comments .comment__title-link",
|
|
||||||
);
|
|
||||||
|
|
||||||
links.forEach((link) => {
|
|
||||||
const href = link.getAttribute("href");
|
|
||||||
if (!href) return;
|
|
||||||
|
|
||||||
if (href.startsWith("/posts/")) {
|
|
||||||
link.setAttribute("href", `/${lang}${href}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
window.remark_config = {
|
|
||||||
host: import.meta.env.PUBLIC_REMARK42_HOST,
|
|
||||||
site_id: import.meta.env.PUBLIC_REMARK42_SITE_ID,
|
|
||||||
components: ["last-comments"],
|
|
||||||
theme: localStorage.getItem("color-theme") ?? "light",
|
|
||||||
max_last_comments: 10,
|
|
||||||
no_footer: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const loaderId = "remark42-last-comments-loader";
|
|
||||||
if (!document.getElementById(loaderId)) {
|
|
||||||
const init = document.createElement("script");
|
|
||||||
init.id = loaderId;
|
|
||||||
init.innerHTML = `
|
|
||||||
!function(e,n){
|
|
||||||
for(var o=0;o<e.length;o++){
|
|
||||||
var r=n.createElement("script"),c=".js",d=n.head||n.body;
|
|
||||||
"noModule"in r?(r.type="module",c=".mjs"):r.async=!0;
|
|
||||||
r.defer=!0;
|
|
||||||
r.src=window.remark_config.host+"/web/"+e[o]+c;
|
|
||||||
d.appendChild(r);
|
|
||||||
}
|
|
||||||
}(window.remark_config.components||["last-comments"],document);
|
|
||||||
`;
|
|
||||||
document.body.appendChild(init);
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
const root = document.querySelector(".remark42__last-comments");
|
|
||||||
if (root && root.children.length > 0) {
|
|
||||||
rewriteLinks();
|
|
||||||
loaded = true;
|
|
||||||
clearInterval(timer);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="latest-comments">
|
|
||||||
<h2>最新评论</h2>
|
|
||||||
|
|
||||||
{#if !loaded}
|
|
||||||
<div class="comments-loading" aria-hidden="true">
|
|
||||||
<div class="loading-card"></div>
|
|
||||||
<div class="loading-card"></div>
|
|
||||||
<div class="loading-card"></div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="remark42__last-comments" data-max="10"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.loading-card {
|
|
||||||
height: 92px;
|
|
||||||
margin: 0 0 14px 0;
|
|
||||||
padding: 0.9rem 1rem;
|
|
||||||
border: 1px dashed #aeb8c2;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(160, 175, 190, 0.06) 25%,
|
|
||||||
rgba(160, 175, 190, 0.16) 50%,
|
|
||||||
rgba(160, 175, 190, 0.06) 75%
|
|
||||||
);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: shimmer 1.2s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
from {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments h2 {
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.remark42__last-comments) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(article.comment.list-comments__item) {
|
|
||||||
display: block;
|
|
||||||
margin: 0 0 14px 0;
|
|
||||||
padding: 0.9rem 1rem;
|
|
||||||
border: 1px dashed #aeb8c2;
|
|
||||||
background: rgba(160, 175, 190, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(article.comment.list-comments__item:last-child) {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark)
|
|
||||||
.latest-comments
|
|
||||||
:global(article.comment.list-comments__item) {
|
|
||||||
background: #2a3138;
|
|
||||||
border-color: #7f8c97;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__body) {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
|
||||||
grid-template-areas:
|
|
||||||
"info title"
|
|
||||||
"text title";
|
|
||||||
gap: 0.35rem 1rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__title) {
|
|
||||||
grid-area: title;
|
|
||||||
text-align: right;
|
|
||||||
min-width: 0;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__title-link) {
|
|
||||||
color: #6f8090;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.87rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.45;
|
|
||||||
white-space: normal;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__title-link:hover) {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__info) {
|
|
||||||
grid-area: info;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
color: inherit;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
word-break: break-word;
|
|
||||||
font-style: italic;
|
|
||||||
font-family:
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
"Segoe UI",
|
|
||||||
"PingFang SC",
|
|
||||||
"Hiragino Sans GB",
|
|
||||||
"Microsoft YaHei",
|
|
||||||
"Noto Sans CJK SC",
|
|
||||||
"Source Han Sans SC",
|
|
||||||
sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__info)::before {
|
|
||||||
content: "@";
|
|
||||||
margin-right: 0.08em;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__text) {
|
|
||||||
grid-area: text;
|
|
||||||
margin: 0;
|
|
||||||
color: #555;
|
|
||||||
line-height: 1.7;
|
|
||||||
font-size: 1.03rem;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-family:
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
"Segoe UI",
|
|
||||||
"PingFang SC",
|
|
||||||
"Hiragino Sans GB",
|
|
||||||
"Microsoft YaHei",
|
|
||||||
"Noto Sans CJK SC",
|
|
||||||
"Source Han Sans SC",
|
|
||||||
sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark)
|
|
||||||
.latest-comments
|
|
||||||
:global(article.comment.list-comments__item) {
|
|
||||||
border-color: #7f8c97;
|
|
||||||
background: rgba(180, 190, 200, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .latest-comments :global(.comment__title-link) {
|
|
||||||
color: #c8d2dc;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .latest-comments :global(.comment__text) {
|
|
||||||
color: #d3d7db;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.latest-comments :global(.comment__body) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-areas:
|
|
||||||
"info"
|
|
||||||
"title"
|
|
||||||
"text";
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__title) {
|
|
||||||
text-align: left;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-comments :global(.comment__title-link) {
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -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,15 @@
|
||||||
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 { 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 Spinner from "@/components/Spinner.astro";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pageTitle,
|
pageTitle,
|
||||||
|
|
@ -12,61 +20,36 @@ const {
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang={htmlLang}>
|
||||||
<head>
|
<head>
|
||||||
|
<ClientRouter />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<SEO title={pageTitle} description={description} image={image} />
|
<SEO title={pageTitle} description={description} image={image} />
|
||||||
<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>
|
||||||
<div class="floating-actions">
|
<FloatingActions />
|
||||||
<button
|
|
||||||
id="theme-toggle-fab"
|
|
||||||
aria-label="Toggle theme"
|
|
||||||
title="Toggle theme"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="theme-toggle-icon"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
class="sun"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
class="moon"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id="back-to-top" aria-label="Back to top" title="Back to top">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M18 15l-6-6-6 6"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<body>
|
<body>
|
||||||
<Header />
|
<Header />
|
||||||
<main class="page-content">
|
<Spinner />
|
||||||
|
<main
|
||||||
|
class="page-content"
|
||||||
|
transition:animate={fade({ duration: "0.05s" })}
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
@ -80,184 +63,4 @@ const {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-actions {
|
|
||||||
position: fixed;
|
|
||||||
right: 1.25rem;
|
|
||||||
bottom: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#theme-toggle-fab,
|
|
||||||
#back-to-top {
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
border: #a7a7a7 1.5px solid;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
color: #222;
|
|
||||||
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#theme-toggle-fab {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#theme-toggle-fab {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle-icon {
|
|
||||||
width: 1.2rem;
|
|
||||||
height: 1.2rem;
|
|
||||||
fill: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle-icon .sun,
|
|
||||||
.theme-toggle-icon .moon {
|
|
||||||
transform-origin: center;
|
|
||||||
transition:
|
|
||||||
opacity 0.2s ease,
|
|
||||||
transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle-icon .sun {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle-icon .moon {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.75) rotate(-20deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .theme-toggle-icon .sun {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.75) rotate(20deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .theme-toggle-icon .moon {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#theme-toggle-fab,
|
|
||||||
#back-to-top {
|
|
||||||
/* opacity: 0; */
|
|
||||||
visibility: hidden;
|
|
||||||
transform: translateY(150px);
|
|
||||||
transition:
|
|
||||||
opacity 0.2s ease,
|
|
||||||
transform 0.27s ease,
|
|
||||||
visibility 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#theme-toggle-fab.show,
|
|
||||||
#back-to-top.show {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#theme-toggle-fab.show:hover,
|
|
||||||
#back-to-top.show:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) #theme-toggle-fab,
|
|
||||||
:global(.dark) #back-to-top {
|
|
||||||
background: rgba(34, 34, 34, 0.92);
|
|
||||||
color: #f5f5f5;
|
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
|
|
||||||
border: #515151 2px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.floating-actions {
|
|
||||||
right: 1rem;
|
|
||||||
bottom: 1rem;
|
|
||||||
gap: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#theme-toggle-fab,
|
|
||||||
#back-to-top {
|
|
||||||
width: 2.75rem;
|
|
||||||
height: 2.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
const initFloatingActions = () => {
|
|
||||||
const backToTop = document.getElementById("back-to-top");
|
|
||||||
const themeToggle = document.getElementById("theme-toggle-fab");
|
|
||||||
|
|
||||||
if (backToTop && backToTop.dataset.bound !== "true") {
|
|
||||||
const toggleBackToTop = () => {
|
|
||||||
if (window.scrollY > 300) {
|
|
||||||
backToTop.classList.add("show");
|
|
||||||
} else {
|
|
||||||
backToTop.classList.remove("show");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
backToTop.addEventListener("click", () => {
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("scroll", toggleBackToTop, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
toggleBackToTop();
|
|
||||||
backToTop.dataset.bound = "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (themeToggle && themeToggle.dataset.bound !== "true") {
|
|
||||||
const togglethemeToggle = () => {
|
|
||||||
if (window.scrollY > 300) {
|
|
||||||
themeToggle.classList.add("show");
|
|
||||||
} else {
|
|
||||||
themeToggle.classList.remove("show");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const root = document.documentElement;
|
|
||||||
|
|
||||||
const applyTheme = (theme: string) => {
|
|
||||||
if (theme === "dark") {
|
|
||||||
root.classList.add("dark");
|
|
||||||
localStorage.setItem("color-theme", "dark");
|
|
||||||
} else {
|
|
||||||
root.classList.remove("dark");
|
|
||||||
localStorage.setItem("color-theme", "light");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
themeToggle.addEventListener("click", () => {
|
|
||||||
const isDark = root.classList.contains("dark");
|
|
||||||
applyTheme(isDark ? "light" : "dark");
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("scroll", togglethemeToggle, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
togglethemeToggle();
|
|
||||||
themeToggle.dataset.bound = "true";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initFloatingActions();
|
|
||||||
document.addEventListener("astro:page-load", initFloatingActions);
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from "./BaseLayout.astro";
|
import BaseLayout from "./BaseLayout.astro";
|
||||||
import Remark42Embed from "@/components/remark42-embed.svelte";
|
import Remark42Embed from "@/components/Remark42Embed.astro";
|
||||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
import 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">{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} client:load />
|
{t.post.commentsDesc.map((line: string) => <p set:html={line} />)}
|
||||||
|
<Remark42Embed slug={slug} />
|
||||||
</article>
|
</article>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
|
@ -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); */
|
||||||
|
|
@ -187,7 +204,7 @@ const t = getTranslations(lang);
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||||
import PostList from "@/components/Posts/PostList.astro";
|
import PostList from "@/components/Posts/PostList.astro";
|
||||||
import Remark42LatestComments from "@/components/remark42-latest-comments.svelte";
|
import Remark42LatestComments from "@/components/remark42-latest-comments.svelte";
|
||||||
|
import LatestComments from "@/components/LatestComments.astro";
|
||||||
import { getLangFromUrl, getTranslations } from "@/i18n";
|
import { getLangFromUrl, getTranslations } from "@/i18n";
|
||||||
import "@/styles/global.css";
|
import "@/styles/global.css";
|
||||||
|
|
||||||
|
|
@ -13,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"
|
||||||
|
|
@ -32,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"
|
||||||
|
|
@ -46,11 +48,14 @@ 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>
|
||||||
|
|
||||||
<Remark42LatestComments lang={lang} client:load />
|
<!-- <Remark42LatestComments lang={lang} client:load /> -->
|
||||||
|
<LatestComments />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -64,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>
|
||||||
|
|
@ -53,6 +53,8 @@ const t = getTranslations(lang);
|
||||||
title={post.data.title}
|
title={post.data.title}
|
||||||
date={formattedDate}
|
date={formattedDate}
|
||||||
img={post.data.image.url}
|
img={post.data.image.url}
|
||||||
|
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>
|
||||||
))
|
))
|
||||||
|
|
@ -39,6 +39,19 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
"Open Sans",
|
||||||
|
"Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.22em 0.65em;
|
padding: 0.22em 0.65em;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
@ -51,6 +64,7 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
|
||||||
.tag a {
|
.tag a {
|
||||||
color: #6f4e37;
|
color: #6f4e37;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag a:hover {
|
.tag a:hover {
|
||||||
|
|
|
||||||
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;
|
||||||
|
|
@ -49,10 +28,32 @@ td code {
|
||||||
background: rgba(127, 127, 127, 0.12);
|
background: rgba(127, 127, 127, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
article svg.flowchart,
|
||||||
|
article svg[class*="flowchart"],
|
||||||
|
article svg[id^="mermaid-"] {
|
||||||
|
display: block;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #828282 transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -73,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 {
|
||||||
|
|
@ -88,7 +90,7 @@ html.dark body::after {
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
body::after {
|
body::after {
|
||||||
opacity: 0.1;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,10 +102,6 @@ html.dark body::after {
|
||||||
body {
|
body {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body::after {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -119,7 +117,7 @@ h1 {
|
||||||
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #416bd6;
|
color: #3D74B6;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
@ -164,3 +162,111 @@ img {
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 避免 Markdown 内容撑宽正文 */
|
||||||
|
.post-article,
|
||||||
|
.post-content {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 块级公式过长时独立横向滚动 */
|
||||||
|
.post-content .katex-display {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content .katex-display > .katex {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: max-content;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Markdown details 下拉栏
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.post-content details {
|
||||||
|
min-width: 0;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content details summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content details summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content details summary::before {
|
||||||
|
content: "›";
|
||||||
|
display: inline-block;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content details[open] summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content details > :not(summary) {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content details > ol,
|
||||||
|
.post-content details > ul {
|
||||||
|
padding: 0.6rem 1rem 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
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