init: 初始化仓库

This commit is contained in:
MKStoler1024 2025-03-28 23:22:10 +08:00
commit b5479a885c
39 changed files with 3581 additions and 0 deletions

79
.github/workflows/build.yml vendored Normal file
View 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
View 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
View 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
View 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, "&lt;").replace(/>/g, "&gt;");
};
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
View 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
View 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
View 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')
}
}

View 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
View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
export { default as Layout } from '../.vitepress/theme/Layout.vue'

5
index.md Normal file
View File

@ -0,0 +1,5 @@
---
home: true
---
index

48
package.json Normal file
View 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
View File

@ -0,0 +1,12 @@
---
title: 本页面的 github 仓库链接
date: 2022-02-11
---
[点击这里](https://github.com/flaribbit/vitepress-theme-sakura)
还在摸鱼中,当前预览不代表最终效果
---
啊你居然点进来,可惜没有更多内容了

58
posts/doc1.md Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
public/60651947_p0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

174
readme.md Normal file
View File

@ -0,0 +1,174 @@
# vitepress-theme-sakura
[![npm](https://img.shields.io/npm/v/vitepress-theme-sakura?logo=npm)](https://www.npmjs.com/package/vitepress-theme-sakura)
[![Build](https://github.com/flaribbit/vitepress-theme-sakura/actions/workflows/build.yml/badge.svg)](https://github.com/flaribbit/vitepress-theme-sakura/actions)
可爱又轻量的二次元博客主题大概是隔壁wp主题的移植吧
[在线预览](https://flaribbit.github.io/vitepress-theme-sakura/)
![preview](./preview.webp)
## 快速开始
安装依赖,推荐使用 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
View File

@ -0,0 +1,5 @@
---
title: 标签
---
标签页面还没有完成

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"types": [
"vite/client"
],
"moduleResolution": "node"
},
"include": [
".vitepress/theme/**/*"
]
}

1433
yarn.lock Normal file

File diff suppressed because it is too large Load Diff