Compare commits
58 Commits
b3bc2967a8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
bb347dab3b
|
|||
|
5e5f13a949
|
|||
|
cd689aba98
|
|||
|
2de3841f7d
|
|||
|
ee3585586d
|
|||
|
6c525bcf98
|
|||
|
7a3059f486
|
|||
|
1b8bd19370
|
|||
|
ca1cf10aa8
|
|||
|
880afe34ab
|
|||
|
c3973e779a
|
|||
|
d605131bda
|
|||
|
2a71d714e8
|
|||
|
a47a537a64
|
|||
|
e36492a692
|
|||
|
9c019351a3
|
|||
|
5e53a213e6
|
|||
|
29b3c89c40
|
|||
|
43a85ac056
|
|||
|
3d0eeb7996
|
|||
|
e466006f81
|
|||
|
12e66a644e
|
|||
|
404c315657
|
|||
|
ac984b320f
|
|||
|
c5c25b6617
|
|||
|
d8fb7fd63f
|
|||
|
428be3170e
|
|||
|
b833065baf
|
|||
|
00218a17ba
|
|||
|
fc73828155
|
|||
|
d06e525385
|
|||
|
b1e1f05886
|
|||
|
14c30a2a30
|
|||
|
8fa4ed1b0d
|
|||
|
4c60081298
|
|||
|
aa33703b8b
|
|||
|
b3e3f48bdb
|
|||
|
3afbc518a5
|
|||
|
7b732de549
|
|||
|
b32c5bc6e9
|
|||
|
c6bc10d6cd
|
|||
|
605d53d590
|
|||
|
4e56bc0d38
|
|||
|
706e98e39d
|
|||
|
a69d3f81ce
|
|||
|
93b2853d30
|
|||
|
17a6506bd6
|
|||
|
e79d768a6d
|
|||
|
da63acccec
|
|||
|
a87c6faa1e
|
|||
|
36a6ff369d
|
|||
|
b6d60552e6
|
|||
|
4c5abe8b01
|
|||
|
bc5247ddc8
|
|||
|
621d744efc
|
|||
|
7501bb9f57
|
|||
|
eb0be5ebca
|
|||
|
b4298602bf
|
@ -4,7 +4,7 @@ root = true
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,ts,astro,json,yml}]
|
||||
[*.{js,ts,astro,json,yml,vue,svelte}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
@ -1,15 +1,48 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config'
|
||||
import { defineConfig, envField } from 'astro/config'
|
||||
|
||||
import node from '@astrojs/node'
|
||||
|
||||
import svelte from '@astrojs/svelte'
|
||||
import vue from '@astrojs/vue'
|
||||
import react from '@astrojs/react'
|
||||
import icon from 'astro-icon'
|
||||
|
||||
const ICON2_URL =
|
||||
'https://cdn.passthem.top/sharex/2026/03/%E6%93%A6%E6%B1%97%E5%90%8E_%E6%96%B9%E5%BD%A2.png.upscale.png'
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node({
|
||||
mode: 'standalone',
|
||||
}),
|
||||
|
||||
integrations: [svelte()],
|
||||
integrations: [svelte(), vue(), react(), icon()],
|
||||
site: 'https://passthem.top',
|
||||
redirects: {
|
||||
'/assets/icon-qq-BThBBmjV.jpg': ICON2_URL,
|
||||
},
|
||||
vite: {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['node:crypto', 'buffer', 'keyv'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 环境变量管理
|
||||
env: {
|
||||
schema: {
|
||||
LEGACY_BACKEND_URL_SSR: envField.string({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
default: 'https://legacy.passthem.top/api',
|
||||
}),
|
||||
REDIS_URL: envField.string({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
default: '',
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
43
eslint.config.mjs
Normal file
43
eslint.config.mjs
Normal file
@ -0,0 +1,43 @@
|
||||
import eslintPluginAstro from 'eslint-plugin-astro'
|
||||
import tseslint from '@typescript-eslint/eslint-plugin'
|
||||
import tsparser from '@typescript-eslint/parser'
|
||||
import svelteParser from 'svelte-eslint-parser'
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**', '.astro/**'],
|
||||
plugins: {
|
||||
astro: eslintPluginAstro,
|
||||
'@typescript-eslint': tseslint,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...eslintPluginAstro.configs.recommended.rules,
|
||||
...tseslint.configs.recommended.rules,
|
||||
'no-console': 'warn',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
languageOptions: {
|
||||
parser: svelteParser,
|
||||
parserOptions: {
|
||||
parser: tsparser,
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
3105
package-lock.json
generated
3105
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@ -9,21 +9,61 @@
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
"astro": "astro",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,jsx,tsx,svelte,vue}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{astro,css,md,mdx,json}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^10.0.4",
|
||||
"@astrojs/react": "^5.0.2",
|
||||
"@astrojs/rss": "^4.0.18",
|
||||
"@astrojs/svelte": "^8.0.4",
|
||||
"@astrojs/vue": "^6.0.1",
|
||||
"@iconify-json/material-symbols": "^1.2.65",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@keyv/redis": "^5.1.6",
|
||||
"@markdoc/markdoc": "^0.5.7",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@unocss/reset": "^66.6.7",
|
||||
"artalk": "^2.9.1",
|
||||
"astro": "^6.1.0",
|
||||
"astro-icon": "^1.1.5",
|
||||
"axios": "^1.13.6",
|
||||
"katex": "^0.16.43",
|
||||
"keyv": "^5.6.0",
|
||||
"markdown-it": "^14.1.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"shiki": "^4.0.2",
|
||||
"svelte": "^5.55.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"vue": "^3.5.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
||||
"@typescript-eslint/parser": "^8.57.2",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-plugin-astro": "^1.6.0",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.4.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-astro": "^0.14.1"
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte-eslint-parser": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ const config = {
|
||||
tabWidth: 2,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
plugins: ['prettier-plugin-astro'],
|
||||
plugins: ['prettier-plugin-astro', 'prettier-plugin-svelte'],
|
||||
overrides: [
|
||||
{
|
||||
files: '*.astro',
|
||||
@ -17,6 +17,12 @@ const config = {
|
||||
parser: 'astro',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: '*.css',
|
||||
options: {
|
||||
tabWidth: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
47
scripts/generate_contact.py
Normal file
47
scripts/generate_contact.py
Normal file
@ -0,0 +1,47 @@
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
raw_contact: list[dict[str, str]] = [
|
||||
{
|
||||
"platform": "Github",
|
||||
"href": "https://github.com/passthem-desu",
|
||||
"name": "passthem-desu",
|
||||
},
|
||||
{
|
||||
"platform": "Wakatime",
|
||||
"href": "https://wakatime.com/@passthem",
|
||||
"name": "@passthem",
|
||||
},
|
||||
{
|
||||
"platform": "Youtube",
|
||||
"href": "https://www.youtube.com/@Passthem183",
|
||||
"name": "@Passthem183",
|
||||
},
|
||||
{
|
||||
"platform": "OtoSite",
|
||||
"href": "https://otomad.site/@passthem",
|
||||
"name": "@passthem",
|
||||
},
|
||||
{
|
||||
"platform": "Email",
|
||||
"href": "mailto:passthem183@gmail.com",
|
||||
"name": "passthem183@gmail.com",
|
||||
},
|
||||
{
|
||||
"platform": "Bilibili",
|
||||
"href": "https://space.bilibili.com/92852604",
|
||||
"name": "passthem",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def hack(raw: str) -> str:
|
||||
return base64.b64encode(raw[::-1].encode()).decode()
|
||||
|
||||
|
||||
hacked_contact = [{k: hack(v) for k, v in c.items()} for c in raw_contact]
|
||||
(Path(__file__).parent / "../src/lib/data").mkdir(exist_ok=True)
|
||||
_ = (Path(__file__).parent / "../src/lib/data/contact.json").write_text(
|
||||
json.dumps(hacked_contact)
|
||||
)
|
||||
BIN
src/assets/blogs-header.webp
Normal file
BIN
src/assets/blogs-header.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
157
src/assets/fonts.css
Normal file
157
src/assets/fonts.css
Normal file
@ -0,0 +1,157 @@
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Bold.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-BoldItalic.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraBold.woff2')
|
||||
format('woff2');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraBoldItalic.woff2')
|
||||
format('woff2');
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraLight.woff2')
|
||||
format('woff2');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ExtraLightItalic.woff2')
|
||||
format('woff2');
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Italic.woff2')
|
||||
format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Light.woff2')
|
||||
format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-LightItalic.woff2')
|
||||
format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Medium.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-MediumItalic.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Regular.woff2')
|
||||
format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-SemiBold.woff2')
|
||||
format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-SemiBoldItalic.woff2')
|
||||
format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-Thin.woff2')
|
||||
format('woff2');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Maple Mono';
|
||||
src: url('https://cdn.passthem.top/fonts/MapleMonoNormal-NF-CN-ThinItalic.woff2')
|
||||
format('woff2');
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'ZLabsRoundPix 16px M CN';
|
||||
src: url('https://cdn.passthem.top/fonts/ZLabsRoundPix_16px_M_CN.ttf.woff2')
|
||||
format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-mono: 'Maple Mono NF CN', 'Maple Mono', monospace, var(--font-sans);
|
||||
--font-sans:
|
||||
'HarmonyOS Sans SC', 'Source Han Sans SC', 'Noto Sans CJK SC',
|
||||
sans-serif;
|
||||
}
|
||||
BIN
src/assets/mainpage_avatars/ice.png
Executable file
BIN
src/assets/mainpage_avatars/ice.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/mainpage_avatars/mnk.jpg
Executable file
BIN
src/assets/mainpage_avatars/mnk.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
src/assets/mainpage_avatars/passthem.png
Normal file
BIN
src/assets/mainpage_avatars/passthem.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 675 KiB |
94
src/assets/prose.css
Normal file
94
src/assets/prose.css
Normal file
@ -0,0 +1,94 @@
|
||||
.prose {
|
||||
& p,
|
||||
& h1,
|
||||
& h2,
|
||||
& h3,
|
||||
& h4,
|
||||
& h5,
|
||||
& h6 {
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin-block: 0.6em;
|
||||
}
|
||||
|
||||
& h1,
|
||||
& h2,
|
||||
& h3,
|
||||
& h4,
|
||||
& h5,
|
||||
& h6 {
|
||||
font-weight: 700;
|
||||
margin-block: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
& h2 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
& h4 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
& h5 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
& h6 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
& blockquote {
|
||||
background-color: var(--color-bg-1);
|
||||
color: var(--color-fg-1);
|
||||
|
||||
border-radius: 0.5rem;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1.25rem;
|
||||
margin-block: 1.25rem;
|
||||
|
||||
border-inline-start: solid 0.25rem;
|
||||
}
|
||||
|
||||
& pre.shiki {
|
||||
border-radius: 0.5rem;
|
||||
padding-block: 1rem;
|
||||
padding-inline: 1.25rem;
|
||||
margin-block: 1.25rem;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
& a {
|
||||
color: var(--color-blue);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
& img {
|
||||
border-radius: 0.5rem;
|
||||
margin-block: 1.25rem;
|
||||
}
|
||||
|
||||
& p > code {
|
||||
background-color: var(--color-bg-1);
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
& pre,
|
||||
& code {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
@ -5,20 +5,54 @@
|
||||
|
||||
/* == 主题色 == */
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
color-scheme: light dark;
|
||||
|
||||
--color-bg-0: light-dark(oklch(95% 0 0), oklch(30% 0.02 270));
|
||||
--color-fg-0: light-dark(oklch(25% 0.02 270), oklch(90% 0.02 270));
|
||||
--color-bg-n: light-dark(oklch(100% 0 0), oklch(30% 0.025 270));
|
||||
--color-bg-0: light-dark(oklch(95% 0 0), oklch(25% 0.02 270));
|
||||
--color-bg-1: light-dark(oklch(90% 0 0), oklch(20% 0.02 270));
|
||||
--color-bg-2: light-dark(oklch(85% 0 0), oklch(15% 0.02 270));
|
||||
|
||||
--color-link: light-dark(oklch(40% 0.2 270), oklch(80% 0.2 270));
|
||||
--color-fg-0: light-dark(oklch(25% 0.02 270), oklch(90% 0.02 270));
|
||||
--color-fg-1: light-dark(oklch(35% 0.02 270), oklch(80% 0.02 270));
|
||||
|
||||
--color-blue: light-dark(oklch(40% 0.2 270), oklch(80% 0.2 270));
|
||||
--color-green: light-dark(oklch(50% 0.1 150), oklch(80% 0.15 150));
|
||||
--color-link: var(--color-blue);
|
||||
|
||||
--size-nav: 3rem;
|
||||
|
||||
@media (width < 768px) {
|
||||
--size-nav: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* == 页面设置 == */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background-color: var(--color-bg-0);
|
||||
color: var(--color-fg-0);
|
||||
background-color: var(--color-bg-0);
|
||||
color: var(--color-fg-0);
|
||||
|
||||
font-family: var(--font-sans);
|
||||
|
||||
/* writing-mode: vertical-rl; */
|
||||
}
|
||||
|
||||
/* == 配置默认字体 == */
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* == 支持 Shiki 的高亮 == */
|
||||
pre.shiki,
|
||||
pre.shiki span {
|
||||
color: light-dark(var(--shiki-light), var(--shiki-dark));
|
||||
background-color: light-dark(var(--shiki-light-bg), var(--shiki-dark-bg));
|
||||
}
|
||||
|
||||
30
src/components/Artalk.svelte
Normal file
30
src/components/Artalk.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
// https://artalk.js.org/zh/develop/import-framework
|
||||
|
||||
import Artalk from 'artalk'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
|
||||
import 'artalk/Artalk.css'
|
||||
|
||||
let el: HTMLElement
|
||||
let artalk: Artalk
|
||||
|
||||
onMount(() => {
|
||||
artalk = Artalk.init({
|
||||
el: el,
|
||||
pageKey: location.pathname,
|
||||
pageTitle: document.title,
|
||||
server: 'https://artalk.sv.passthem.top',
|
||||
site: '小帕的小窝',
|
||||
darkMode: 'auto',
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (artalk) {
|
||||
artalk.destroy()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="comment" bind:this={el}></div>
|
||||
165
src/components/BlogCard.astro
Normal file
165
src/components/BlogCard.astro
Normal file
@ -0,0 +1,165 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
interface Props {
|
||||
link: string
|
||||
title: string
|
||||
featureImage?: string
|
||||
createdAt?: Date
|
||||
updatedAt?: Date
|
||||
author?: {
|
||||
name: string
|
||||
avatar?: string
|
||||
}
|
||||
}
|
||||
|
||||
const { link, title, featureImage, author, createdAt, updatedAt } = Astro.props
|
||||
|
||||
const formatDate = (date: Date) =>
|
||||
`${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日`
|
||||
---
|
||||
|
||||
<a class="card" href={link}>
|
||||
{
|
||||
featureImage && (
|
||||
<Image
|
||||
class="image"
|
||||
src={featureImage}
|
||||
width={640}
|
||||
height={360}
|
||||
densities={[1.5, 2]}
|
||||
alt="博文的头图"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="info">
|
||||
<h1 class="title">{title}</h1>
|
||||
{
|
||||
author && (
|
||||
<div class="author">
|
||||
<Image
|
||||
class="image"
|
||||
src={author.avatar ?? ''}
|
||||
alt={`用户 ${author.name} 的头像`}
|
||||
width={24}
|
||||
height={24}
|
||||
densities={[1.5, 2]}
|
||||
/>
|
||||
<span class="content">{author.name}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
createdAt && (
|
||||
<div class="iconinfo">
|
||||
<Icon class="icon" name="material-symbols:calendar-add-on-rounded" />
|
||||
<span class="content">{formatDate(createdAt)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
updatedAt &&
|
||||
(!createdAt || formatDate(createdAt) != formatDate(updatedAt)) && (
|
||||
<div class="iconinfo">
|
||||
<Icon class="icon" name="material-symbols:calendar-check-rounded" />
|
||||
<span class="content">{formatDate(updatedAt)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
--icon-size: 24px;
|
||||
|
||||
background-color: var(--color-bg-n);
|
||||
|
||||
margin: 1rem;
|
||||
padding-inline: 1rem;
|
||||
padding-block: 1.25rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 0.75rem; */
|
||||
border-radius: 2rem;
|
||||
transition: all cubic-bezier(0.4, 1, 0.4, 1) 0.3s;
|
||||
|
||||
@media (width >= 768px) {
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
outline: solid 0px transparent;
|
||||
&:hover {
|
||||
outline-offset: 4px;
|
||||
outline: solid 4px var(--color-blue);
|
||||
}
|
||||
|
||||
&:has(> .image) {
|
||||
padding-block-start: 1rem;
|
||||
@media (width >= 768px) {
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
& > .image {
|
||||
border-radius: 1rem;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
margin-block-end: 0.75rem;
|
||||
|
||||
@media (width >= 768px) {
|
||||
width: 300px;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .info {
|
||||
& > h1 {
|
||||
margin-inline: 0.5rem;
|
||||
font-size: larger;
|
||||
font-weight: 600;
|
||||
margin-block-start: 0.25rem;
|
||||
margin-block-end: 0.75rem;
|
||||
|
||||
@media (width < 768px) {
|
||||
margin-inline: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
& > .author,
|
||||
& > .iconinfo {
|
||||
margin-inline: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-block: 0.1rem;
|
||||
|
||||
& > .image {
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
width: var(--icon-size);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
padding: 2px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
& > .content {
|
||||
opacity: 0.5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
& > .author {
|
||||
margin-block: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -52,4 +52,46 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={click}>没用的按钮 | {clicks + clicksPending}</button>
|
||||
<button class="useless-button" onclick={click}>
|
||||
<span class="name">到此一游</span>
|
||||
<span class="number">{clicks + clicksPending}</span>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.useless-button {
|
||||
display: block;
|
||||
background-color: var(--color-bg-n);
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
border-radius: 1rem;
|
||||
|
||||
overflow: hidden;
|
||||
transition: transform ease 0.1s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
& > .name {
|
||||
display: inline-block;
|
||||
font-weight: 300;
|
||||
padding-inline-start: 1rem;
|
||||
padding-inline-end: 0.5rem;
|
||||
padding-block: 0.5rem;
|
||||
}
|
||||
|
||||
& > .number {
|
||||
display: inline-block;
|
||||
padding-inline-start: 0.75rem;
|
||||
padding-inline-end: 0.75rem;
|
||||
padding-block: 0.5rem;
|
||||
background-color: var(--color-green);
|
||||
color: var(--color-bg-n);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
177
src/components/MainpageComments.astro
Normal file
177
src/components/MainpageComments.astro
Normal file
@ -0,0 +1,177 @@
|
||||
---
|
||||
// 不想写得屎长屎长,所以就用单独的文件写了
|
||||
|
||||
import { Image } from 'astro:assets'
|
||||
import IceAvatar from '../assets/mainpage_avatars/ice.png'
|
||||
import MnkAvatar from '../assets/mainpage_avatars/mnk.jpg'
|
||||
---
|
||||
|
||||
<div class="comments">
|
||||
{
|
||||
[
|
||||
[
|
||||
IceAvatar,
|
||||
'超几何冰精',
|
||||
'⑨',
|
||||
<>
|
||||
<p>
|
||||
<s>@☀️ 可以当靶场玩吗</s>
|
||||
</p>
|
||||
<p>我喜欢passthem</p>
|
||||
</>,
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/pigeon-16qh82EW.jpg',
|
||||
'pigeon某人',
|
||||
'可爱的鸽子,Bilibili 著名 YTPMVer',
|
||||
<>
|
||||
<Image
|
||||
src="https://legacy.passthem.top/assets/idk-DjwAIbw0.jpg"
|
||||
width={100}
|
||||
height={95}
|
||||
alt="用户「pigeon某人」的个人形象的表情包"
|
||||
/>
|
||||
<p>这个可以贴吗</p>
|
||||
</>,
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/kio-DikBD7t4.jpg',
|
||||
'asynkio',
|
||||
'西多kitaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
<p>asynkio 祝 jazzwhom.top 開張大吉!</p>,
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/leukos-Ci6_szKv.jpg',
|
||||
'leukos',
|
||||
'冰镇西米露,在幻想乡被称作露米娅',
|
||||
<p>帕,移动</p>,
|
||||
],
|
||||
[
|
||||
MnkAvatar,
|
||||
'モノクロ子',
|
||||
'全て変えるのなら黒にする。',
|
||||
<>
|
||||
<>
|
||||
<p>pt的音mad做的特别厉害!当时给我看哭了……</p>
|
||||
<p>谢谢你,pt。我希望有一天我也能做出那样的音mad。</p>
|
||||
</>
|
||||
</>,
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/graffiti-DHeEktlb.webp',
|
||||
'Graffiti_',
|
||||
'泡壳老师,虽然是僵尸,却比人类都要暖',
|
||||
<p>人与人之间是情感连接着的</p>,
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/miskplar-CJpcFQZ_.webp',
|
||||
'miskplar',
|
||||
'校友',
|
||||
<p>后悔的时候,不要后悔</p>,
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/sge-Da3d1ENG.webp',
|
||||
'一根烤肠肠',
|
||||
'小孩小孩你别馋,过了腊八就是年',
|
||||
<p>产出作品是你持续站在舞台上的唯一方式</p>,
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/aishi-OOmvQDjV.jpg',
|
||||
'蔼石',
|
||||
'校友,东方社团「有顶天变电站」运营中',
|
||||
<Image
|
||||
src="https://legacy.passthem.top/assets/mgmg-qXzHVGjP.jpg"
|
||||
width={140}
|
||||
height={140}
|
||||
alt="表情包,东方project角色露娜萨·普莉兹姆利巴吃年糕"
|
||||
/>,
|
||||
],
|
||||
].map((elements) => (
|
||||
<div class="comment">
|
||||
<div class="comment-head">
|
||||
<Image
|
||||
class="avatar"
|
||||
src={elements[0] as string}
|
||||
width={48}
|
||||
height={48}
|
||||
densities={[1.5, 2]}
|
||||
alt={`${elements[1]} 的头像`}
|
||||
/>
|
||||
<div class="userinfo">
|
||||
<div class="username">{elements[1]}</div>
|
||||
<div class="comment">{elements[2]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-content">{elements[3]}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.comments {
|
||||
overflow-y: scroll;
|
||||
padding-inline: 3rem;
|
||||
margin-inline: 1rem;
|
||||
height: 100%;
|
||||
padding-block: 2rem;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
& > .comment {
|
||||
padding-block-start: 1.5rem;
|
||||
padding-block-end: 1.5rem;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 2px rgb(from var(--color-fg-0) r g b / 0.05);
|
||||
}
|
||||
|
||||
& > .comment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: 1.25rem;
|
||||
padding-block-end: 1.5rem;
|
||||
gap: 1rem;
|
||||
|
||||
& > .avatar {
|
||||
flex: none;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 9999px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
& > .userinfo {
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
& > .username {
|
||||
overflow-x: hidden;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
& > .comment {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .comment-content {
|
||||
padding-inline: 1.5rem;
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
s {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
src/components/MainpageContactMe.svelte
Normal file
98
src/components/MainpageContactMe.svelte
Normal file
@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import _data from '../lib/data/contact.json'
|
||||
|
||||
/**
|
||||
* 为什么要一个独立的 Contact 组件呢...?这就不得不提到,现在可是有网络爬虫的
|
||||
* 时代!我们得给我们的个人信息做反爬呀!
|
||||
*
|
||||
* 另见:https://spencermortensen.com/articles/email-obfuscation/
|
||||
*/
|
||||
|
||||
let data = $state<{ platform: string; name: string; href: string }[]>([])
|
||||
|
||||
function unhack(raw: string) {
|
||||
return atob(raw).split('').reverse().join('')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
data = _data
|
||||
})
|
||||
</script>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
{#each data as item}
|
||||
<tr>
|
||||
<td>{unhack(item.platform)}</td>
|
||||
<td><a href={unhack(item.href)}>{unhack(item.name)}</a></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
font-family: var(--font-mono);
|
||||
|
||||
& tr {
|
||||
text-align: left;
|
||||
display: grid;
|
||||
grid-template-columns: 6em 16em;
|
||||
gap: 1em;
|
||||
line-height: 2em;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
grid-template-columns: 18em;
|
||||
gap: 0;
|
||||
line-height: 1.8em;
|
||||
margin-block: 1em;
|
||||
}
|
||||
|
||||
& td:nth-child(1) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
|
||||
& td:nth-child(2) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '[';
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ']';
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:has(a:focus) {
|
||||
background-color: var(--color-fg-0);
|
||||
color: var(--color-bg-0);
|
||||
}
|
||||
|
||||
& > a {
|
||||
text-align: center;
|
||||
margin-inline: 1em;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
262
src/components/MainpageTypewriter.svelte
Normal file
262
src/components/MainpageTypewriter.svelte
Normal file
@ -0,0 +1,262 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
// 基础定义部分
|
||||
type WriterStage = { text: string; editing: string[] } | string
|
||||
type WriterWord = { stages: WriterStage[] }
|
||||
|
||||
// 辅助函数
|
||||
const typeCharacters = (base: string, chars: string) => [
|
||||
base,
|
||||
...chars.split('').map((_, i) => base + chars.slice(0, i + 1)),
|
||||
]
|
||||
|
||||
const words: WriterWord[] = [
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['x', 'xi', 'xih', "xi'hr"] },
|
||||
{ text: '喜欢', editing: ['y', 'yb', 'ybl'] },
|
||||
...typeCharacters('喜欢音', 'MAD'),
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['x', 'xi', 'xih', "xi'hr"] },
|
||||
...typeCharacters('喜欢', 'YTPMV'),
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['x', 'xi', 'xih', "xi'hr"] },
|
||||
{ text: '喜欢', editing: ['u', 'ui', 'uij', "ui'jt"] },
|
||||
{ text: '喜欢视觉', editing: ['y', 'yi', 'yiu', "yi'uu"] },
|
||||
'喜欢视觉艺术',
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['x', 'xt', 'xtx', "xt'xi"] },
|
||||
{ text: '学习', editing: ['q', 'qm', 'qmd', "qm'dr"] },
|
||||
{ text: '学习前端', editing: ['k', 'kd', 'kdf', "kd'fa"] },
|
||||
'学习前端开发',
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['x', 'xt', 'xtx', "xt'xi"] },
|
||||
{ text: '学习', editing: ['h', 'hz', "hz'd", "hz'dr"] },
|
||||
{ text: '学习后端', editing: ['k', 'kd', 'kdf', "kd'fa"] },
|
||||
'学习后端开发',
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['x', 'xt', 'xtx', "xt'xi"] },
|
||||
{
|
||||
text: '学习',
|
||||
editing: ['q', 'qm', 'qmr', "qm'ru", "qm'ru'u", "qm'ru'ui"],
|
||||
},
|
||||
'学习嵌入式',
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['x', 'xt', 'xtx', "xt'xi"] },
|
||||
...typeCharacters('学习', 'DevOps'),
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['z', 'zr', "zr'y", "zr'yj"] },
|
||||
{ text: '钻研', editing: ['h', 'hy', 'hyd'] },
|
||||
{ text: '钻研混', editing: ['m', 'mu'] },
|
||||
'钻研混母',
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['z', 'zr', "zr'y", "zr'yj"] },
|
||||
{ text: '钻研', editing: ['u', 'uz', "uz'g", "uz'gs"] },
|
||||
{ text: '钻研手工', editing: ['k', 'kd', 'kdf', "kd'fa"] },
|
||||
'钻研手工开发',
|
||||
],
|
||||
},
|
||||
{
|
||||
stages: [
|
||||
{ text: '', editing: ['b', 'bw', "bw'v", "bw'vj"] },
|
||||
{ text: '备战', editing: ['k', 'kc', "kc'y", "kc'yj"] },
|
||||
'备战考研',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// 状态机定义
|
||||
type StatusMachine = {
|
||||
wipWord: number
|
||||
wipStage: number
|
||||
wipEditing: number
|
||||
countdown: number
|
||||
}
|
||||
let status = $state<StatusMachine>({
|
||||
wipWord: 0,
|
||||
wipStage: 0,
|
||||
wipEditing: 0,
|
||||
countdown: 0,
|
||||
})
|
||||
|
||||
// --- 显示 ---
|
||||
/** 当前正在处理的词语 */
|
||||
let wipWord = $derived(words[status.wipWord])
|
||||
|
||||
/** 当前正在处理的词语的阶段。如果是最后一个阶段(也就是删除阶段),则取最后
|
||||
* 一个元素。
|
||||
*/
|
||||
let wipStage = $derived(
|
||||
wipWord.stages[Math.min(status.wipStage, wipWord.stages.length - 1)],
|
||||
)
|
||||
|
||||
/** 当前的文本(未经过裁剪的,不含输入法未上屏部分) */
|
||||
let currentText = $derived(
|
||||
typeof wipStage == 'string' ? wipStage : wipStage.text,
|
||||
)
|
||||
|
||||
/** 如果在最后一个阶段,则根据进度去裁剪文本 */
|
||||
let currentTextSliced = $derived(
|
||||
status.wipStage >= wipWord.stages.length
|
||||
? currentText.slice(0, currentText.length - status.wipEditing)
|
||||
: currentText,
|
||||
)
|
||||
|
||||
/** 输入法待上屏区域 */
|
||||
let currentEditing = $derived(
|
||||
typeof wipStage == 'string' || status.wipEditing <= 0
|
||||
? ''
|
||||
: wipStage.editing[status.wipEditing - 1],
|
||||
)
|
||||
|
||||
// 主要的状态更新逻辑
|
||||
const DELAY_STEP_WORD = 250
|
||||
const DELAY_STEP_STAGE = 80
|
||||
const DELAY_STEP_EDIT = 80
|
||||
const DELAY_STEP_DELETE = 50
|
||||
const DELAY_DISPLAY_HOLD = 3000
|
||||
|
||||
// 调试用,如果发现动画有问题,可以在这里调慢动画
|
||||
const DELAY_GLOBAL_FACTOR = 1
|
||||
|
||||
function step(status: StatusMachine, dt: number): StatusMachine {
|
||||
if (status.countdown > 0) {
|
||||
return {
|
||||
...status,
|
||||
countdown: status.countdown - dt / DELAY_GLOBAL_FACTOR,
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof wipStage == 'object') {
|
||||
if (status.wipEditing < wipStage.editing.length) {
|
||||
return {
|
||||
...status,
|
||||
wipEditing: status.wipEditing + 1,
|
||||
countdown: status.countdown + DELAY_STEP_EDIT,
|
||||
}
|
||||
}
|
||||
} else if (status.wipStage >= wipWord.stages.length) {
|
||||
// 此时执行删除操作
|
||||
if (status.wipEditing < currentText.length) {
|
||||
return {
|
||||
...status,
|
||||
wipEditing: status.wipEditing + 1,
|
||||
countdown: status.countdown + DELAY_STEP_DELETE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status.wipStage < wipWord.stages.length) {
|
||||
return {
|
||||
...status,
|
||||
wipStage: status.wipStage + 1,
|
||||
wipEditing: 0,
|
||||
countdown:
|
||||
status.wipStage == wipWord.stages.length - 1
|
||||
? DELAY_DISPLAY_HOLD
|
||||
: DELAY_STEP_STAGE,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...status,
|
||||
wipWord: (status.wipWord + 1) % words.length,
|
||||
wipStage: 0,
|
||||
wipEditing: 0,
|
||||
countdown: DELAY_STEP_WORD,
|
||||
}
|
||||
}
|
||||
|
||||
// 时间循环
|
||||
onMount(() => {
|
||||
let enabled = true
|
||||
let lastTime = Date.now()
|
||||
|
||||
function _step() {
|
||||
if (!enabled) return
|
||||
|
||||
let currentTime = Date.now()
|
||||
|
||||
status = step(status, currentTime - lastTime)
|
||||
lastTime = currentTime
|
||||
|
||||
requestAnimationFrame(_step)
|
||||
}
|
||||
|
||||
_step()
|
||||
|
||||
return () => {
|
||||
enabled = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<span class="typewriter"
|
||||
><span style="user-select: none;"></span><span class="text"
|
||||
>{currentTextSliced}</span
|
||||
><span class="edit">{currentEditing}</span></span
|
||||
>
|
||||
|
||||
<style>
|
||||
@keyframes blink {
|
||||
from,
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.typewriter {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-color: var(--color-bg-1);
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
& > .edit {
|
||||
text-decoration: underline;
|
||||
/* font-family: var(--font-mono); */
|
||||
}
|
||||
|
||||
&::after {
|
||||
--gap-ud: 0.5rem;
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-end: 0.5rem;
|
||||
inset-block-start: var(--gap-ud);
|
||||
block-size: calc(100% - var(--gap-ud) * 2);
|
||||
inline-size: 2px;
|
||||
background-color: var(--color-fg-0);
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
src/components/MarkdocRenderer.tsx
Normal file
35
src/components/MarkdocRenderer.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
|
||||
import { renderMarkdocTree } from '../lib/markdoc'
|
||||
import { CodeBlock } from './markdoc/CodeBlock.tsx'
|
||||
|
||||
export const MarkdocContent: React.FC<{
|
||||
content: string
|
||||
}> = ({ content }) => {
|
||||
/* 注意一下这个元件是动态的 */
|
||||
const [tree, setTree] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
renderMarkdocTree(content).then((result) => {
|
||||
if (active) setTree(result)
|
||||
})
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [content])
|
||||
|
||||
if (!tree) return <div className="loading">渲染中... (๑•̀ㅂ•́)و✧</div>
|
||||
|
||||
return <MarkdocTreeRender tree={tree} />
|
||||
}
|
||||
|
||||
export const MarkdocTreeRender: React.FC<{
|
||||
tree: RenderableTreeNode
|
||||
}> = ({ tree }) => {
|
||||
return (
|
||||
<div className="markdoc-container prose">
|
||||
{Markdoc.renderers.react(tree, React, { components: { CodeBlock } })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
5
src/components/markdoc/CodeBlock.tsx
Normal file
5
src/components/markdoc/CodeBlock.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export const CodeBlock: React.FC<{ highlightedHtml: string }> = (props) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: props.highlightedHtml }}></div>
|
||||
}
|
||||
@ -1,5 +1,10 @@
|
||||
---
|
||||
import '../assets/style.css'
|
||||
import '../assets/fonts.css'
|
||||
import '../assets/prose.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import '@unocss/reset/tailwind-v4.css'
|
||||
import { ClientRouter } from 'astro:transitions'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
@ -13,9 +18,16 @@ const { title = '小帕的小窝' } = Astro.props
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="小帕的小窝 RSS 订阅"
|
||||
href={new URL('rss.xml', Astro.site || 'https://passthem.top')}
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<ClientRouter />
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
/* 在博客翻新时期使用的占位 Layout,讲究的就是极简 */
|
||||
|
||||
import BaseLayout from '../layout/BaseLayout.astro'
|
||||
import FullLayoutV1 from './FullLayoutV1.astro'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
@ -10,28 +10,15 @@ interface Props {
|
||||
const { title = '小帕的小窝' } = Astro.props
|
||||
---
|
||||
|
||||
<BaseLayout title={title}>
|
||||
<FullLayoutV1 title={title}>
|
||||
<div class="main">
|
||||
<slot />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</FullLayoutV1>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
font-family:
|
||||
'HarmonyOS Sans SC',
|
||||
'Noto Sans SC',
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Open Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -42,6 +29,16 @@ const { title = '小帕的小窝' } = Astro.props
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
& :global(h1) {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
& :global(p) {
|
||||
margin-block: 0.25rem;
|
||||
}
|
||||
|
||||
& :global(a) {
|
||||
color: var(--color-link);
|
||||
text-decoration: none;
|
||||
|
||||
328
src/layout/FullLayoutV1.astro
Normal file
328
src/layout/FullLayoutV1.astro
Normal file
@ -0,0 +1,328 @@
|
||||
---
|
||||
import BaseLayout from './BaseLayout.astro'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
withGap?: boolean
|
||||
}
|
||||
|
||||
const { title = '小帕的小窝', withGap = false } = Astro.props
|
||||
---
|
||||
|
||||
<BaseLayout title={title}>
|
||||
<div class="nav-mobile" aria-hidden="true" transition:persist>
|
||||
<button
|
||||
id="nav-mobile-button"
|
||||
onclick="document.getElementById('nav-main').classList.remove('nav-container-hidden')"
|
||||
><Icon name="mdi:menu-close" /></button
|
||||
>
|
||||
<a href="/">小帕的小窝</a>
|
||||
</div>
|
||||
<div
|
||||
id="nav-main"
|
||||
class="nav-container nav-container-hidden"
|
||||
transition:persist
|
||||
>
|
||||
<nav>
|
||||
<ul class="left">
|
||||
<li class="website-logo">
|
||||
<a href="/">小帕的小窝</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/blogs"><Icon name="mdi:invoice-text-outline" />文章</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about"
|
||||
><Icon name="mdi:information-slab-circle-outline" />关于</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="right">
|
||||
<li>
|
||||
<a href="https://legacy.passthem.top"
|
||||
><Icon name="mdi:history" /> 旧博客系统*</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/rss.xml">
|
||||
<Icon name="material-symbols:rss-feed" /> 博客 RSS
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div
|
||||
class="loading-indicator hidden"
|
||||
id="loading-indicator"
|
||||
aria-hidden="true"
|
||||
transition:persist
|
||||
>
|
||||
</div>
|
||||
<main>
|
||||
{withGap && <div aria-hidden="true" class="layout-gap" />}
|
||||
<slot />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const navClose = () => {
|
||||
const navMain = document.getElementById('nav-main')
|
||||
navMain?.classList.add('nav-container-hidden')
|
||||
}
|
||||
|
||||
const navOpen = () => {
|
||||
const navMain = document.getElementById('nav-main')
|
||||
navMain?.classList.remove('nav-container-hidden')
|
||||
}
|
||||
|
||||
document.getElementById('nav-main')?.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.id == 'nav-main') {
|
||||
document.getElementById('nav-main')?.classList.add('nav-container-hidden')
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('nav-main')?.addEventListener('focusin', navOpen)
|
||||
document.getElementById('nav-main')?.addEventListener('focusout', (e) => {
|
||||
if (
|
||||
e.relatedTarget &&
|
||||
!document
|
||||
.getElementById('nav-main')
|
||||
?.contains(e.relatedTarget as HTMLElement)
|
||||
) {
|
||||
navClose()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('astro:before-preparation', () => {
|
||||
document.getElementById('loading-indicator')?.classList.remove('hidden')
|
||||
})
|
||||
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
requestAnimationFrame(navClose)
|
||||
document.getElementById('loading-indicator')?.classList.add('hidden')
|
||||
})
|
||||
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
document.querySelectorAll('#nav-main li:has(a)').forEach((li) => {
|
||||
const child = li.querySelector('a')
|
||||
if (!child) {
|
||||
li.classList.add('url-here')
|
||||
return
|
||||
}
|
||||
if (
|
||||
window.location.href.replace(/\/$/, '') == child.href.replace(/\/$/, '')
|
||||
) {
|
||||
li.classList.add('url-here')
|
||||
} else {
|
||||
li.classList.remove('url-here')
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.nav-container {
|
||||
inline-size: 100dvi;
|
||||
block-size: 3rem;
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(4px);
|
||||
background-color: rgb(from var(--color-bg-n) r g b / 0.5);
|
||||
|
||||
@media (width < 768px) {
|
||||
block-size: 100dvh;
|
||||
background-color: #00000033;
|
||||
transition:
|
||||
background-color ease 0.2s,
|
||||
backdrop-filter ease 0.2s;
|
||||
}
|
||||
|
||||
&.nav-container-hidden {
|
||||
@media (width < 768px) {
|
||||
backdrop-filter: blur(0px);
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > nav {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
padding-inline: 40px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-inline-size: 1400px;
|
||||
margin-inline: auto;
|
||||
|
||||
font-weight: 400;
|
||||
|
||||
@media (width < 768px) {
|
||||
flex-direction: column;
|
||||
padding-block: 5rem;
|
||||
padding-inline: 2rem;
|
||||
align-items: start;
|
||||
inline-size: 70%;
|
||||
background-color: var(--color-bg-n);
|
||||
margin-inline-start: 0;
|
||||
font-size: larger;
|
||||
transition: transform cubic-bezier(0.4, 1, 0.4, 1) 0.4s;
|
||||
}
|
||||
|
||||
.nav-container-hidden & {
|
||||
@media (width < 768px) {
|
||||
transform: translateX(-80dvi);
|
||||
}
|
||||
}
|
||||
|
||||
& > ul {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@media (width < 768px) {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& li {
|
||||
height: 2rem;
|
||||
|
||||
@media (width < 768px) {
|
||||
height: 3rem;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(from var(--color-fg-0) r g b / 0.1);
|
||||
}
|
||||
|
||||
&.url-here {
|
||||
color: var(--color-blue);
|
||||
background-color: rgb(from var(--color-blue) r g b / 0.1);
|
||||
}
|
||||
|
||||
& > a {
|
||||
padding-inline: 0.75rem;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
& > svg {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .website-logo > a {
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
|
||||
/* & > .logo { */
|
||||
/* --size: 2.5rem; */
|
||||
/**/
|
||||
/* width: var(--size); */
|
||||
/* height: var(--size); */
|
||||
/* display: inline-block; */
|
||||
/* } */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-mobile {
|
||||
inline-size: 100dvw;
|
||||
block-size: 4rem;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(4px);
|
||||
background-color: rgb(from var(--color-bg-n) r g b / 0.5);
|
||||
font-size: larger;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-inline: 1rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
& > button {
|
||||
inline-size: 3rem;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
background-color: rgb(from var(--color-fg-0) r g b / 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
& > a {
|
||||
font-weight: 600;
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
background-color: rgb(from var(--color-fg-0) r g b / 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
--color1: oklch(from var(--color-blue) 0.7 0.17 h);
|
||||
--color2: oklch(from var(--color-blue) 0.57 0.25 h);
|
||||
--loading-x-size: 2rem;
|
||||
--loading-height: 0.25rem;
|
||||
|
||||
position: fixed;
|
||||
height: var(--loading-height);
|
||||
background-color: var(--color1);
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-block-start: 0;
|
||||
z-index: 10001;
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
transparent 0%,
|
||||
transparent 25%,
|
||||
var(--color2) 25%,
|
||||
var(--color2) 75%,
|
||||
transparent 75%,
|
||||
transparent 100%
|
||||
);
|
||||
background-repeat: repeat;
|
||||
background-size: 2rem 100%;
|
||||
|
||||
animation: loading-animation infinite 0.5s linear;
|
||||
transition: inset-block-start cubic-bezier(0.4, 1, 0.4, 1) 0.2s;
|
||||
|
||||
&.hidden {
|
||||
inset-block-start: calc(-1 * var(--loading-height));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-animation {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: var(--loading-x-size) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-gap {
|
||||
height: var(--size-nav);
|
||||
}
|
||||
</style>
|
||||
@ -2,8 +2,9 @@ import axios from 'axios'
|
||||
|
||||
export const legacyClient = axios.create({
|
||||
baseURL: import.meta.env.SSR
|
||||
? 'https://legacy.passthem.top/api'
|
||||
: '/api/legacy',
|
||||
? ((await import('astro:env/server')) as any)['LEGACY_BACKEND_URL_SSR']
|
||||
: 'https://legacy.passthem.top/api',
|
||||
// baseURL: 'https://legacy.passthem.top/api',
|
||||
timeout: 6000,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
|
||||
91
src/lib/apis/legacy/blog.ts
Normal file
91
src/lib/apis/legacy/blog.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { legacyClient } from '../clients'
|
||||
|
||||
export type ListBlogItemType = {
|
||||
id: number
|
||||
title: string
|
||||
|
||||
/** ISO8601 */
|
||||
created_at: Date
|
||||
|
||||
/** ISO8601 */
|
||||
updated_at: Date
|
||||
|
||||
featured_image: null | {
|
||||
image_url: string
|
||||
}
|
||||
|
||||
author: {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
avatar: {
|
||||
image_url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type BlogContentType = ListBlogItemType & {
|
||||
content: string
|
||||
}
|
||||
|
||||
export const listBlogs = async ({
|
||||
page = 1,
|
||||
limit = 20,
|
||||
}: {
|
||||
page?: number
|
||||
limit?: number
|
||||
}) => {
|
||||
const resp = await legacyClient.post('/v1/blog/list', { page, limit })
|
||||
return resp.data.data.map((blog: any) => {
|
||||
let _blog = blog
|
||||
if (blog.featured_image) {
|
||||
_blog = {
|
||||
...blog,
|
||||
featured_image: {
|
||||
image_url:
|
||||
'https://legacy.passthem.top' + blog.featured_image.image_url,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
_blog = blog
|
||||
}
|
||||
|
||||
_blog.author.avatar = {
|
||||
image_url: 'https://legacy.passthem.top' + _blog.author.avatar.image_url,
|
||||
}
|
||||
_blog.created_at = new Date(_blog.created_at)
|
||||
_blog.updated_at = new Date(_blog.updated_at)
|
||||
|
||||
return _blog
|
||||
}) as ListBlogItemType[]
|
||||
}
|
||||
|
||||
export const getBlog: (
|
||||
blog_id: number,
|
||||
) => Promise<null | BlogContentType> = async (blog_id: number) => {
|
||||
const resp = await legacyClient.get(`/v1/blog/${blog_id}`, {
|
||||
validateStatus: (status) => status == 200 || status == 404,
|
||||
})
|
||||
if (resp.status == 404) {
|
||||
return null
|
||||
}
|
||||
let _blog = resp.data
|
||||
|
||||
if (_blog.featured_image) {
|
||||
_blog = {
|
||||
..._blog,
|
||||
featured_image: {
|
||||
image_url:
|
||||
'https://legacy.passthem.top' + _blog.featured_image.image_url,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
_blog.author.avatar = {
|
||||
image_url: 'https://legacy.passthem.top' + _blog.author.avatar.image_url,
|
||||
}
|
||||
_blog.created_at = new Date(_blog.created_at)
|
||||
_blog.updated_at = new Date(_blog.updated_at)
|
||||
|
||||
return _blog
|
||||
}
|
||||
32
src/lib/data/contact.json
Normal file
32
src/lib/data/contact.json
Normal file
@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"platform": "YnVodGlH",
|
||||
"href": "dXNlZC1tZWh0c3NhcC9tb2MuYnVodGlnLy86c3B0dGg=",
|
||||
"name": "dXNlZC1tZWh0c3NhcA=="
|
||||
},
|
||||
{
|
||||
"platform": "ZW1pdGFrYVc=",
|
||||
"href": "bWVodHNzYXBAL21vYy5lbWl0YWthdy8vOnNwdHRo",
|
||||
"name": "bWVodHNzYXBA"
|
||||
},
|
||||
{
|
||||
"platform": "ZWJ1dHVvWQ==",
|
||||
"href": "MzgxbWVodHNzYVBAL21vYy5lYnV0dW95Lnd3dy8vOnNwdHRo",
|
||||
"name": "MzgxbWVodHNzYVBA"
|
||||
},
|
||||
{
|
||||
"platform": "ZXRpU290Tw==",
|
||||
"href": "bWVodHNzYXBAL2V0aXMuZGFtb3RvLy86c3B0dGg=",
|
||||
"name": "bWVodHNzYXBA"
|
||||
},
|
||||
{
|
||||
"platform": "bGlhbUU=",
|
||||
"href": "bW9jLmxpYW1nQDM4MW1laHRzc2FwOm90bGlhbQ==",
|
||||
"name": "bW9jLmxpYW1nQDM4MW1laHRzc2Fw"
|
||||
},
|
||||
{
|
||||
"platform": "aWxpYmlsaUI=",
|
||||
"href": "NDA2MjU4MjkvbW9jLmlsaWJpbGliLmVjYXBzLy86c3B0dGg=",
|
||||
"name": "bWVodHNzYXA="
|
||||
}
|
||||
]
|
||||
4
src/lib/keyv-cache.server.ts
Normal file
4
src/lib/keyv-cache.server.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { REDIS_URL } from 'astro:env/server'
|
||||
import KeyvRedis from '@keyv/redis'
|
||||
|
||||
export const storage = REDIS_URL ? new KeyvRedis(REDIS_URL) : new Map()
|
||||
20
src/lib/markdoc-cache.server.ts
Normal file
20
src/lib/markdoc-cache.server.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import './server-only.ts'
|
||||
|
||||
import type { RenderableTreeNode } from '@markdoc/markdoc'
|
||||
import KeyV from 'keyv'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { storage } from './keyv-cache.server.ts'
|
||||
|
||||
const markdocVersion = '1'
|
||||
|
||||
export const markdocCache = new KeyV<RenderableTreeNode>({
|
||||
store: storage,
|
||||
namespace: `markdoc-cache-${markdocVersion}`,
|
||||
ttl: 1000 * 60 * 60 * 24 * 7,
|
||||
})
|
||||
export const generateCacheKey = (content: string) => {
|
||||
return createHash('sha256')
|
||||
.update(content)
|
||||
.update(markdocVersion)
|
||||
.digest('hex')
|
||||
}
|
||||
21
src/lib/markdoc.server.ts
Normal file
21
src/lib/markdoc.server.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import './server-only.ts'
|
||||
|
||||
import { renderMarkdocTree } from './markdoc.ts'
|
||||
import { generateCacheKey, markdocCache } from './markdoc-cache.server.ts'
|
||||
|
||||
export const getCachedMarkdocTree = async (content: string) => {
|
||||
if (!import.meta.env.SSR) {
|
||||
return await renderMarkdocTree(content)
|
||||
}
|
||||
|
||||
const key = generateCacheKey(content)
|
||||
const cached = await markdocCache.get(key)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const tree = await renderMarkdocTree(content)
|
||||
markdocCache.set(key, tree)
|
||||
return tree
|
||||
}
|
||||
47
src/lib/markdoc.ts
Normal file
47
src/lib/markdoc.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import Markdoc, { type Config } from '@markdoc/markdoc'
|
||||
import { ensureShikiEngine, shikiRender } from './markdown'
|
||||
|
||||
const { nodes, Tag } = Markdoc
|
||||
|
||||
export const markdocConfig: Config = {
|
||||
nodes: {
|
||||
fence: {
|
||||
render: 'CodeBlock',
|
||||
attributes: {
|
||||
...nodes.fence.attributes,
|
||||
highlightedHtml: { type: String },
|
||||
},
|
||||
transform(node, config) {
|
||||
/*
|
||||
* 注意力!!!!!!!!!!!!!!
|
||||
* 注意!!!!!!!!!!!!
|
||||
*
|
||||
* 如果改了这里的逻辑,记得更改 `markdoc-cache.ts` 里的版本号
|
||||
* 以让对应的缓存能够被更新
|
||||
*
|
||||
**/
|
||||
|
||||
const attributes = node.transformAttributes(config)
|
||||
|
||||
const highlightedHtml = shikiRender(
|
||||
node.attributes.content,
|
||||
node.attributes.language,
|
||||
)
|
||||
|
||||
return new Tag(
|
||||
'CodeBlock',
|
||||
{ ...attributes, highlightedHtml },
|
||||
node.transformChildren(config),
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const renderMarkdocTree = async (content: string) => {
|
||||
const ast = Markdoc.parse(content)
|
||||
await ensureShikiEngine()
|
||||
const tree = Markdoc.transform(ast, markdocConfig)
|
||||
|
||||
return tree
|
||||
}
|
||||
117
src/lib/markdown.ts
Normal file
117
src/lib/markdown.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import mdKatex from '@traptitech/markdown-it-katex'
|
||||
import {
|
||||
createHighlighter,
|
||||
type BundledLanguage,
|
||||
type Highlighter,
|
||||
} from 'shiki'
|
||||
|
||||
let highlighter: Highlighter | null = null
|
||||
|
||||
const allowed_langs: BundledLanguage[] = [
|
||||
'python',
|
||||
'javascript',
|
||||
'typescript',
|
||||
'typst',
|
||||
'markdown',
|
||||
'json',
|
||||
'toml',
|
||||
'yaml',
|
||||
'bash',
|
||||
'c',
|
||||
'c++',
|
||||
'rust',
|
||||
'go',
|
||||
'zig',
|
||||
'makefile',
|
||||
'make',
|
||||
'nim',
|
||||
'nix',
|
||||
'kdl',
|
||||
'md',
|
||||
'sh',
|
||||
'mermaid',
|
||||
'cpp',
|
||||
'fish',
|
||||
'zsh',
|
||||
'docker',
|
||||
'dockerfile',
|
||||
'yml',
|
||||
'system-verilog',
|
||||
'sql',
|
||||
'glsl',
|
||||
'mediawiki',
|
||||
'systemd',
|
||||
'ini',
|
||||
]
|
||||
|
||||
export async function ensureShikiEngine() {
|
||||
if (highlighter === null) {
|
||||
highlighter = await createHighlighter({
|
||||
themes: ['one-light', 'one-dark-pro'],
|
||||
/* 额...要我自己定义需要的所有语言吗 */
|
||||
langs: allowed_langs,
|
||||
})
|
||||
}
|
||||
return highlighter
|
||||
}
|
||||
|
||||
export function shikiRender(code: string, lang: string) {
|
||||
return highlighter!.codeToHtml(code, {
|
||||
lang: (allowed_langs as string[]).indexOf(lang) != -1 ? lang : 'text',
|
||||
themes: {
|
||||
light: 'one-light',
|
||||
dark: 'one-dark-pro',
|
||||
},
|
||||
defaultColor: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* KaTeX 渲染结果修改。
|
||||
*
|
||||
* 让 KaTeX 的公式可复制。
|
||||
*/
|
||||
|
||||
function setupKatexCopy(md: MarkdownIt) {
|
||||
const defaultInline = md.renderer.rules.math_inline!
|
||||
const defaultBlock = md.renderer.rules.math_block!
|
||||
|
||||
md.renderer.rules.math_inline = (tokens, idx, options, env, self) => {
|
||||
const content = tokens[idx].content
|
||||
const rendered = defaultInline(tokens, idx, options, env, self)
|
||||
|
||||
return `<span class="katex-wrapper inline" data-code="${encodeURIComponent(content)}">${rendered}</span>`
|
||||
}
|
||||
|
||||
md.renderer.rules.math_block = (tokens, idx, options, env, self) => {
|
||||
const content = tokens[idx].content
|
||||
const rendered = defaultBlock(tokens, idx, options, env, self)
|
||||
return `
|
||||
<div class="katex-wrapper block" data-code="${encodeURIComponent(content)}">${rendered}
|
||||
<button class="copy-katex" title="Copy LaTeX">
|
||||
复制
|
||||
</button>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown 渲染函数。
|
||||
*
|
||||
* 为了让客户端和服务端表现相同,所以单独拿出来了一个 Markdown 渲染器。
|
||||
*/
|
||||
export async function renderMarkdown(content: string): Promise<string> {
|
||||
await ensureShikiEngine()
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
highlight: shikiRender,
|
||||
})
|
||||
|
||||
md.use(mdKatex)
|
||||
setupKatexCopy(md)
|
||||
|
||||
return md.render(content)
|
||||
}
|
||||
3
src/lib/server-only.ts
Normal file
3
src/lib/server-only.ts
Normal file
@ -0,0 +1,3 @@
|
||||
if (!import.meta.env.SSR) {
|
||||
throw Error('这段代码不可以在客户端执行!')
|
||||
}
|
||||
10
src/pages/404.astro
Normal file
10
src/pages/404.astro
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
import BoringLayout from '../layout/BoringLayout.astro'
|
||||
---
|
||||
|
||||
<BoringLayout title="404 - 小帕的小窝">
|
||||
<h1>网页走丢了!!</h1>
|
||||
<p>不知道为什么就到了这里。。</p>
|
||||
<p><a href="javascript:history.back()">点击这里</a>回到上一页</p>
|
||||
<p>或者<a href="/">点击这里</a>回到主页</p>
|
||||
</BoringLayout>
|
||||
10
src/pages/500.astro
Normal file
10
src/pages/500.astro
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
import BoringLayout from '../layout/BoringLayout.astro'
|
||||
---
|
||||
|
||||
<BoringLayout title="500 - 小帕的小窝">
|
||||
<h1>服务器内部错误</h1>
|
||||
<p>哦不肯定是小帕搞错了什么东西。。</p>
|
||||
<p><a href="javascript:history.back()">点击这里</a>回到上一页</p>
|
||||
<p>或者<a href="/">点击这里</a>回到主页</p>
|
||||
</BoringLayout>
|
||||
5
src/pages/[...slug].astro
Normal file
5
src/pages/[...slug].astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
export const prerender = false
|
||||
|
||||
return Astro.redirect('/404')
|
||||
---
|
||||
@ -1,35 +1,13 @@
|
||||
---
|
||||
import BoringLayout from '../layout/BoringLayout.astro'
|
||||
|
||||
const friends = [
|
||||
['https://omega98.top', '核子的博客'],
|
||||
['https://tnot.top', 'TNOT 的博客'],
|
||||
['https://ruusuge.top', 'ルース毛的博客'],
|
||||
['https://akarin.dev/', '宸佬的博客'],
|
||||
['https://wzq02.top', 'wzq02 的博客'],
|
||||
]
|
||||
---
|
||||
|
||||
<BoringLayout title="关于">
|
||||
<h1>关于<a href="/">这里</a></h1>
|
||||
<p>这里是 passthem 和他朋友们的博客</p>
|
||||
<p>主要由我也就是 passthem 运维这个网站,但是我会接收朋友们的投稿</p>
|
||||
<h1>怎么感觉有点未完成</h1>
|
||||
<p>之前,我心血来潮,写了一个自己的博客系统,但是我把它的源代码搞丢了!</p>
|
||||
<p>正好想重新设计,所以,我打算重构了。但是,重构它很漫长</p>
|
||||
<p>这里的占位的东西,会慢慢填充的!</p>
|
||||
<h1>友链</h1>
|
||||
<p>
|
||||
{
|
||||
friends
|
||||
.map((friend) => {
|
||||
return <a href={friend[0]}>{friend[1]}</a>
|
||||
})
|
||||
.map((block, i) => {
|
||||
if (i == friends.length - 1) {
|
||||
return block
|
||||
} else {
|
||||
return [block, <span>・</span>]
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
</p>
|
||||
</BoringLayout>
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
import type { APIRoute } from 'astro'
|
||||
import axios from 'axios'
|
||||
|
||||
export const prerender = false
|
||||
export const ALL: APIRoute = async ({ params, request }) => {
|
||||
const { path } = params
|
||||
const legacyBaseUrl =
|
||||
import.meta.env.LEGACY_SERVER_URL || 'https://legacy.passthem.top/api'
|
||||
|
||||
const targetUrl = `${legacyBaseUrl}/${path}`
|
||||
const headers = Object.fromEntries(request.headers.entries())
|
||||
|
||||
delete headers['host']
|
||||
delete headers['connection']
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: request.method,
|
||||
url: targetUrl,
|
||||
data:
|
||||
request.method !== 'GET'
|
||||
? await request.json().catch(() => null)
|
||||
: undefined,
|
||||
headers,
|
||||
params: Object.fromEntries(new URL(request.url).searchParams),
|
||||
validateStatus: () => true,
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
|
||||
return new Response(response.data, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': response.headers['content-type'] || 'application/json',
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Gateway Error', message: error.message }),
|
||||
{ status: 502 },
|
||||
)
|
||||
}
|
||||
}
|
||||
144
src/pages/blogs.astro
Normal file
144
src/pages/blogs.astro
Normal file
@ -0,0 +1,144 @@
|
||||
---
|
||||
import FullLayoutV1 from '../layout/FullLayoutV1.astro'
|
||||
import { listBlogs, type ListBlogItemType } from '../lib/apis/legacy/blog'
|
||||
import BlogCard from '../components/BlogCard.astro'
|
||||
import BlogHeaderImage from '../assets/blogs-header.webp'
|
||||
import { Image } from 'astro:assets'
|
||||
|
||||
export const prerender = false
|
||||
|
||||
const _page = parseInt(Astro.url.searchParams.get('page') || '1')
|
||||
const page = isNaN(_page) ? 1 : Math.max(1, _page)
|
||||
|
||||
let blogs: ListBlogItemType[] = []
|
||||
|
||||
try {
|
||||
blogs = await listBlogs({ page, limit: 100 })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return Astro.redirect('/500')
|
||||
}
|
||||
---
|
||||
|
||||
<FullLayoutV1 withGap title="博客列表 - 小帕的小窝">
|
||||
<!-- <div class="__dev__caution">
|
||||
<h1>仍在开发中,界面会崩坏</h1>
|
||||
</div> -->
|
||||
<div class="container">
|
||||
<section class="blogs-header">
|
||||
<Image
|
||||
class="image"
|
||||
src={BlogHeaderImage}
|
||||
alt="博客列表的头图,一个悬挂在空中的 LCD1602,上面写着一些示例文本"
|
||||
width={960}
|
||||
height={540}
|
||||
densities={[1.5, 2]}
|
||||
/>
|
||||
<div class="description">
|
||||
<h1>博客列表</h1>
|
||||
<h2>这里是 passthem 和他朋友们的文章</h2>
|
||||
</div>
|
||||
</section>
|
||||
<main>
|
||||
<ul>
|
||||
{
|
||||
blogs.map((blog) => (
|
||||
<BlogCard
|
||||
title={blog.title}
|
||||
link={`/blogs/${blog.id}`}
|
||||
featureImage={blog.featured_image?.image_url}
|
||||
author={{
|
||||
name: blog.author.nickname,
|
||||
avatar: blog.author.avatar.image_url,
|
||||
}}
|
||||
createdAt={blog.created_at}
|
||||
updatedAt={blog.updated_at}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</main>
|
||||
</div>
|
||||
</FullLayoutV1>
|
||||
|
||||
<style>
|
||||
.__dev__caution {
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-inline-size: 960px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.blogs-header {
|
||||
margin-block-end: 3rem;
|
||||
position: relative;
|
||||
aspect-ratio: 16 / 9;
|
||||
|
||||
@media (width < 768px) and (width >= 540px) {
|
||||
aspect-ratio: 2.5 / 1;
|
||||
}
|
||||
|
||||
@media (width >= 768px) {
|
||||
aspect-ratio: 4 / 1;
|
||||
}
|
||||
|
||||
& > .image {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: saturate(0.6);
|
||||
object-fit: cover;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
& > .description {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 0.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #00000099;
|
||||
color: white;
|
||||
|
||||
& > h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& > h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
& > h1,
|
||||
& > h2 {
|
||||
text-shadow: 0 0 10px black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 768px) {
|
||||
main {
|
||||
margin-inline: auto;
|
||||
max-width: 840px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
181
src/pages/blogs/[blog_id].astro
Normal file
181
src/pages/blogs/[blog_id].astro
Normal file
@ -0,0 +1,181 @@
|
||||
---
|
||||
import { getBlog } from '../../lib/apis/legacy/blog'
|
||||
import FullLayoutV1 from '../../layout/FullLayoutV1.astro'
|
||||
import { MarkdocTreeRender } from '../../components/MarkdocRenderer.tsx'
|
||||
import { getCachedMarkdocTree } from '../../lib/markdoc.server.ts'
|
||||
import { Image } from 'astro:assets'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
import Artalk from '../../components/Artalk.svelte'
|
||||
|
||||
export const prerender = false
|
||||
|
||||
const { blog_id = '' } = Astro.params
|
||||
const blog_id_num = parseInt(blog_id)
|
||||
|
||||
if (isNaN(blog_id_num) || blog_id_num < 0) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
let blogData = null
|
||||
|
||||
try {
|
||||
blogData = await getBlog(blog_id_num)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return Astro.redirect('/500')
|
||||
}
|
||||
|
||||
if (blogData === null) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
const tree = await getCachedMarkdocTree(blogData.content)
|
||||
const formatDate = (date: Date) =>
|
||||
`${date.getFullYear()} 年 ${date.getMonth() + 1} 月 ${date.getDate()} 日`
|
||||
---
|
||||
|
||||
<FullLayoutV1 title={blogData.title} withGap>
|
||||
{
|
||||
blogData.featured_image && (
|
||||
<div class="featured-image">
|
||||
<Image
|
||||
class="image"
|
||||
src={blogData.featured_image.image_url}
|
||||
width={960}
|
||||
height={540}
|
||||
alt="博客的头图"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<main>
|
||||
<h1>{blogData.title}</h1>
|
||||
{
|
||||
blogData.author && (
|
||||
<div class="blog-info">
|
||||
<a
|
||||
class="author"
|
||||
href={`https://legacy.passthem.top/user/${blogData.author.username}`}
|
||||
>
|
||||
<div class="avatar">
|
||||
<Image
|
||||
class="image"
|
||||
src={blogData.author.avatar.image_url}
|
||||
width={32}
|
||||
height={32}
|
||||
alt={`用户 ${blogData.author.nickname} 的头像`}
|
||||
/>
|
||||
</div>
|
||||
<div class="username">{blogData.author.nickname}</div>
|
||||
</a>
|
||||
<span class="split">・</span>
|
||||
<span>{formatDate(blogData.updated_at)} 更新</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<article>
|
||||
<MarkdocTreeRender tree={tree} client:load />
|
||||
</article>
|
||||
<h1 class="comment-title">
|
||||
<Icon name="material-symbols:chat-rounded" /> 评论区
|
||||
</h1>
|
||||
<Artalk client:idle />
|
||||
</main>
|
||||
</FullLayoutV1>
|
||||
|
||||
<style>
|
||||
.__dev__caution {
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.featured-image {
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin-block-end: 2rem;
|
||||
position: relative;
|
||||
|
||||
& > .image {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
background: linear-gradient(to bottom, transparent, #00000044);
|
||||
}
|
||||
|
||||
@media (width < 768px) and (width >= 540px) {
|
||||
aspect-ratio: 2.5 / 1;
|
||||
}
|
||||
|
||||
@media (width >= 768px) {
|
||||
aspect-ratio: 4 / 1;
|
||||
margin-block-end: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-fg-1);
|
||||
|
||||
& > .author {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
& > .avatar {
|
||||
& > .image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: cover;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
max-inline-size: 840px;
|
||||
margin-inline: auto;
|
||||
padding-block-end: 4rem;
|
||||
padding-inline: 20px;
|
||||
|
||||
& > h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 900;
|
||||
margin-block-end: 1.2rem;
|
||||
margin-block-start: 2rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
&.comment-title {
|
||||
margin-block-start: 3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,9 +0,0 @@
|
||||
---
|
||||
import BoringLayout from '../layout/BoringLayout.astro'
|
||||
---
|
||||
|
||||
<BoringLayout title="联络我">
|
||||
<h1>联络我</h1>
|
||||
<p>邮箱:<a href="mailto:passthem183@gmail.com">passthem183@gmail.com</a></p>
|
||||
<p>点击 <a href="/">这里</a> 返回主页</p>
|
||||
</BoringLayout>
|
||||
@ -1,13 +1,19 @@
|
||||
---
|
||||
export const prerender = false
|
||||
|
||||
import BoringLayout from '../layout/BoringLayout.astro'
|
||||
// import BoringLayout from '../layout/BoringLayout.astro'
|
||||
import FullLayoutV1 from '../layout/FullLayoutV1.astro'
|
||||
import MainpageButton from '../components/MainpageButton.svelte'
|
||||
import MainpageComments from '../components/MainpageComments.astro'
|
||||
import MainpageTypewriter from '../components/MainpageTypewriter.svelte'
|
||||
import MainpageContactMe from '../components/MainpageContactMe.svelte'
|
||||
import { Image } from 'astro:assets'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import PassthemAvatar from '../assets/mainpage_avatars/passthem.png'
|
||||
|
||||
import { mainpageGetClick } from '../lib/apis/legacy/mainpage'
|
||||
|
||||
// 无用按钮的数字预注入
|
||||
|
||||
let initialClicks = 0
|
||||
|
||||
try {
|
||||
@ -18,10 +24,653 @@ try {
|
||||
}
|
||||
---
|
||||
|
||||
<BoringLayout>
|
||||
<h1>博客系统翻新中...</h1>
|
||||
<p>点击 <a href="https://legacy.passthem.top">这里</a> 查看旧版博客</p>
|
||||
<p>或者了解更多 <a href="/about">关于这里</a></p>
|
||||
<p>也欢迎你来 <a href="/contact">联络我</a></p>
|
||||
<p><MainpageButton initialClicks={initialClicks} client:load /></p>
|
||||
</BoringLayout>
|
||||
<FullLayoutV1 title="首页 - 小帕的小窝">
|
||||
<!-- 就像写幻灯片一样的逻辑写它吧! -->
|
||||
<section class="slide">
|
||||
<!-- 自我介绍部分 -->
|
||||
<div class="self-introduction">
|
||||
<figure class="avatar">
|
||||
<Image
|
||||
src={PassthemAvatar}
|
||||
alt="passthem 的头像,一个戴眼镜的男孩"
|
||||
loading="lazy"
|
||||
width={240}
|
||||
height={240}
|
||||
densities={[1.5, 2]}
|
||||
/>
|
||||
</figure>
|
||||
<div class="descriptions">
|
||||
<div class="motto">
|
||||
<p>passthem¹</p>
|
||||
<p>一个<MainpageTypewriter client:load />的</p>
|
||||
<p>个人势创作者</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<MainpageButton initialClicks={initialClicks} client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notation">
|
||||
<p>¹我在各个平台的惯用 ID,开头字母大小写无所谓</p>
|
||||
<p>
|
||||
²头像由 <a href="https://space.bilibili.com/2017869">Yscao@bilibili</a> 绘制
|
||||
</p>
|
||||
<p>
|
||||
³有人说这个配色像 <a
|
||||
href="https://www.bilibili.com/video/av383664698/"
|
||||
class="thsx">涂黑书信</a
|
||||
>
|
||||
</p>
|
||||
<p class="animation-error-warning">
|
||||
⁴你的系统设置中似乎禁用了动画效果,而 Firefox
|
||||
在禁用动画效果时,网页的表现会很奇怪
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide">
|
||||
<div class="slide-title-design1" aria-hidden="true">
|
||||
<svg width="800" height="240" viewBox="0 0 800 240">
|
||||
<text x="10" y="220" class="stroke-layer">留言</text>
|
||||
<text x="0" y="210" class="main-layer">留言</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="slide-title-design2" aria-hidden="true">
|
||||
<Icon name="mdi:comment-quote" />
|
||||
</div>
|
||||
<div class="comment-page-container">
|
||||
<h1>来自大家的留言</h1>
|
||||
<div class="comment-container">
|
||||
<MainpageComments />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide">
|
||||
<div class="slide-title-design1" aria-hidden="true">
|
||||
<svg width="800" height="240" viewBox="0 0 800 240">
|
||||
<text x="10" y="220" class="stroke-layer">联络</text>
|
||||
<text x="0" y="210" class="main-layer">联络</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="slide-title-design2" aria-hidden="true">
|
||||
<Icon name="mdi:email" />
|
||||
</div>
|
||||
<div class="contact-container">
|
||||
<h1>
|
||||
<div class="text">联系我<div class="animation-before"></div></div><div
|
||||
class="animation"
|
||||
>
|
||||
</div>
|
||||
</h1>
|
||||
<MainpageContactMe client:idle />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slide">
|
||||
<div class="slide-title-design1" aria-hidden="true">
|
||||
<svg width="800" height="240" viewBox="0 0 800 240">
|
||||
<text x="10" y="220" class="stroke-layer">友链</text>
|
||||
<text x="0" y="210" class="main-layer">友链</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="slide-title-design2" aria-hidden="true">
|
||||
<Icon name="mdi:link-variant" />
|
||||
</div>
|
||||
<h1>友情链接</h1>
|
||||
<div class="friends">
|
||||
{
|
||||
[
|
||||
[
|
||||
'https://omega98.top/images/blog_avatar.jpg',
|
||||
'https://omega98.top',
|
||||
'核子的博客',
|
||||
],
|
||||
['https://www.tnot.top/logo.png', 'https://tnot.top', 'TNOT 的博客'],
|
||||
[
|
||||
'https://ruusuge.top/img/logo_hu_cc283ac364b1bc0b.png',
|
||||
'https://ruusuge.top',
|
||||
'ルース毛的博客',
|
||||
],
|
||||
[
|
||||
'https://cdn.jsdelivr.net/gh/TransparentLC/transparentlc.github.io/img/avatar.jpg',
|
||||
'https://akarin.dev',
|
||||
'存在感消失的地方',
|
||||
],
|
||||
[
|
||||
'https://wzq02.top/assets/icons/favicon-96x96.png',
|
||||
'https://wzq02.top',
|
||||
'wzq02 的博客',
|
||||
],
|
||||
[
|
||||
'https://pics.r1kka.one/file/1738764637932_-305500c1acccdf39.jpg',
|
||||
'https://r1kka.one/',
|
||||
"Rikka's Blog",
|
||||
],
|
||||
[
|
||||
'https://shimizukaede.top/vite.svg',
|
||||
'https://shimizukaede.top',
|
||||
'Kaede 的博客',
|
||||
],
|
||||
[
|
||||
'https://nonerd.tech/assets/apple-touch-icon.png',
|
||||
'https://nonerd.tech/',
|
||||
'废柴铁克诺',
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/ydt-DIeb2Djx.png',
|
||||
'https://qm.qq.com/q/nDnHUy9KQo',
|
||||
'有顶天变电站 (QQ)',
|
||||
],
|
||||
[
|
||||
'https://legacy.passthem.top/assets/lfxdxy-BogfTZvz.png',
|
||||
'https://qm.qq.com/q/QOpCVZcvyS',
|
||||
'六方相的新月 (QQ)',
|
||||
],
|
||||
].map((b) => (
|
||||
<a href={b[1]} class="friend">
|
||||
<div class="avatar">
|
||||
<Image
|
||||
src={b[0]}
|
||||
width={60}
|
||||
height={60}
|
||||
alt={`网站「${b[2]}」的图标`}
|
||||
/>
|
||||
</div>
|
||||
<div class="description">{b[2]}</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</FullLayoutV1>
|
||||
|
||||
<script>
|
||||
const setupObserver = () => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.intersectionRatio > 0.5) {
|
||||
entry.target.classList.add('slide-visible')
|
||||
} else {
|
||||
entry.target.classList.remove('slide-visible')
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
)
|
||||
|
||||
document.querySelectorAll('section.slide').forEach((ele) => {
|
||||
observer.observe(ele)
|
||||
;(ele as HTMLElement).addEventListener('focusin', () => {
|
||||
ele.scrollIntoView({
|
||||
block: 'start',
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener('astro:page-load', setupObserver)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(html) {
|
||||
scroll-snap-type: y proximity;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.slide {
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
height: 100lvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
|
||||
&:nth-child(2n) {
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
& > .notation {
|
||||
position: absolute;
|
||||
width: 100dvw;
|
||||
padding-inline-start: 2rem;
|
||||
padding-block-end: 2rem;
|
||||
inset-inline-start: 0;
|
||||
inset-block-end: calc(100lvh - 100dvh);
|
||||
font-size: smaller;
|
||||
color: var(--color-fg-0);
|
||||
opacity: 0.3;
|
||||
|
||||
& a {
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-fg-0);
|
||||
color: var(--color-bg-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& h1 {
|
||||
font-size: clamp(2.5rem, 6vw, 3rem);
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.animation-error-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@-moz-document url-prefix() {
|
||||
.animation-error-warning {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global(html) {
|
||||
scroll-snap-type: none;
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@property --avatar-scalar {
|
||||
syntax: '<number>';
|
||||
inherits: true;
|
||||
initial-value: 0;
|
||||
}
|
||||
|
||||
@property --avatar-hover-scalar {
|
||||
syntax: '<number>';
|
||||
inherits: true;
|
||||
initial-value: 0;
|
||||
}
|
||||
|
||||
@property --avatar-rotation {
|
||||
syntax: '<angle>';
|
||||
inherits: true;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
|
||||
/* 自我介绍部分 */
|
||||
.self-introduction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row-reverse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
& > .avatar {
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
--final-width: clamp(160px, 30vw, 240px);
|
||||
--avatar-scalar: 0;
|
||||
--avatar-hover-scalar: 1;
|
||||
--avatar-rotation: 20deg;
|
||||
transition:
|
||||
--avatar-rotation cubic-bezier(0.2, 1, 0.4, 1) 1.8s,
|
||||
--avatar-scalar cubic-bezier(0.2, 2, 0.3, 0.8) 1.2s,
|
||||
--avatar-hover-scalar cubic-bezier(0.2, 3, 0.4, 0.6) 0.6s,
|
||||
width cubic-bezier(0.4, 1, 0.4, 1) 0.9s;
|
||||
|
||||
transform: rotate(var(--avatar-rotation)) scale(var(--avatar-scalar))
|
||||
scale(var(--avatar-hover-scalar));
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
width: 0;
|
||||
|
||||
section.slide.slide-visible & {
|
||||
--avatar-scalar: 1;
|
||||
--avatar-rotation: 0deg;
|
||||
width: var(--final-width);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
--avatar-hover-scalar: 1.05;
|
||||
}
|
||||
}
|
||||
|
||||
& > .descriptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
|
||||
& > .motto {
|
||||
font-size: clamp(2rem, 4vw, 4rem);
|
||||
font-weight: 800;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
width: 11em;
|
||||
|
||||
& > p {
|
||||
width: max-content;
|
||||
margin-inline: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-inline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 涂黑书信的特殊处理 */
|
||||
.thsx {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.motto > p {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
background-color: var(--color-fg-0);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset-inline-start: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
section:has(.thsx:hover) .motto > p,
|
||||
section:has(.thsx:focus) .motto > p {
|
||||
&:nth-child(1)::before {
|
||||
transition-delay: 0;
|
||||
}
|
||||
&:nth-child(2)::before {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
&:nth-child(3)::before {
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
|
||||
&::before {
|
||||
transition: inset-inline-start cubic-bezier(0.2, 1, 0.17, 1) 1s;
|
||||
inset-inline-start: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
section:has(.thsx:hover) .buttons {
|
||||
transition: filter cubic-bezier(0.2, 1, 0.17, 1) 1s;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
/* 一个特殊设计的标题元素,呼呼 */
|
||||
.slide-title-design1 {
|
||||
position: absolute;
|
||||
inset-inline-start: -3rem;
|
||||
inset-block-end: -2rem;
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
z-index: -1;
|
||||
transition: all ease 1.5s;
|
||||
|
||||
section.slide.slide-visible & {
|
||||
opacity: 0.2;
|
||||
inset-inline-start: -1rem;
|
||||
}
|
||||
|
||||
& > svg {
|
||||
font-weight: 900;
|
||||
font-size: 10rem;
|
||||
|
||||
& > .stroke-layer {
|
||||
fill: none;
|
||||
stroke: var(--color-fg-0);
|
||||
stroke-width: 1px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
& > .main-layer {
|
||||
fill: var(--color-fg-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
.slide-title-design2 {
|
||||
position: absolute;
|
||||
inset-inline-end: -5rem;
|
||||
inset-block-start: -2rem;
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
z-index: -1;
|
||||
transition: all ease 1.5s;
|
||||
|
||||
section.slide.slide-visible & {
|
||||
opacity: 0.2;
|
||||
inset-inline-end: -3rem;
|
||||
}
|
||||
|
||||
& > svg {
|
||||
font-size: 288px;
|
||||
transform: rotate(-12deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 评论页 */
|
||||
.comment-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > .comment-container {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
transition: all cubic-bezier(0.4, 1, 0.4, 1) 1.2s;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--color-bg-n);
|
||||
width: calc(min(100dvw - 2rem, 40rem));
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 1rem;
|
||||
transition: all cubic-bezier(0.4, 1, 0.4, 1) 1.2s;
|
||||
position: absolute;
|
||||
transform: rotate(0deg);
|
||||
z-index: -1;
|
||||
background-color: rgb(from var(--color-fg-0) r g b / 0.05);
|
||||
}
|
||||
|
||||
section.slide.slide-visible & {
|
||||
margin-block-start: 2rem;
|
||||
height: 60dvh;
|
||||
|
||||
&::before {
|
||||
transform: rotate(-6deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 联络页 */
|
||||
@keyframes bg-contact-roll {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: var(--bg-width) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
& > h1 {
|
||||
--height: 1.5em;
|
||||
--skew-angle: 15deg;
|
||||
--col1: var(--color-fg-0);
|
||||
--bg-width: 2rem;
|
||||
--unit: calc(tan(var(--skew-angle)) * var(--height) * 0.5);
|
||||
|
||||
height: var(--height);
|
||||
line-height: var(--height);
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
color: var(--color-bg-0);
|
||||
margin-block-end: 0.5em;
|
||||
position: relative;
|
||||
|
||||
& > .text {
|
||||
background-color: var(--color-fg-0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& .animation-before {
|
||||
width: calc(var(--unit) * 2);
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
inset-inline-end: calc(var(--unit) * -2);
|
||||
inset-block-start: 0;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
background-color: var(--col1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: skewX(var(--skew-angle)) translateX(calc(-1 * var(--unit)));
|
||||
}
|
||||
}
|
||||
|
||||
& > .animation {
|
||||
content: '';
|
||||
display: block;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0,
|
||||
transparent 25%,
|
||||
var(--col1) 25%,
|
||||
var(--col1) 75%,
|
||||
transparent 75%,
|
||||
transparent 100%
|
||||
);
|
||||
background-repeat: repeat;
|
||||
background-size: var(--bg-width) 100%;
|
||||
animation: bg-contact-roll infinite linear 2s;
|
||||
transform: skewX(var(--skew-angle)) translateX(calc(var(--unit) - 1px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 友链页 */
|
||||
.slide:has(> .friends) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.friends {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
max-width: 50rem;
|
||||
max-height: 30rem;
|
||||
padding-block: 3rem;
|
||||
overflow-y: auto;
|
||||
margin-inline: 1rem;
|
||||
/* background-color: rgb(from var(--color-bg-0) r g b / .3); */
|
||||
background-color: rgb(from var(--color-bg-2) r g b / 0.5);
|
||||
border-radius: 2rem;
|
||||
backdrop-filter: blur(5px);
|
||||
|
||||
& > .friend {
|
||||
background-color: var(--color-bg-0);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
width: 14rem;
|
||||
height: 60px;
|
||||
grid-template-columns: 60px 1fr;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
|
||||
& > .avatar {
|
||||
border-radius: 30px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-n);
|
||||
/* box-shadow: 0 0 0 10px var(--color-bg-2); */
|
||||
transition: all cubic-bezier(0.2, 1, 0.4, 1) 0.5s;
|
||||
}
|
||||
|
||||
& > .description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 30px;
|
||||
background-color: var(--color-bg-n);
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
inset-inline-start: calc(-100% + 30px);
|
||||
box-shadow: 0 0 0 10px var(--color-blue);
|
||||
transition: inset-inline-start cubic-bezier(0.2, 1, 0.4, 1) 0.5s;
|
||||
}
|
||||
|
||||
outline: solid 0px transparent;
|
||||
outline-offset: 0px;
|
||||
transition: all cubic-bezier(0.2, 1, 0.4, 1) 0.5s;
|
||||
|
||||
&:focus {
|
||||
outline: solid 3px var(--color-blue);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&:focus::before {
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
&:hover .avatar,
|
||||
&:focus .avatar {
|
||||
/* transform: rotate(360deg); */
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
97
src/pages/rss.xml.ts
Normal file
97
src/pages/rss.xml.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import rss, { type RSSFeedItem } from '@astrojs/rss'
|
||||
import type { APIRoute } from 'astro'
|
||||
import {
|
||||
getBlog,
|
||||
listBlogs,
|
||||
type ListBlogItemType,
|
||||
} from '../lib/apis/legacy/blog'
|
||||
import { ensureShikiEngine } from '../lib/markdown'
|
||||
// import { toMarkdocTree } from '../lib/markdoc'
|
||||
import Markdoc from '@markdoc/markdoc'
|
||||
import KeyV from 'keyv'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { storage } from '../lib/keyv-cache.server'
|
||||
|
||||
export const prerender = false
|
||||
|
||||
const renderCache = new KeyV<string>({
|
||||
store: storage,
|
||||
namespace: 'markdoc-rss',
|
||||
})
|
||||
|
||||
const _render = async (content: string) => {
|
||||
const key = 'html:' + createHash('sha256').update(content).digest('hex')
|
||||
const cached = await renderCache.get(key)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const blogTree = Markdoc.transform(Markdoc.parse(content))
|
||||
const html = Markdoc.renderers.html(blogTree)
|
||||
|
||||
await renderCache.set(key, html, 1000 * 60 * 60 * 24)
|
||||
return html
|
||||
}
|
||||
|
||||
const _getBlog = async (blogId: number) => {
|
||||
const key = `raw:blog-${blogId}`
|
||||
const cached = await renderCache.get(key)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const content = (await getBlog(blogId))?.content
|
||||
if (content) {
|
||||
await renderCache.set(key, content, 1000 * 60 * 60 * 12)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
export const GET = (async (context) => {
|
||||
let blogs: ListBlogItemType[] = []
|
||||
let pid = 0
|
||||
|
||||
let newBlogs = []
|
||||
|
||||
while (pid == 0 || newBlogs.length > 0) {
|
||||
newBlogs = await listBlogs({
|
||||
page: ++pid,
|
||||
limit: 100,
|
||||
})
|
||||
blogs = [...blogs, ...newBlogs]
|
||||
}
|
||||
|
||||
const site = context.site || 'https://passthem.top'
|
||||
await ensureShikiEngine()
|
||||
|
||||
return rss({
|
||||
title: '小帕的小窝',
|
||||
description: '小帕和他朋友们的博客',
|
||||
site,
|
||||
items: await Promise.all(
|
||||
blogs.map(async (blog) => {
|
||||
const blogContent = (await _getBlog(blog.id)) || '博客内容暂不可用'
|
||||
const rssItem: RSSFeedItem = {
|
||||
title: blog.title,
|
||||
description: `一篇由 ${blog.author.nickname} 写的博客`,
|
||||
link: `${site}/blogs/${blog.id}`,
|
||||
pubDate: new Date(blog.created_at),
|
||||
content: await _render(blogContent),
|
||||
author: blog.author.nickname,
|
||||
enclosure: blog.featured_image
|
||||
? {
|
||||
url: blog.featured_image.image_url,
|
||||
length: 0,
|
||||
type: 'image/jpeg',
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
|
||||
return rssItem
|
||||
}),
|
||||
),
|
||||
customData: `<language>zh-hans</language>`,
|
||||
})
|
||||
}) satisfies APIRoute
|
||||
@ -1,5 +1,9 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
"exclude": ["dist"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user