init: 初始化仓库
This commit is contained in:
commit
b5479a885c
79
.github/workflows/build.yml
vendored
Normal file
79
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# Sample workflow for building and deploying a Next.js site to GitHub Pages
|
||||||
|
#
|
||||||
|
# To get started with Next.js see: https://nextjs.org/docs/getting-started
|
||||||
|
#
|
||||||
|
name: Deploy VitePress site to Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Runs on pushes targeting the default branch
|
||||||
|
push:
|
||||||
|
branches: ["master"]
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
|
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build job
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Detect package manager
|
||||||
|
id: detect-package-manager
|
||||||
|
run: |
|
||||||
|
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
|
||||||
|
echo "manager=yarn" >> $GITHUB_OUTPUT
|
||||||
|
echo "command=install" >> $GITHUB_OUTPUT
|
||||||
|
echo "runner=yarn" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
elif [ -f "${{ github.workspace }}/package.json" ]; then
|
||||||
|
echo "manager=npm" >> $GITHUB_OUTPUT
|
||||||
|
echo "command=ci" >> $GITHUB_OUTPUT
|
||||||
|
echo "runner=npx --no-install" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Unable to determine package manager"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: ${{ steps.detect-package-manager.outputs.manager }}
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
- name: Install dependencies
|
||||||
|
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
|
||||||
|
- name: Add Packages
|
||||||
|
run: ${{ steps.detect-package-manager.outputs.runner }} add vitepress vitepress-theme-sakura sass
|
||||||
|
- name: Build with Next.js
|
||||||
|
run: ${{ steps.detect-package-manager.outputs.runner }} build
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: .vitepress/dist
|
||||||
|
|
||||||
|
# Deployment job
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
115
.gitignore
vendored
Normal file
115
.gitignore
vendored
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
### OSX ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# pnpm link folder
|
||||||
|
pnpm-global
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# rollup.js default build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# vitepress build output
|
||||||
|
.vitepress/dist
|
||||||
|
.vitepress/cache
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
TODOs.md
|
||||||
|
src/api/index.json
|
||||||
|
src/examples/data.json
|
||||||
|
src/tutorial/data.json
|
||||||
|
draft.md
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.idea
|
||||||
44
.vitepress/config.ts
Normal file
44
.vitepress/config.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { defineConfigWithTheme } from 'vitepress'
|
||||||
|
import fixKatex from './fix-katex'
|
||||||
|
export interface ThemeConfig {
|
||||||
|
name?: string,
|
||||||
|
cover?: string,
|
||||||
|
hello?: string,
|
||||||
|
motto?: string,
|
||||||
|
social?: { icon: string, url: string }[],
|
||||||
|
waline?: string,
|
||||||
|
}
|
||||||
|
export default defineConfigWithTheme<ThemeConfig>({
|
||||||
|
lang: 'zh-CN',
|
||||||
|
base: '/vitepress-theme-sakura/',
|
||||||
|
// from https://codybontecou.com/tailwindcss-with-vitepress.html
|
||||||
|
head: [
|
||||||
|
// 字体支持
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/regular.min.css' }],
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css' }],
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Noto+Serif+SC' }],
|
||||||
|
// waline
|
||||||
|
['script', { src: 'https://cdn.jsdelivr.net/npm/@waline/client@1.5.4/dist/Waline.min.js' }],
|
||||||
|
// katex
|
||||||
|
['script', { src: 'https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.js' }],
|
||||||
|
['script', { src: 'https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/contrib/auto-render.min.js' }],
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css' }],
|
||||||
|
],
|
||||||
|
markdown: {
|
||||||
|
theme: 'github-light',
|
||||||
|
lineNumbers: true,
|
||||||
|
config: md => {
|
||||||
|
md.use(fixKatex)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
themeConfig: {
|
||||||
|
name: 'flaribbit',
|
||||||
|
cover: '/vitepress-theme-sakura/60651947_p0.jpg',
|
||||||
|
social: [
|
||||||
|
{ icon: 'fa-github', url: 'https://github.com' },
|
||||||
|
{ icon: 'fa-twitter', url: 'https://twitter.com' },
|
||||||
|
{ icon: 'fa-weibo', url: 'https://weibo.com' },
|
||||||
|
],
|
||||||
|
waline: 'https://blog-waline-e7jqcxb9s-flaribbit.vercel.app/',
|
||||||
|
}
|
||||||
|
})
|
||||||
195
.vitepress/fix-katex.js
Normal file
195
.vitepress/fix-katex.js
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
// from https://github.com/waylonflinn/markdown-it-katex
|
||||||
|
/*
|
||||||
|
Like markdown-it-simplemath, this is a stripped down, simplified version of:
|
||||||
|
https://github.com/runarberg/markdown-it-math
|
||||||
|
It differs in that it takes (a subset of) LaTeX as input and relies on KaTeX
|
||||||
|
for rendering output.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Test if potential opening or closing delimieter
|
||||||
|
// Assumes that there is a "$" at state.src[pos]
|
||||||
|
function isValidDelim(state, pos) {
|
||||||
|
var prevChar,
|
||||||
|
nextChar,
|
||||||
|
max = state.posMax,
|
||||||
|
can_open = true,
|
||||||
|
can_close = true;
|
||||||
|
|
||||||
|
prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1;
|
||||||
|
nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1;
|
||||||
|
|
||||||
|
// Check non-whitespace conditions for opening and closing, and
|
||||||
|
// check that closing delimeter isn't followed by a number
|
||||||
|
if (
|
||||||
|
prevChar === 0x20 /* " " */ ||
|
||||||
|
prevChar === 0x09 /* \t */ ||
|
||||||
|
(nextChar >= 0x30 /* "0" */ && nextChar <= 0x39) /* "9" */
|
||||||
|
) {
|
||||||
|
can_close = false;
|
||||||
|
}
|
||||||
|
if (nextChar === 0x20 /* " " */ || nextChar === 0x09 /* \t */) {
|
||||||
|
can_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
can_open: can_open,
|
||||||
|
can_close: can_close,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function math_inline(state, silent) {
|
||||||
|
var start, match, token, res, pos, esc_count;
|
||||||
|
|
||||||
|
if (state.src[state.pos] !== "$") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = isValidDelim(state, state.pos);
|
||||||
|
if (!res.can_open) {
|
||||||
|
if (!silent) {
|
||||||
|
state.pending += "$";
|
||||||
|
}
|
||||||
|
state.pos += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check for and bypass all properly escaped delimieters
|
||||||
|
// This loop will assume that the first leading backtick can not
|
||||||
|
// be the first character in state.src, which is known since
|
||||||
|
// we have found an opening delimieter already.
|
||||||
|
start = state.pos + 1;
|
||||||
|
match = start;
|
||||||
|
while ((match = state.src.indexOf("$", match)) !== -1) {
|
||||||
|
// Found potential $, look for escapes, pos will point to
|
||||||
|
// first non escape when complete
|
||||||
|
pos = match - 1;
|
||||||
|
while (state.src[pos] === "\\") {
|
||||||
|
pos -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even number of escapes, potential closing delimiter found
|
||||||
|
if ((match - pos) % 2 == 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No closing delimter found. Consume $ and continue.
|
||||||
|
if (match === -1) {
|
||||||
|
if (!silent) {
|
||||||
|
state.pending += "$";
|
||||||
|
}
|
||||||
|
state.pos = start;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have empty content, ie: $$. Do not parse.
|
||||||
|
if (match - start === 0) {
|
||||||
|
if (!silent) {
|
||||||
|
state.pending += "$$";
|
||||||
|
}
|
||||||
|
state.pos = start + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid closing delimiter
|
||||||
|
res = isValidDelim(state, match);
|
||||||
|
if (!res.can_close) {
|
||||||
|
if (!silent) {
|
||||||
|
state.pending += "$";
|
||||||
|
}
|
||||||
|
state.pos = start;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
token = state.push("math_inline", "math", 0);
|
||||||
|
token.markup = "$";
|
||||||
|
token.content = state.src.slice(start - 1, match + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.pos = match + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function math_block(state, start, end, silent) {
|
||||||
|
var firstLine,
|
||||||
|
lastLine,
|
||||||
|
next,
|
||||||
|
lastPos,
|
||||||
|
found = false,
|
||||||
|
token,
|
||||||
|
pos = state.bMarks[start] + state.tShift[start],
|
||||||
|
max = state.eMarks[start];
|
||||||
|
|
||||||
|
if (pos + 2 > max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (state.src.slice(pos, pos + 2) !== "$$") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pos += 2;
|
||||||
|
firstLine = state.src.slice(pos, max);
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (firstLine.trim().slice(-2) === "$$") {
|
||||||
|
// Single line expression
|
||||||
|
firstLine = firstLine.trim();
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (next = start; !found; ) {
|
||||||
|
next++;
|
||||||
|
|
||||||
|
if (next >= end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = state.bMarks[next] + state.tShift[next];
|
||||||
|
max = state.eMarks[next];
|
||||||
|
|
||||||
|
if (pos < max && state.tShift[next] < state.blkIndent) {
|
||||||
|
// non-empty line with negative indent should stop the list:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.src.slice(pos, max).trim().slice(-2) === "$$") {
|
||||||
|
lastPos = state.src.slice(0, max).lastIndexOf("$$") + 2;
|
||||||
|
lastLine = state.src.slice(pos, lastPos);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.line = next + 1;
|
||||||
|
|
||||||
|
token = state.push("math_block", "math", 0);
|
||||||
|
token.block = true;
|
||||||
|
token.content =
|
||||||
|
(firstLine && firstLine.trim() ? firstLine + "\n" : "") +
|
||||||
|
state.getLines(start + 1, next, state.tShift[start], true) +
|
||||||
|
(lastLine && lastLine.trim() ? lastLine : "");
|
||||||
|
token.map = [start, state.line];
|
||||||
|
token.markup = "$$";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (md) {
|
||||||
|
var fixKatex = function (latex) {
|
||||||
|
return latex.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
};
|
||||||
|
var inlineRenderer = function (tokens, idx) {
|
||||||
|
return fixKatex(tokens[idx].content);
|
||||||
|
};
|
||||||
|
var blockRenderer = function (tokens, idx) {
|
||||||
|
return fixKatex(tokens[idx].content);
|
||||||
|
};
|
||||||
|
md.inline.ruler.after("escape", "math_inline", math_inline);
|
||||||
|
md.block.ruler.after("blockquote", "math_block", math_block, {
|
||||||
|
alt: ["paragraph", "reference", "blockquote", "list"],
|
||||||
|
});
|
||||||
|
md.renderer.rules.math_inline = inlineRenderer;
|
||||||
|
md.renderer.rules.math_block = blockRenderer;
|
||||||
|
}
|
||||||
51
.vitepress/genFeed.mjs
Normal file
51
.vitepress/genFeed.mjs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { Feed } from 'feed'
|
||||||
|
import postsData from './posts.data.mjs'
|
||||||
|
import { resolveSiteData } from 'vitepress'
|
||||||
|
const url = `https://blog.vuejs.org`
|
||||||
|
|
||||||
|
genFeed()
|
||||||
|
|
||||||
|
async function genFeed() {
|
||||||
|
const siteData = await resolveSiteData('.')
|
||||||
|
const posts = await postsData.load(true)
|
||||||
|
const cwd = process.cwd()
|
||||||
|
const feed = new Feed({
|
||||||
|
title: siteData.title,
|
||||||
|
description: siteData.description,
|
||||||
|
id: url,
|
||||||
|
link: url,
|
||||||
|
language: siteData.lang,
|
||||||
|
image: 'https://vuejs.org/images/logo.png',
|
||||||
|
favicon: `${url}/favicon.ico`,
|
||||||
|
copyright: siteData.themeConfig.name || '-',
|
||||||
|
})
|
||||||
|
|
||||||
|
posts.forEach((post) => {
|
||||||
|
const file = path.resolve(cwd, `.vitepress/dist/${post.href}`)
|
||||||
|
const rendered = fs.readFileSync(file, 'utf-8')
|
||||||
|
const content = rendered.match(
|
||||||
|
/<main>([\s\S]*)<\/main>/
|
||||||
|
)
|
||||||
|
|
||||||
|
feed.addItem({
|
||||||
|
title: post.title,
|
||||||
|
id: `${url}${post.href}`,
|
||||||
|
link: `${url}${post.href}`,
|
||||||
|
description: post.excerpt,
|
||||||
|
content: content[1],
|
||||||
|
author: [
|
||||||
|
{
|
||||||
|
name: post.data.author,
|
||||||
|
link: post.data.twitter
|
||||||
|
? `https://twitter.com/${post.data.twitter}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
],
|
||||||
|
date: post.data.date
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.writeFileSync(path.resolve(cwd, '.vitepress/dist/feed.rss'), feed.rss2())
|
||||||
|
}
|
||||||
10
.vitepress/posts.data.d.ts
vendored
Normal file
10
.vitepress/posts.data.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface PostData {
|
||||||
|
title: string,
|
||||||
|
href: string,
|
||||||
|
create: number,
|
||||||
|
update: number,
|
||||||
|
tags?: string[],
|
||||||
|
cover?: string,
|
||||||
|
excerpt: string,
|
||||||
|
}
|
||||||
|
export declare const data: PostData[]
|
||||||
70
.vitepress/posts.data.mjs
Normal file
70
.vitepress/posts.data.mjs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// from https://github.com/vuejs/blog
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import matter from 'gray-matter'
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { createMarkdownRenderer } from 'vitepress'
|
||||||
|
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const cwd = process.cwd()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
watch: path.relative(__dirname, cwd + '/posts/*.md').replace(/\\/g, '/'),
|
||||||
|
async load(asFeed = false) {
|
||||||
|
const md = await createMarkdownRenderer(cwd)
|
||||||
|
const postDir = path.join(cwd, 'posts')
|
||||||
|
checkTags()
|
||||||
|
return fs
|
||||||
|
.readdirSync(postDir)
|
||||||
|
.filter((file) => file.endsWith('.md'))
|
||||||
|
.map((file) => getPost(md, file, postDir, asFeed))
|
||||||
|
.sort((a, b) => b.create - a.create)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = new Map()
|
||||||
|
|
||||||
|
function getPost(md, file, postDir, asFeed = false) {
|
||||||
|
const fullePath = path.join(postDir, file)
|
||||||
|
const timestamp = Math.floor(fs.statSync(fullePath).mtimeMs)
|
||||||
|
|
||||||
|
const cached = cache.get(fullePath)
|
||||||
|
if (cached && timestamp === cached.timestamp) {
|
||||||
|
return cached.post
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = fs.readFileSync(fullePath, 'utf-8')
|
||||||
|
const { data, excerpt } = matter(src, { excerpt: true })
|
||||||
|
|
||||||
|
const post = {
|
||||||
|
title: data.title,
|
||||||
|
href: `posts/${file.replace(/\.md$/, '.html')}`,
|
||||||
|
create: +new Date(data.date) || timestamp,
|
||||||
|
update: timestamp,
|
||||||
|
tags: data.tags,
|
||||||
|
cover: data.cover,
|
||||||
|
excerpt: md.render(excerpt)
|
||||||
|
}
|
||||||
|
if (asFeed) {
|
||||||
|
// only attach these when building the RSS feed to avoid bloating the
|
||||||
|
// client bundle size
|
||||||
|
post.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.set(fullePath, {
|
||||||
|
timestamp,
|
||||||
|
post
|
||||||
|
})
|
||||||
|
return post
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTags() {
|
||||||
|
const dir = path.join(cwd, 'tags')
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
console.log('Creating page: /tags')
|
||||||
|
fs.mkdirSync(dir)
|
||||||
|
fs.writeFileSync('tags/index.md', '---\ntitle: 标签\n---\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
396
.vitepress/theme/Article.vue
Normal file
396
.vitepress/theme/Article.vue
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
<template>
|
||||||
|
<div class="abanner" :style="cover" v-if="index >= 0">
|
||||||
|
<div class="titlebox">
|
||||||
|
<h1 class="title">{{ title }}</h1>
|
||||||
|
<div class="info">{{ author }} · 更新于 {{ date }} · {{ view }} 次阅读</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="article">
|
||||||
|
<Content class="content" />
|
||||||
|
<div class="content nav">
|
||||||
|
<span>
|
||||||
|
<a :href="nav[0].href" v-if="nav[0].show">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
{{ nav[0].text }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<a :href="nav[1].href" v-if="nav[1].show">
|
||||||
|
{{ nav[1].text }}
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Waline v-if="index != -1" ref="waline" />
|
||||||
|
<TOC :data="data.page.value.headers" :active="active" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
declare const renderMathInElement: any;
|
||||||
|
declare const katex: any;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useData, useRoute } from 'vitepress'
|
||||||
|
import { onMounted, onUnmounted, ref, reactive, watch, nextTick } from 'vue'
|
||||||
|
import { data as posts } from '../posts.data'
|
||||||
|
import { throttleAndDebounce } from './utils'
|
||||||
|
import Waline from './Waline.vue'
|
||||||
|
import TOC from './TOC.vue'
|
||||||
|
|
||||||
|
const data = useData()
|
||||||
|
const base = data.site.value.base
|
||||||
|
const route = useRoute()
|
||||||
|
const title = ref('')
|
||||||
|
const author = data.theme.value.name
|
||||||
|
const date = ref('')
|
||||||
|
const view = ref(0)
|
||||||
|
const cover = ref('')
|
||||||
|
const active = ref(0)
|
||||||
|
const waline = ref<InstanceType<typeof Waline>>()
|
||||||
|
const nav = reactive([
|
||||||
|
{ href: '', text: '', show: true },
|
||||||
|
{ href: '', text: '', show: true },
|
||||||
|
])
|
||||||
|
|
||||||
|
const index = ref(0)
|
||||||
|
const update = () => {
|
||||||
|
index.value = posts.findIndex(p => p.href == route.path.replace(base, ''))
|
||||||
|
if (index.value == -1) return
|
||||||
|
title.value = data.page.value.title
|
||||||
|
cover.value = `background-image: url(${data.page.value.frontmatter.cover || data.theme.value.cover})`
|
||||||
|
date.value = new Date(data.page.value.lastUpdated || posts[index.value].create).toLocaleDateString('sv-SE')
|
||||||
|
waline.value?.update()
|
||||||
|
let ival = index.value
|
||||||
|
if (ival - 1 >= 0) {
|
||||||
|
nav[0].href = base + posts[ival - 1].href
|
||||||
|
nav[0].text = posts[ival - 1].title
|
||||||
|
nav[0].show = true
|
||||||
|
} else {
|
||||||
|
nav[0].show = false
|
||||||
|
}
|
||||||
|
if (ival + 1 < posts.length) {
|
||||||
|
nav[1].href = base + posts[ival + 1].href
|
||||||
|
nav[1].text = posts[ival + 1].title
|
||||||
|
nav[1].show = true
|
||||||
|
} else {
|
||||||
|
nav[1].show = false
|
||||||
|
}
|
||||||
|
// web only, not support in SSR
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
nextTick().then(() => {
|
||||||
|
updateKatex()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
watch(route, update)
|
||||||
|
|
||||||
|
const setActiveLink = () => {
|
||||||
|
const headers = data.page.value.headers
|
||||||
|
if (headers.length == 0) return
|
||||||
|
for (let i = 0; i < headers.length; i++) {
|
||||||
|
const el = document.getElementById(headers[i].slug)
|
||||||
|
const rect = el?.getBoundingClientRect()!
|
||||||
|
if (rect.top > 200) {
|
||||||
|
let hash = ' '
|
||||||
|
if (i > 0) {
|
||||||
|
active.value = i - 1
|
||||||
|
hash = '#' + headers[i - 1].slug
|
||||||
|
}
|
||||||
|
history.replaceState(null, document.title, hash)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active.value = headers.length - 1
|
||||||
|
history.replaceState(null, document.title, '#' + headers[headers.length - 1].slug)
|
||||||
|
}
|
||||||
|
const onScroll = throttleAndDebounce(setActiveLink, 300)
|
||||||
|
const updateKatex = () => {
|
||||||
|
if (typeof renderMathInElement === 'undefined') return
|
||||||
|
const el = document.querySelector('.article .content')
|
||||||
|
if (!el) return
|
||||||
|
renderMathInElement(el, {
|
||||||
|
delimiters: [
|
||||||
|
{ left: '$$', right: '$$', display: true },
|
||||||
|
{ left: '$', right: '$', display: false },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
setActiveLink()
|
||||||
|
window.addEventListener('scroll', onScroll)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
let el = document.querySelector<HTMLScriptElement>('script[src*="auto-render"]')
|
||||||
|
if (el) el.onload = () => updateKatex()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('scroll', onScroll)
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.abanner {
|
||||||
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
margin-top: 64px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.titlebox {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 20px;
|
||||||
|
text-shadow: 2px 2px 10px black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article {
|
||||||
|
position: relative;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #e58700;
|
||||||
|
position: relative;
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
|
||||||
|
&.header-anchor {
|
||||||
|
float: left;
|
||||||
|
margin-top: 0.125em;
|
||||||
|
margin-left: -0.87em;
|
||||||
|
padding-right: 0.23em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
transform-origin: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
transform: scaleX(0);
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
transform-origin: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@for $i from 1 through 6 {
|
||||||
|
h#{$i}:hover .header-anchor {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border-bottom: 1px dashed var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-block-end: 0.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.katex-display {
|
||||||
|
overflow: auto hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.abanner {
|
||||||
|
height: 200px;
|
||||||
|
|
||||||
|
.titlebox {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-block {
|
||||||
|
|
||||||
|
&.tip,
|
||||||
|
&.info,
|
||||||
|
&.warning,
|
||||||
|
&.danger {
|
||||||
|
margin: 1rem 0;
|
||||||
|
border-left: 0.5rem solid;
|
||||||
|
padding: 0.1rem 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tip {
|
||||||
|
background-color: #f3f5f7;
|
||||||
|
border-color: #3eaf7c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
background-color: #f3f5f7;
|
||||||
|
border-color: #476582;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
border-color: #e7c000;
|
||||||
|
color: #6b5900;
|
||||||
|
background-color: #fff7d0;
|
||||||
|
|
||||||
|
.custom-block-title {
|
||||||
|
color: #b29400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
border-color: #c00;
|
||||||
|
color: #4d0000;
|
||||||
|
background-color: #ffe6e6;
|
||||||
|
|
||||||
|
.custom-block-title {
|
||||||
|
color: #900000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-block-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// inline code
|
||||||
|
code {
|
||||||
|
font-size: var(--code-font-size);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
background-color: rgba(27, 31, 35, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");
|
||||||
|
--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
div[class*="language-"] {
|
||||||
|
position: relative;
|
||||||
|
line-height: var(--code-line-height);
|
||||||
|
font-size: var(--code-font-size);
|
||||||
|
font-family: var(--code-font-family);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
padding-top: 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
button.copy {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: white;
|
||||||
|
background-image: var(--vp-icon-copy);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50%;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover button.copy {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -28px);
|
||||||
|
left: 50%;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 16px;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 32px;
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 12px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fc625d;
|
||||||
|
box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers-wrapper {
|
||||||
|
padding-left: 16px;
|
||||||
|
color: var(--color-gray);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
136
.vitepress/theme/Banner.vue
Normal file
136
.vitepress/theme/Banner.vue
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div class="banner" :style="`background-image: url(${cover})`">
|
||||||
|
<div class="wave1"></div>
|
||||||
|
<div class="wave2"></div>
|
||||||
|
<div class="info">
|
||||||
|
<GlitchText :text="hello" />
|
||||||
|
<span class="box">
|
||||||
|
<p class="text">
|
||||||
|
<i class="fa fa-quote-left"></i>
|
||||||
|
{{ motto }}
|
||||||
|
<i class="fa fa-quote-right"></i>
|
||||||
|
</p>
|
||||||
|
<div class="contact">
|
||||||
|
<a :href="s.url" v-for="s in social" aria-label="icon" target="_blank">
|
||||||
|
<i :class="['fab', s.icon]"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
import GlitchText from './GlitchText.vue'
|
||||||
|
const themeConfig = useData().theme.value
|
||||||
|
const hello = themeConfig.hello || 'Hello, sakura'
|
||||||
|
const motto = themeConfig.motto || 'You got to put the past behind you before you can move on.'
|
||||||
|
const social = themeConfig.social || []
|
||||||
|
const cover = themeConfig.cover
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "./base.scss";
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.wave1,
|
||||||
|
.wave2 {
|
||||||
|
position: absolute;
|
||||||
|
width: 400%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave1 {
|
||||||
|
background: url($theme-base+"assets/wave1.png") repeat-x;
|
||||||
|
height: 65px;
|
||||||
|
animation: wave-animation-1 30s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave2 {
|
||||||
|
background: url($theme-base+"assets/wave2.png") repeat-x;
|
||||||
|
height: 80px;
|
||||||
|
animation: wave-animation-2 20s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
display: inline-block;
|
||||||
|
width: 600px;
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
margin: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.banner {
|
||||||
|
.info {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin: 1em 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wave-animation-1 {
|
||||||
|
0% {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
left: -997px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wave-animation-2 {
|
||||||
|
0% {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
left: -1009px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
120
.vitepress/theme/BlogList.vue
Normal file
120
.vitepress/theme/BlogList.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bloglist">
|
||||||
|
<div class="section">
|
||||||
|
<i class="fa-solid fa-book"></i> 文章列表
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
<div class="card" v-for="p in posts">
|
||||||
|
<div class="image"></div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="date">
|
||||||
|
<i class="fa fa-clock"></i>
|
||||||
|
发布于 {{ new Date(p.create).toLocaleDateString('sv-SE') }}
|
||||||
|
</div>
|
||||||
|
<a :href="base + p.href">
|
||||||
|
<div class="title">{{ p.title }}</div>
|
||||||
|
</a>
|
||||||
|
<!-- <div class="view">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
{{ 114514 }} 阅读
|
||||||
|
</div>-->
|
||||||
|
<div class="content" v-html="p.excerpt"></div>
|
||||||
|
<div v-if="click" class="tags">
|
||||||
|
<a v-for="t in p.tags" href="#" @click="click(t)">
|
||||||
|
<i class="fa fa-tag"></i>
|
||||||
|
{{ t }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-else class="tags">
|
||||||
|
<a v-for="t in p.tags" :href="`${base}tags/?q=${t}`">
|
||||||
|
<i class="fa fa-tag"></i>
|
||||||
|
{{ t }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type PostData } from '../posts.data'
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
const base = useData().site.value.base
|
||||||
|
const { posts, click = null } = defineProps<{
|
||||||
|
posts: PostData[]
|
||||||
|
click?: (tag: string) => void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.bloglist {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date,
|
||||||
|
.view,
|
||||||
|
.tags {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
color: var(--color-gray);
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 20px -6px rgba(0, 0, 0, 0.5);
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 5px 10px 5px rgb(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 20px 0;
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags a {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: var(--color-gray);
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.bloglist {
|
||||||
|
.card {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: 0 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
.vitepress/theme/GlitchText.vue
Normal file
94
.vitepress/theme/GlitchText.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div class="glitch" :data-text="text">{{ text }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { text } = defineProps<{ text: string }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@mixin glitch-common {
|
||||||
|
content: attr(data-text);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
.glitch {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 80px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: rgba(0, 0, 0, 0.2) 4px 4px 8px;
|
||||||
|
&:before {
|
||||||
|
@include glitch-common;
|
||||||
|
left: -1px;
|
||||||
|
text-shadow: 1px 0 #ff3f1a;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
@include glitch-common;
|
||||||
|
left: 1px;
|
||||||
|
text-shadow: -1px 0 #00a7e0;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
&:before {
|
||||||
|
text-shadow: 4px 0 #ff3f1a;
|
||||||
|
animation: glitch-loop-1 0.8s infinite ease-in-out alternate-reverse;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
text-shadow: -4px 0 #00a7e0;
|
||||||
|
animation: glitch-loop-2 0.8s infinite ease-in-out alternate-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.glitch {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes glitch-loop-1 {
|
||||||
|
0% {
|
||||||
|
clip: rect(36px, 9999px, 9px, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
clip: rect(25px, 9999px, 99px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
clip: rect(50px, 9999px, 102px, 0);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
clip: rect(30px, 9999px, 92px, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
clip: rect(91px, 9999px, 98px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes glitch-loop-2 {
|
||||||
|
0% {
|
||||||
|
top: -1px;
|
||||||
|
left: 1px;
|
||||||
|
clip: rect(65px, 9999px, 119px, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
top: -6px;
|
||||||
|
left: 4px;
|
||||||
|
clip: rect(79px, 9999px, 19px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
top: -3px;
|
||||||
|
left: 2px;
|
||||||
|
clip: rect(68px, 9999px, 11px, 0);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
top: 0;
|
||||||
|
left: -4px;
|
||||||
|
clip: rect(95px, 9999px, 53px, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
clip: rect(31px, 9999px, 149px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
.vitepress/theme/Header.vue
Normal file
87
.vitepress/theme/Header.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<span class="brand"></span>
|
||||||
|
<span class="container">
|
||||||
|
<span class="menu">
|
||||||
|
<ul>
|
||||||
|
<li v-for="m in menu">
|
||||||
|
<a :href="base + m.url">
|
||||||
|
<span>
|
||||||
|
<i :class="['fa', m.icon]"></i>
|
||||||
|
{{ m.name }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="other">
|
||||||
|
<a class="search">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
const base = useData().site.value.base
|
||||||
|
interface MenuItem { icon: string, name: string, url: string }
|
||||||
|
const menu: MenuItem[] = [
|
||||||
|
{ icon: 'fa-home', name: '首页', url: '' },
|
||||||
|
{ icon: 'fa-tag', name: '标签', url: 'tags/' },
|
||||||
|
{ icon: 'fa-leaf', name: '关于', url: 'readme.html' }
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
justify-self: left;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
translate: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other {
|
||||||
|
justify-self: right;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 0 12px;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-gray);
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
.vitepress/theme/Layout.vue
Normal file
79
.vitepress/theme/Layout.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<Header />
|
||||||
|
<aside />
|
||||||
|
<main>
|
||||||
|
<ToTop />
|
||||||
|
<template v-if="path === ''">
|
||||||
|
<Banner />
|
||||||
|
<BlogList :posts="posts" />
|
||||||
|
</template>
|
||||||
|
<Tag v-else-if="path === 'tags/'" />
|
||||||
|
<Article v-else />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Header from './Header.vue'
|
||||||
|
import Banner from './Banner.vue'
|
||||||
|
import Article from './Article.vue'
|
||||||
|
import BlogList from './BlogList.vue'
|
||||||
|
import Tag from './Tag.vue'
|
||||||
|
import ToTop from './ToTop.vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useData } from 'vitepress'
|
||||||
|
import { data as posts } from '../posts.data'
|
||||||
|
const base = useData().site.value.base
|
||||||
|
const route = useRoute()
|
||||||
|
const path = computed(() => route.path.replace(base, '').replace('index.html', ''))
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
--global-font: "Noto Serif SC", "MicroSoft Yahei", serif;
|
||||||
|
--color-accent: #fe9600;
|
||||||
|
--color-gray: #666;
|
||||||
|
--color-text: #02111d;
|
||||||
|
--color-background: #eee;
|
||||||
|
--color-border: #d0d7de;
|
||||||
|
--code-line-height: 24px;
|
||||||
|
--code-font-family: monospace;
|
||||||
|
--code-font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: var(--global-font);
|
||||||
|
font-size: 16px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px dashed var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--color-accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
.vitepress/theme/TOC.vue
Normal file
71
.vitepress/theme/TOC.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="toc">
|
||||||
|
<ol>
|
||||||
|
<li v-for="(h, i) in data" :class="['h' + h.level, { 'active': active === i }]">
|
||||||
|
<a :href="'#' + h.slug">{{ h.title }}</a>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type Header } from 'vitepress'
|
||||||
|
const { data = [], active = 0 } = defineProps<{
|
||||||
|
data: Header[]
|
||||||
|
active: number
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// * {
|
||||||
|
// border: 1px dashed red;
|
||||||
|
// background-color: rgba(255, 0, 0, 0.05);
|
||||||
|
// }
|
||||||
|
.toc {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -240px;
|
||||||
|
height: 100%;
|
||||||
|
width: 240px;
|
||||||
|
|
||||||
|
ol {
|
||||||
|
position: sticky;
|
||||||
|
top: 80px;
|
||||||
|
list-style: none;
|
||||||
|
padding-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
border-left: 2px solid var(--color-background);
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
color: var(--color-text);
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-left-color: var(--color-accent);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h2 {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h3 {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h4 {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.toc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
.vitepress/theme/Tag.vue
Normal file
54
.vitepress/theme/Tag.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tag">
|
||||||
|
<div class="article">
|
||||||
|
<a :class="['item', { 'active': active === tag }]" href="#" v-for="(_, tag) in tagData" @click="setTag(tag)">
|
||||||
|
<span>{{ tag }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<BlogList :posts="active ? tagData[active] : []" :click="setTag" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BlogList from './BlogList.vue'
|
||||||
|
import { data as posts, type PostData } from '../posts.data'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
const active = ref<string | null>(null)
|
||||||
|
const tagData: Record<string, PostData[]> = {}
|
||||||
|
const setTag = (tag: string) => {
|
||||||
|
active.value = tag
|
||||||
|
history.replaceState(null, document.title, '?q=' + tag)
|
||||||
|
}
|
||||||
|
for (const post of posts) {
|
||||||
|
if (!post.tags) continue
|
||||||
|
for (const tag of post.tags) {
|
||||||
|
if (!tagData[tag]) tagData[tag] = []
|
||||||
|
tagData[tag].push(post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
active.value = new URLSearchParams(location.search).get('q')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tag {
|
||||||
|
margin-top: 64px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin: 4px;
|
||||||
|
border-radius: 40px;
|
||||||
|
color: var(--color-gray);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
color: var(--color-accent);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
55
.vitepress/theme/ToTop.vue
Normal file
55
.vitepress/theme/ToTop.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<a href="#" class="totop" @click="toTop" :style="style" aria-label="to-top"></a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
const hide = 'top: -900px'
|
||||||
|
const style = ref(hide)
|
||||||
|
const onScroll = () => {
|
||||||
|
if (window.scrollY > 200) {
|
||||||
|
if (window.innerWidth > 720) {
|
||||||
|
style.value = `top: ${Math.min(window.innerHeight - 968, 0)}px`
|
||||||
|
} else {
|
||||||
|
style.value = `top: -640px`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style.value = hide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toTop = () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', onScroll)
|
||||||
|
onScroll()
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('scroll', onScroll)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "./base.scss";
|
||||||
|
.totop {
|
||||||
|
position: fixed;
|
||||||
|
width: 70px;
|
||||||
|
height: 900px;
|
||||||
|
right: 25px;
|
||||||
|
z-index: 50;
|
||||||
|
background-image: url($theme-base+"assets/scroll.png");
|
||||||
|
transition: top 0.5s ease-in-out;
|
||||||
|
animation: float 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes float {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
.vitepress/theme/Waline.vue
Normal file
49
.vitepress/theme/Waline.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div id="waline"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
declare const Waline: any;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
let waline: any = null
|
||||||
|
onMounted(() => {
|
||||||
|
const serverURL = useData().site.value.themeConfig.waline
|
||||||
|
if (!serverURL) {
|
||||||
|
console.error('未配置waline服务端地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
let el = document.querySelector<HTMLScriptElement>('script[src*="waline"]')
|
||||||
|
if (el) el.onload = () => init(serverURL)
|
||||||
|
} else {
|
||||||
|
init(serverURL)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const init = (serverURL: string) => {
|
||||||
|
waline = Waline({
|
||||||
|
el: '#waline',
|
||||||
|
serverURL: serverURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const update = () => {
|
||||||
|
waline?.update()
|
||||||
|
}
|
||||||
|
defineExpose({ update })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.v[data-class="v"] {
|
||||||
|
|
||||||
|
label,
|
||||||
|
.vinput,
|
||||||
|
.veditor,
|
||||||
|
.vbtn {
|
||||||
|
font-family: var(--global-font);
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
.vitepress/theme/assets/scroll.png
Normal file
BIN
.vitepress/theme/assets/scroll.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
.vitepress/theme/assets/wave1.png
Normal file
BIN
.vitepress/theme/assets/wave1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
BIN
.vitepress/theme/assets/wave2.png
Normal file
BIN
.vitepress/theme/assets/wave2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
1
.vitepress/theme/base.scss
Normal file
1
.vitepress/theme/base.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
$theme-base: "https://cdn.jsdelivr.net/npm/vitepress-theme-sakura/.vitepress/theme/";
|
||||||
5
.vitepress/theme/env.d.ts
vendored
Normal file
5
.vitepress/theme/env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
11
.vitepress/theme/index.ts
Normal file
11
.vitepress/theme/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Layout from './Layout.vue'
|
||||||
|
import { type EnhanceAppContext } from 'vitepress'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Layout,
|
||||||
|
NotFound: () => 'custom 404', // <- this is a Vue 3 functional component
|
||||||
|
enhanceApp({ app, router, siteData }: EnhanceAppContext) {
|
||||||
|
// app is the Vue 3 app instance from `createApp()`. router is VitePress'
|
||||||
|
// custom router. `siteData`` is a `ref`` of current site-level metadata.
|
||||||
|
}
|
||||||
|
}
|
||||||
18
.vitepress/theme/utils.ts
Normal file
18
.vitepress/theme/utils.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export function throttleAndDebounce(fn: () => void, delay: number): () => void {
|
||||||
|
let timeout: number
|
||||||
|
let called = false
|
||||||
|
return () => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
if (!called) {
|
||||||
|
fn()
|
||||||
|
called = true
|
||||||
|
setTimeout(() => {
|
||||||
|
called = false
|
||||||
|
}, delay)
|
||||||
|
} else {
|
||||||
|
timeout = setTimeout(fn, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
build/index.js
Normal file
25
build/index.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const vue = require('@vitejs/plugin-vue')
|
||||||
|
const { build } = require('vite')
|
||||||
|
const externals = ['vue', 'vitepress', '../posts.data']
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await build({
|
||||||
|
plugins: [vue()],
|
||||||
|
build: {
|
||||||
|
outDir: './dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
lib: {
|
||||||
|
formats: ['es'],
|
||||||
|
entry: './build/lib.js',
|
||||||
|
fileName: (format) => `lib.js`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: externals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fs.copyFileSync('./build/lib.d.ts', './dist/lib.d.ts')
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
5
build/lib.d.ts
vendored
Normal file
5
build/lib.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { DefineComponent } from 'vue'
|
||||||
|
|
||||||
|
export { ThemeConfig } from '../.vitepress/config'
|
||||||
|
export const Layout: DefineComponent<{}>
|
||||||
|
export const fixKatex: (md: any) => void
|
||||||
1
build/lib.js
Normal file
1
build/lib.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Layout } from '../.vitepress/theme/Layout.vue'
|
||||||
48
package.json
Normal file
48
package.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "vitepress-theme-sakura",
|
||||||
|
"version": "0.5.1",
|
||||||
|
"description": "vitepress-theme-sakura",
|
||||||
|
"type": "module",
|
||||||
|
"main": ".vitepress/theme/Layout.vue",
|
||||||
|
"author": "flaribbit",
|
||||||
|
"license": "ISC",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vitepress dev",
|
||||||
|
"build": "vitepress build && node .vitepress/genFeed.mjs",
|
||||||
|
"serve": "vitepress serve",
|
||||||
|
"lib:build": "node ./build",
|
||||||
|
"preview": "vitepress serve"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"feed": "^4.2.2",
|
||||||
|
"gray-matter": "^4.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
|
"sass": "^1.69.7",
|
||||||
|
"vite": "^5.0.11",
|
||||||
|
"vitepress": "^1.0.0-rc.36",
|
||||||
|
"vue": "^3.4.13"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "git+https://github.com/flaribbit/vitepress-theme-sakura.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/flaribbit/vitepress-theme-sakura/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/flaribbit/vitepress-theme-sakura#readme",
|
||||||
|
"keywords": [
|
||||||
|
"vitepress",
|
||||||
|
"theme",
|
||||||
|
"sakura"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".vitepress/fix-katex.js",
|
||||||
|
".vitepress/genFeed.mjs",
|
||||||
|
".vitepress/posts.data.mjs",
|
||||||
|
".vitepress/theme"
|
||||||
|
]
|
||||||
|
}
|
||||||
12
posts/comming-soon.md
Normal file
12
posts/comming-soon.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: 本页面的 github 仓库链接
|
||||||
|
date: 2022-02-11
|
||||||
|
---
|
||||||
|
|
||||||
|
[点击这里](https://github.com/flaribbit/vitepress-theme-sakura)
|
||||||
|
|
||||||
|
还在摸鱼中,当前预览不代表最终效果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
啊你居然点进来,可惜没有更多内容了
|
||||||
58
posts/doc1.md
Normal file
58
posts/doc1.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: What is VitePress?
|
||||||
|
date: 2022-01-01
|
||||||
|
tags: [vue, vue3, vitepress]
|
||||||
|
---
|
||||||
|
|
||||||
|
VitePress is [VuePress](https://vuepress.vuejs.org/)' little brother, built on top of [Vite](https://github.com/vitejs/vite).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
::: warning WARNING
|
||||||
|
VitePress is currently in 0.x status. It is already suitable for out-of-the-box documentation use, but the config and theming API may still change between minor releases.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
We love VuePress v1, but being built on top of Webpack, the time it takes to spin up the dev server for a simple doc site with a few pages is just becoming unbearable. Even HMR updates can take up to seconds to reflect in the browser!
|
||||||
|
|
||||||
|
Fundamentally, this is because VuePress v1 is a Webpack app under the hood. Even with just two pages, it's a full on Webpack project (including all the theme source files) being compiled. It gets even worse when the project has many pages – every page must first be fully compiled before the server can even display anything!
|
||||||
|
|
||||||
|
Incidentally, Vite solves these problems really well: nearly instant server start, an on-demand compilation that only compiles the page being served, and lightning-fast HMR. Plus, there are a few additional design issues I have noted in VuePress v1 over time but never had the time to fix due to the amount of refactoring it would require.
|
||||||
|
|
||||||
|
Now, with Vite and Vue 3, it is time to rethink what a "Vue-powered static site generator" can really be.
|
||||||
|
|
||||||
|
## Improvements Over VuePress v1
|
||||||
|
|
||||||
|
There're couple of things that are improved from VuePress v1....
|
||||||
|
|
||||||
|
### It Uses Vue 3
|
||||||
|
|
||||||
|
Leverages Vue 3's improved template static analysis to stringify static content as much as possible. Static content is sent as string literals instead of JavaScript render function code – the JS payload is therefore _much_ cheaper to parse, and hydration also becomes faster.
|
||||||
|
|
||||||
|
Note the optimization is applied while still allowing the user to freely mix Vue components inside markdown content – the compiler does the static/dynamic separation for you automatically and you never need to think about it.
|
||||||
|
|
||||||
|
### It Uses Vite Under The Hood
|
||||||
|
|
||||||
|
- Faster dev server start
|
||||||
|
- Faster hot updates
|
||||||
|
- Faster build (uses Rollup internally)
|
||||||
|
|
||||||
|
### Lighter Page Weight
|
||||||
|
|
||||||
|
- Vue 3 tree-shaking + Rollup code splitting
|
||||||
|
- Does not ship metadata for every page on every request. This decouples page weight from total number of pages. Only the current page's metadata is sent. Client side navigation fetches the new page's component and metadata together.
|
||||||
|
- Does not use `vue-router` because the need of VitePress is very simple and specific - a simple custom router (under 200 LOC) is used instead.
|
||||||
|
- (WIP) i18n locale data should also be fetched on demand.
|
||||||
|
|
||||||
|
## Other Differences
|
||||||
|
|
||||||
|
VitePress is more opinionated and less configurable: VitePress aims to scale back the complexity in the current VuePress and restart from its minimalist roots.
|
||||||
|
|
||||||
|
VitePress is future oriented: VitePress only targets browsers that support native ES module imports. It encourages the use of native JavaScript without transpilation, and CSS variables for theming.
|
||||||
|
|
||||||
|
## Will This Become The Next VuePress in The Future?
|
||||||
|
|
||||||
|
We already have [vuepress-next](https://github.com/vuepress/vuepress-next), which would be the next major version of VuePress. It also makes lots of improvements over VuePress v1, and also supports Vite now.
|
||||||
|
|
||||||
|
VitePress is not compatible with the current VuePress ecosystem (mostly themes and plugins). The overall idea is that VitePress will have a drastically more minimal theming API (preferring JavaScript APIs instead of file layout conventions) and likely no plugins (all customization is done in themes).
|
||||||
60
posts/doc2.md
Normal file
60
posts/doc2.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: 测试文档
|
||||||
|
tags: [测试, 算法, 测试1, 测试2, 测试3, 测试4, 测试5, 测试6, 测试7]
|
||||||
|
cover: /vitepress-theme-sakura/60651947_p0.jpg
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码测试
|
||||||
|
|
||||||
|
二分查找 `binsearch`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
//[l, r)
|
||||||
|
function search(nums: number[], target: number): number {
|
||||||
|
let l = 0, r = nums.length;
|
||||||
|
while (l < r) {
|
||||||
|
let c = (l + r) >> 1;
|
||||||
|
if (nums[c] > target) {
|
||||||
|
r = c;
|
||||||
|
} else if (nums[c] < target) {
|
||||||
|
l = c + 1;
|
||||||
|
} else {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2 级标题
|
||||||
|
|
||||||
|
### 3 级标题
|
||||||
|
|
||||||
|
#### 4 级标题
|
||||||
|
|
||||||
|
有序列表
|
||||||
|
1. First item
|
||||||
|
2. Second item
|
||||||
|
3. Third item
|
||||||
|
4. Fourth item
|
||||||
|
|
||||||
|
无序列表
|
||||||
|
- First item
|
||||||
|
- Second item
|
||||||
|
- Third item
|
||||||
|
- Fourth item
|
||||||
|
|
||||||
|
## katex 测试
|
||||||
|
|
||||||
|
正态分布的概率密度函数均值为 $\mu$ 方差为 $\sigma^2$ (或标准差 $\sigma$ ),是高斯函数的一个实例:
|
||||||
|
|
||||||
|
$$f(x;\mu,\sigma)=\frac{1}{\sigma\sqrt{2\pi}} \exp \left( -\frac{(x- \mu)^2}{2\sigma^2} \right)$$
|
||||||
|
|
||||||
|
如果一个随机变量 $X$ 服从这个分布,我们写作 $X \sim N(\mu, \sigma^2)$ 。如果 $\mu =0$ 并且 $\sigma =1$ ,这个分布被称为标准正态分布,这个分布能够简化为
|
||||||
|
$$f(x) = \frac{1}{\sqrt{2\pi}} \exp\left(-\frac{x^2}{2} \right)$$
|
||||||
|
|
||||||
|
累积分布函数是指随机变量 $X$ 小于或等于 $x$ 的概率,用概率密度函数表示为
|
||||||
|
$$F(x;\mu,\sigma)=\frac{1}{\sigma\sqrt{2\pi}}\int_{-\infty}^x \exp \left( -\frac{(t - \mu)^2}{2\sigma^2}\ \right) dt$$
|
||||||
|
|
||||||
|
正态分布的累积分布函数能够由一个叫做误差函数的特殊函数表示:
|
||||||
|
$$\Phi (z)={\frac 12}\left[1+\operatorname {erf}\left({\frac {z-\mu }{\sigma {\sqrt 2}}}\right)\right]$$
|
||||||
BIN
preview.webp
Normal file
BIN
preview.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
BIN
public/60651947_p0.jpg
Normal file
BIN
public/60651947_p0.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 547 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
174
readme.md
Normal file
174
readme.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# vitepress-theme-sakura
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/vitepress-theme-sakura)
|
||||||
|
[](https://github.com/flaribbit/vitepress-theme-sakura/actions)
|
||||||
|
|
||||||
|
可爱又轻量的二次元博客主题!大概是隔壁wp主题的移植吧
|
||||||
|
|
||||||
|
[在线预览](https://flaribbit.github.io/vitepress-theme-sakura/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
安装依赖,推荐使用 pnpm 包管理器,如果没有的话根据自己的情况把命令中的 `pnpm` 换成 `npm` 或者 `yarn`。
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm add vitepress vitepress-theme-sakura sass
|
||||||
|
```
|
||||||
|
|
||||||
|
创建配置文件
|
||||||
|
|
||||||
|
`.vitepress/config.ts`
|
||||||
|
```ts
|
||||||
|
import { type ThemeConfig } from 'vitepress-theme-sakura/.vitepress/config'
|
||||||
|
import { defineConfigWithTheme } from 'vitepress'
|
||||||
|
import fixKatex from 'vitepress-theme-sakura/.vitepress/fix-katex'
|
||||||
|
export default defineConfigWithTheme<ThemeConfig>({
|
||||||
|
lang: 'zh-CN',
|
||||||
|
title: 'xxx的小站',
|
||||||
|
head: [
|
||||||
|
// 字体支持
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/regular.min.css' }],
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css' }],
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Noto+Serif+SC' }],
|
||||||
|
// waline
|
||||||
|
['script', { src: 'https://cdn.jsdelivr.net/npm/@waline/client@1.5.4/dist/Waline.min.js' }],
|
||||||
|
// katex
|
||||||
|
['script', { src: 'https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.js' }],
|
||||||
|
['script', { src: 'https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/contrib/auto-render.min.js' }],
|
||||||
|
['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css' }],
|
||||||
|
],
|
||||||
|
markdown: {
|
||||||
|
theme: 'github-light',
|
||||||
|
lineNumbers: true,
|
||||||
|
config: md => { md.use(fixKatex) },
|
||||||
|
},
|
||||||
|
themeConfig: {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `themeConfig` 里面的内容就是要手动填写的主题配置。
|
||||||
|
|
||||||
|
`.vitepress/theme/index.ts`
|
||||||
|
```ts
|
||||||
|
import { type EnhanceAppContext } from 'vitepress'
|
||||||
|
// @ts-ignore
|
||||||
|
import Layout from 'vitepress-theme-sakura'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Layout,
|
||||||
|
NotFound: () => 'custom 404', // <- this is a Vue 3 functional component
|
||||||
|
enhanceApp({ app, router, siteData }: EnhanceAppContext) {
|
||||||
|
// app is the Vue 3 app instance from `createApp()`. router is VitePress'
|
||||||
|
// custom router. `siteData`` is a `ref`` of current site-level metadata.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后创建 `posts` 目录,编写 markdown 文件保存到 `posts` 目录中。
|
||||||
|
|
||||||
|
执行 `vitepress dev` 命令,以开发模式预览效果;
|
||||||
|
|
||||||
|
执行 `vitepress build` 命令,构建用于发布的网页文件,输出到 `.vitepress/dist` 目录中。
|
||||||
|
|
||||||
|
## 主题配置
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ThemeConfig {
|
||||||
|
name?: string, // 名字,用于显示文章作者等
|
||||||
|
cover?: string, // 首页封面图
|
||||||
|
hello?: string, // 首页问候语
|
||||||
|
motto?: string, // 首页签名
|
||||||
|
social?: { icon: string, url: string }[], // 联系方式
|
||||||
|
waline?: string, // waline 评论系统服务端地址
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
代码块主题配置,参见 [shiki 主题](https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes)
|
||||||
|
```ts
|
||||||
|
export type Theme =
|
||||||
|
| 'css-variables'
|
||||||
|
| 'dark-plus'
|
||||||
|
| 'dracula-soft'
|
||||||
|
| 'dracula'
|
||||||
|
| 'github-dark-dimmed'
|
||||||
|
| 'github-dark'
|
||||||
|
| 'github-light'
|
||||||
|
| 'hc_light'
|
||||||
|
| 'light-plus'
|
||||||
|
| 'material-darker'
|
||||||
|
| 'material-default'
|
||||||
|
| 'material-lighter'
|
||||||
|
| 'material-ocean'
|
||||||
|
| 'material-palenight'
|
||||||
|
| 'min-dark'
|
||||||
|
| 'min-light'
|
||||||
|
| 'monokai'
|
||||||
|
| 'nord'
|
||||||
|
| 'one-dark-pro'
|
||||||
|
| 'poimandres'
|
||||||
|
| 'rose-pine-dawn'
|
||||||
|
| 'rose-pine-moon'
|
||||||
|
| 'rose-pine'
|
||||||
|
| 'slack-dark'
|
||||||
|
| 'slack-ochin'
|
||||||
|
| 'solarized-dark'
|
||||||
|
| 'solarized-light'
|
||||||
|
| 'vitesse-dark'
|
||||||
|
| 'vitesse-light'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文章配置
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
title: 标题
|
||||||
|
date: 日期
|
||||||
|
cover: 封面图
|
||||||
|
tags: [标签1, 标签2]
|
||||||
|
---
|
||||||
|
|
||||||
|
这里的文字会作为预览显示在文章列表中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
这里的文字点进文章才能看到
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
- [x] 首页横幅
|
||||||
|
- [x] 文章列表
|
||||||
|
- [x] 文章样式
|
||||||
|
- [x] waline 评论
|
||||||
|
- [x] 文章导航
|
||||||
|
- [x] 代码高亮
|
||||||
|
- [x] 手机端适配
|
||||||
|
- [x] 目录
|
||||||
|
- [x] 目录锚点双向同步
|
||||||
|
- [x] 标签页面
|
||||||
|
- [x] latex 公式
|
||||||
|
- [x] RSS
|
||||||
|
- [ ] 页脚
|
||||||
|
- [ ] 搜索
|
||||||
|
- [ ] PWA
|
||||||
|
|
||||||
|
## 其他说明
|
||||||
|
如你所见,这个博客主题完成度不是很高。
|
||||||
|
|
||||||
|
我写这个主题的初衷有两个,一是为了学习前端,二是看到隔壁 wordpress 主题的性能太烂了,所以就想着移植一下。
|
||||||
|
|
||||||
|
如果你想安安静静写博客,需要体验友好、性能绝佳、扩展性出色的博客系统,推荐看一下 [Valaxy](https://valaxy.site/).
|
||||||
|
|
||||||
|
如果你希望学习自制博客所需要的前端技术,但目前是小白,那或许可以参考本项目代码。本项目代码质量不高,但毕竟也是小白写的,所以应该比较容易看懂。如果不是小白,则可以参考 [云猫猫的 Valaxy 仓库](https://github.com/YunYouJun/valaxy)。
|
||||||
|
|
||||||
|
## 感谢
|
||||||
|
- [wordpress-theme-sakura](https://github.com/mashirozx/sakura)
|
||||||
|
- [hexo-theme-yun](https://github.com/YunYouJun/hexo-theme-yun)
|
||||||
|
- [vuejs/blog](https://github.com/vuejs/blog)
|
||||||
|
- [wordpress-theme-Sakurairo](https://github.com/mirai-mamori/Sakurairo)
|
||||||
|
- [xiaowai-api](https://api.ixiaowai.cn/)
|
||||||
|
|
||||||
|
## 不感谢
|
||||||
|
- 某个关闭图床的网站
|
||||||
5
tags/index.md
Normal file
5
tags/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 标签
|
||||||
|
---
|
||||||
|
|
||||||
|
标签页面还没有完成
|
||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"types": [
|
||||||
|
"vite/client"
|
||||||
|
],
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
".vitepress/theme/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user