Init
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
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
|
||||
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
128
.vitepress/config.mts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { defineConfigWithTheme } from 'vitepress'
|
||||
// @ts-ignore
|
||||
import mdItCustomAttrs from 'markdown-it-custom-attrs'
|
||||
export interface ThemeConfig {
|
||||
//navBar
|
||||
menuList: { name: string; url: string }[]
|
||||
|
||||
//banner
|
||||
videoBanner: boolean
|
||||
name: string
|
||||
welcomeText: string
|
||||
motto: string[]
|
||||
social: { icon: string; url: string }[]
|
||||
|
||||
//spine
|
||||
spineVoiceLang: 'zh' | 'jp'
|
||||
|
||||
//footer
|
||||
footerName: string
|
||||
poweredList: { name: string; url: string }[]
|
||||
|
||||
//gitalk
|
||||
clientID: string
|
||||
clientSecret: string
|
||||
repo: string
|
||||
owner: string
|
||||
admin: string[]
|
||||
}
|
||||
|
||||
export default defineConfigWithTheme<ThemeConfig>({
|
||||
lang: 'zh-CN',
|
||||
head: [
|
||||
['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
|
||||
// gitalk
|
||||
['link', { rel: 'stylesheet', href: 'https://unpkg.com/gitalk/dist/gitalk.css' }],
|
||||
['script', { src: 'https://unpkg.com/gitalk/dist/gitalk.min.js' }],
|
||||
// bluearchive font
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: '/font/Blueaka/Blueaka.css',
|
||||
},
|
||||
],
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: '/font/Blueaka_Bold/Blueaka_Bold.css',
|
||||
},
|
||||
],
|
||||
// 图片灯箱
|
||||
[
|
||||
'link',
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: 'https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.css',
|
||||
},
|
||||
],
|
||||
[
|
||||
'script',
|
||||
{
|
||||
src: 'https://cdn.jsdelivr.net/npm/@fancyapps/ui@4.0/dist/fancybox.umd.js',
|
||||
},
|
||||
],
|
||||
],
|
||||
ignoreDeadLinks: true,
|
||||
// 生成站点地图
|
||||
sitemap: {
|
||||
hostname: 'https://home.gtuantuan.online:9443',
|
||||
},
|
||||
title: "TuanTuan's 部落格",
|
||||
description: "TuanTuan's 部落格",
|
||||
themeConfig: {
|
||||
// navBar
|
||||
menuList: [
|
||||
{ name: '首页', url: '' },
|
||||
{ name: '标签', url: 'tags/' },
|
||||
],
|
||||
|
||||
//banner区配置
|
||||
videoBanner: false,
|
||||
name: "TuanTuan's 部落格",
|
||||
welcomeText: 'Hello World',
|
||||
motto: ['与你的日常,就是奇迹!', '邦邦咔邦!'],
|
||||
social: [
|
||||
{ icon: 'github', url: 'https://github.com/' },
|
||||
{ icon: 'bilibili', url: 'https://www.bilibili.com/' },
|
||||
{ icon: 'qq', url: 'https://im.qq.com/index/' },
|
||||
{ icon: 'wechat', url: 'https://weixin.qq.com/' },
|
||||
],
|
||||
|
||||
|
||||
|
||||
//spine语音配置,可选zh/jp
|
||||
spineVoiceLang: 'jp',
|
||||
|
||||
//footer配置
|
||||
footerName: 'TuanTuan',
|
||||
poweredList: [
|
||||
{ name: 'VitePress', url: 'https://github.com/vuejs/vitepress' },
|
||||
{ name: 'GitHub Pages', url: 'https://docs.github.com/zh/pages' },
|
||||
],
|
||||
// ↓↓↓ 在这里添加 Gitalk 配置 ↓↓↓
|
||||
clientID: 'Ov23liDGj2h0vONdtdBr',
|
||||
clientSecret: '74b862f6d47dadb44ad0f6f057146363eff1570d',
|
||||
repo: 'gtuantuan-comments',
|
||||
owner: 'GTuanTuan',
|
||||
admin: ['GTuanTuan'],
|
||||
// //gitalk配置
|
||||
// clientID: 'Ov23lia9U9wFN3WMyoKK',
|
||||
// clientSecret: 'b2418ab598c188c43a247c99e728dd2735d58c3b',
|
||||
// repo: 'vitepress-theme-bluearchive',
|
||||
// owner: 'Alittfre',
|
||||
// admin: ['Alittfre'],
|
||||
},
|
||||
markdown: {
|
||||
theme: 'solarized-dark',
|
||||
lineNumbers: true,
|
||||
math: true,
|
||||
config: (md) => {
|
||||
// use more markdown-it plugins!
|
||||
md.use(mdItCustomAttrs, 'image', {
|
||||
'data-fancybox': 'gallery',
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
122
.vitepress/theme/Layout.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<Splash></Splash>
|
||||
<template v-if="!page.isNotFound">
|
||||
<main style="min-height: 100vh">
|
||||
<Navbar></Navbar>
|
||||
<Banner>
|
||||
<transition name="fade" mode="out-in">
|
||||
<WelcomeBox v-if="!state.splashLoading && page.filePath === 'index.md'"></WelcomeBox>
|
||||
<Tags v-else-if="page.filePath === 'tags/index.md'"></Tags>
|
||||
<PostInnerBanner v-else></PostInnerBanner>
|
||||
</transition>
|
||||
</Banner>
|
||||
<transition name="fade" mode="out-in">
|
||||
<PostsList
|
||||
v-if="page.filePath === 'index.md' || page.filePath === 'tags/index.md'"
|
||||
></PostsList>
|
||||
<PostViewer v-else></PostViewer>
|
||||
</transition>
|
||||
</main>
|
||||
<Footer></Footer>
|
||||
<Fireworks v-if="state.fireworksEnabled"></Fireworks>
|
||||
<ClientOnly><SpinePlayer></SpinePlayer></ClientOnly>
|
||||
<ToTop></ToTop>
|
||||
<!-- 背景音乐元素 -->
|
||||
<audio id="background-music" loop>
|
||||
<source src="./assets/banner/bgm.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
</template>
|
||||
<NotFound v-else></NotFound>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 组件导入
|
||||
import Splash from './components/Splash.vue'
|
||||
import Navbar from './components/Navbar/index.vue'
|
||||
import Banner from './components/Banner.vue'
|
||||
import WelcomeBox from './components/Welcome-Box.vue'
|
||||
import PostsList from './components/Posts-List.vue'
|
||||
import Tags from './components/Tags.vue'
|
||||
import PostViewer from './components/Post-Viewer.vue'
|
||||
import PostInnerBanner from './components/Post-InnerBanner.vue'
|
||||
import NotFound from './components/NotFound.vue'
|
||||
import ToTop from './components/ToTop.vue'
|
||||
import Fireworks from './components/Fireworks.vue'
|
||||
import Footer from './components/Footer.vue'
|
||||
// @ts-ignore
|
||||
import SpinePlayer from './components/Spine-Player/index.vue'
|
||||
// 路径切换
|
||||
import { useData } from 'vitepress'
|
||||
const { page } = useData()
|
||||
|
||||
import { useStore } from './store'
|
||||
const { state } = useStore()
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body {
|
||||
background-image: var(--theme-background-image);
|
||||
background-color: var(--general-background-color);
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
color: var(--font-color-grey);
|
||||
font-family: 'Blueaka', sans-serif;
|
||||
transition: background-image 0.5s, background-color 0.5s;
|
||||
}
|
||||
|
||||
:root[theme='light'] {
|
||||
--theme-background-image: url('./assets/background.svg');
|
||||
}
|
||||
|
||||
:root[theme='dark'] {
|
||||
--theme-background-image: url('./assets/background_dark.svg');
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
background: var(--color-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
.vitepress/theme/assets/NotFound.webp
Normal file
|
After Width: | Height: | Size: 37 KiB |
144
.vitepress/theme/assets/background.svg
Normal file
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_1" data-name=" 图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 446 349.5">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f0f9ff;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9, .cls-10, .cls-11, .cls-12, .cls-13, .cls-14, .cls-15, .cls-16, .cls-17 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #dceff9;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: url(#_未命名的渐变_2);
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #e6f6ff;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #f7fafc;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: rgba(234, 235, 237, .5);
|
||||
}
|
||||
|
||||
.cls-7, .cls-16 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: #e4f2f9;
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: rgba(245, 251, 255, .5);
|
||||
}
|
||||
|
||||
.cls-18 {
|
||||
clip-path: url(#clippath);
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: #f1fafe;
|
||||
}
|
||||
|
||||
.cls-19 {
|
||||
mask: url(#mask);
|
||||
}
|
||||
|
||||
.cls-11, .cls-16 {
|
||||
fill: #b4d0e4;
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: #edf5f9;
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: #eaebed;
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #edf8fe;
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #e8f6ff;
|
||||
}
|
||||
|
||||
.cls-17 {
|
||||
fill: #f4fbff;
|
||||
}
|
||||
</style>
|
||||
<clipPath id="clippath">
|
||||
<rect class="cls-3" width="446" height="349.5"/>
|
||||
</clipPath>
|
||||
<linearGradient id="_未命名的渐变_2" data-name="未命名的渐变 2" x1="314.07" y1="308.15" x2="137.29" y2="49.2" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".7" stop-color="#231815"/>
|
||||
</linearGradient>
|
||||
<mask id="mask" x="0" y="0" width="446" height="349.5" maskUnits="userSpaceOnUse">
|
||||
<rect class="cls-4" width="446" height="349.5"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g class="cls-18">
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-2" d="M377,349.5h38.5l-19.4-28.5-19.1,28.5Z"/>
|
||||
<path class="cls-7" d="M206,253.5l-4.6-6.7-4.4,6.7h9Z"/>
|
||||
<g class="cls-19">
|
||||
<g>
|
||||
<path class="cls-5" d="M206,253.5h135l-36.8-53.5-32.6-47.5-65.6,101Z"/>
|
||||
<path class="cls-2" d="M350,253.5l-65,96h92l19.1-28.5-46.1-67.5Z"/>
|
||||
<path class="cls-15" d="M423.5,146.5l-73.5,107,46.1,67.5,49.9-74.5v-66l-22.5-34Z"/>
|
||||
<path class="cls-8" d="M446,324.5v-78l-49.9,74.5,19.4,28.5h14l16.5-25Z"/>
|
||||
<path class="cls-1" d="M446,349.5v-25l-16.5,25h16.5Z"/>
|
||||
<path class="cls-14" d="M267.5,146.5h-134l67.9,100.3,66.1-100.3Z"/>
|
||||
<path class="cls-9" d="M175.2,253.5h21.8l4.4-6.7-67.9-100.3-16,23.5,57.7,83.5Z"/>
|
||||
<path class="cls-10" d="M281.5,146.5h-6l33,47.4,32.6-47.4h-59.6Z"/>
|
||||
<path class="cls-10" d="M350,253.5l73.5-107h-82.4l-32.6,47.4,41.5,59.6Z"/>
|
||||
<path class="cls-12" d="M419.2,33L396.5,0l-24.5,39.3h42.9l4.3-6.3h0Z"/>
|
||||
<path class="cls-12" d="M423.5,39.3l-4.3-6.3-4.3,6.3h8.6,0Z"/>
|
||||
<path class="cls-17" d="M74.5,39.3l71.5,107.2,65.6-100.5-4.6-6.7H74.5Z"/>
|
||||
<path class="cls-17" d="M216,39.3h-9l4.6,6.7,4.4-6.7Z"/>
|
||||
<path class="cls-6" d="M353.5,39.3h-137.5l-4.4,6.7,69.9,100.5s72-107.2,72-107.2Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-13" d="M0,0l27.1,39.3h37.9L37,0H0Z"/>
|
||||
<path class="cls-13" d="M65,39.3L90.5,0h-53.5l28,39.3Z"/>
|
||||
<path class="cls-13" d="M117.5,170l16-23.5h12.5L74.5,39.3h132.5L183,0h-92.5l-25.5,39.3H27.1l90.3,130.7h.1Z"/>
|
||||
<path class="cls-13" d="M207,39.3h9L243.5,0h-60.5l24,39.3Z"/>
|
||||
<path class="cls-13" d="M216,39.3h156L338,0h-94.5l-27.5,39.3Z"/>
|
||||
<path class="cls-13" d="M267.5,146.5h14l-69.9-100.5-65.6,100.5h121.5Z"/>
|
||||
<path class="cls-13" d="M353.5,39.3l-72,107.2h59.6l73.8-107.2h-61.4Z"/>
|
||||
<path class="cls-13" d="M372,39.3L396.5,0h-58.5l34,39.3Z"/>
|
||||
<path class="cls-13" d="M446,180.5v-108.5l-22.5-32.7h-8.6l-73.8,107.2h82.4l22.5,34h0Z"/>
|
||||
<path class="cls-13" d="M241.5,349.5h43.5l65-96-41.5-59.6-4.2,6.1,36.8,53.5h-165.8l66.3,96h-.1Z"/>
|
||||
<path class="cls-13" d="M206,253.5l65.6-101-4.1-6-66.1,100.3,4.6,6.7Z"/>
|
||||
<path class="cls-13" d="M308.5,193.9l-33-47.4-3.9,6,32.6,47.5,4.2-6.1h.1Z"/>
|
||||
<path class="cls-13" d="M275.5,146.5h-8l4.1,6,3.9-6Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-16" d="M445.6,94.7c-49.2,0-89.1,39.4-90.1,88.3v4.3c1.3,48.6,41.1,87.6,90,87.6s90.1-40.3,90.1-90.1-40.3-90.1-90.1-90.1h.1ZM352,184.8v-1.8c.9-50.9,42.5-91.9,93.6-91.9s93.6,41.9,93.6,93.6-41.9,93.6-93.6,93.6-92.3-40.6-93.6-91.1v-2.5.1Z"/>
|
||||
<path class="cls-16" d="M445.6,107.5c-42.7,0-77.3,34.6-77.3,77.3s34.6,77.3,77.3,77.3,77.3-34.6,77.3-77.3-34.6-77.3-77.3-77.3h0ZM366.9,184.8c0-43.5,35.2-78.7,78.7-78.7s78.7,35.2,78.7,78.7-35.2,78.7-78.7,78.7-78.7-35.2-78.7-78.7h0Z"/>
|
||||
<path class="cls-16" d="M445.6,120.3c-35.6,0-64.5,28.9-64.5,64.5s28.9,64.5,64.5,64.5,64.5-28.9,64.5-64.5-28.9-64.5-64.5-64.5ZM371.2,184.8c0-41.1,33.3-74.5,74.5-74.5s74.5,33.3,74.5,74.5-33.3,74.5-74.5,74.5-74.5-33.3-74.5-74.5h0Z"/>
|
||||
<path class="cls-16" d="M446,341.2c-1.1-3.2-2.1-10.3-2.1-13.1v-109.6c0-12.9-9.8-31.2-26.2-31.2h-125.9c-2.8,0-7.4-1.1-10.6-2.1,3.2-1.1,7.8-2.1,10.6-2.1h125.9c12.9,0,26.2-17.6,26.2-32.6V50.1h4.3v100.4c0,15,13.3,32.6,26.2,32.6h125.9c2.8,0,7.4,1.1,10.6,2.1-3.2,1.1-11.3,2.1-14.2,2.1h-122.4c-16.5,0-26.2,18.3-26.2,31.2v109.6c0,2.8-1.1,9.9-2.1,13.1h0ZM448.1,47.9v-23.8c0-2.8-1.1-9.9-2.1-13.1-1.1,3.2-2.1,10.3-2.1,13.1v23.8h4.3-.1ZM455.6,184.1c0,5.5-4.4,9.9-9.9,9.9s-9.9-4.4-9.9-9.9,4.4-9.9,9.9-9.9,9.9,4.4,9.9,9.9Z"/>
|
||||
<path class="cls-16" d="M445.6,87.6c57.6,0,104.3-9.4,104.3-20.9s-46.7-20.9-104.3-20.9-104.3,9.4-104.3,20.9,46.7,20.9,104.3,20.9h0ZM446,84.8c44.9,0,81.2-8.1,81.2-18.1s-36.4-18.1-81.2-18.1-81.2,8.1-81.2,18.1,36.4,18.1,81.2,18.1Z"/>
|
||||
<path class="cls-11" d="M400.6,142.6l3.5-3.5,8.9,11.7-.4.4-12.1-8.5.1-.1Z"/>
|
||||
<path class="cls-11" d="M400.3,227l3.5,3.5,8.9-11.7-.4-.4-12.1,8.5.1.1Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
144
.vitepress/theme/assets/background_dark.svg
Normal file
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_1" data-name=" 图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 446 349.5">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #1f1f2c; /* 深色背景 */
|
||||
}
|
||||
|
||||
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7, .cls-8, .cls-9, .cls-10, .cls-11, .cls-12, .cls-13, .cls-14, .cls-15, .cls-16, .cls-17 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #2a2a3a; /* 深紫灰色 */
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: url(#_未命名的渐变_2);
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #252536; /* 深紫色 */
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #1a1a28; /* 更深的紫黑色 */
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: rgba(30, 30, 40, .5); /* 半透明深色 */
|
||||
}
|
||||
|
||||
.cls-7, .cls-16 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: #232334; /* 深蓝紫色 */
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: rgba(25, 25, 35, .5); /* 半透明深色 */
|
||||
}
|
||||
|
||||
.cls-18 {
|
||||
clip-path: url(#clippath);
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: #1f1f2c; /* 深色背景 */
|
||||
}
|
||||
|
||||
.cls-19 {
|
||||
mask: url(#mask);
|
||||
}
|
||||
|
||||
.cls-11, .cls-16 {
|
||||
fill: #353552; /* 深蓝色 */
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: #212132; /* 深紫色 */
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: #1c1c2a; /* 深色背景 */
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #202030; /* 深蓝紫色 */
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #242436; /* 深紫色 */
|
||||
}
|
||||
|
||||
.cls-17 {
|
||||
fill: #1e1e2e; /* 深色背景 */
|
||||
}
|
||||
</style>
|
||||
<clipPath id="clippath">
|
||||
<rect class="cls-3" width="446" height="349.5"/>
|
||||
</clipPath>
|
||||
<linearGradient id="_未命名的渐变_2" data-name="未命名的渐变 2" x1="314.07" y1="308.15" x2="137.29" y2="49.2" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".7" stop-color="#231815"/>
|
||||
</linearGradient>
|
||||
<mask id="mask" x="0" y="0" width="446" height="349.5" maskUnits="userSpaceOnUse">
|
||||
<rect class="cls-4" width="446" height="349.5"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g class="cls-18">
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-2" d="M377,349.5h38.5l-19.4-28.5-19.1,28.5Z"/>
|
||||
<path class="cls-7" d="M206,253.5l-4.6-6.7-4.4,6.7h9Z"/>
|
||||
<g class="cls-19">
|
||||
<g>
|
||||
<path class="cls-5" d="M206,253.5h135l-36.8-53.5-32.6-47.5-65.6,101Z"/>
|
||||
<path class="cls-2" d="M350,253.5l-65,96h92l19.1-28.5-46.1-67.5Z"/>
|
||||
<path class="cls-15" d="M423.5,146.5l-73.5,107,46.1,67.5,49.9-74.5v-66l-22.5-34Z"/>
|
||||
<path class="cls-8" d="M446,324.5v-78l-49.9,74.5,19.4,28.5h14l16.5-25Z"/>
|
||||
<path class="cls-1" d="M446,349.5v-25l-16.5,25h16.5Z"/>
|
||||
<path class="cls-14" d="M267.5,146.5h-134l67.9,100.3,66.1-100.3Z"/>
|
||||
<path class="cls-9" d="M175.2,253.5h21.8l4.4-6.7-67.9-100.3-16,23.5,57.7,83.5Z"/>
|
||||
<path class="cls-10" d="M281.5,146.5h-6l33,47.4,32.6-47.4h-59.6Z"/>
|
||||
<path class="cls-10" d="M350,253.5l73.5-107h-82.4l-32.6,47.4,41.5,59.6Z"/>
|
||||
<path class="cls-12" d="M419.2,33L396.5,0l-24.5,39.3h42.9l4.3-6.3h0Z"/>
|
||||
<path class="cls-12" d="M423.5,39.3l-4.3-6.3-4.3,6.3h8.6,0Z"/>
|
||||
<path class="cls-17" d="M74.5,39.3l71.5,107.2,65.6-100.5-4.6-6.7H74.5Z"/>
|
||||
<path class="cls-17" d="M216,39.3h-9l4.6,6.7,4.4-6.7Z"/>
|
||||
<path class="cls-6" d="M353.5,39.3h-137.5l-4.4,6.7,69.9,100.5s72-107.2,72-107.2Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-13" d="M0,0l27.1,39.3h37.9L37,0H0Z"/>
|
||||
<path class="cls-13" d="M65,39.3L90.5,0h-53.5l28,39.3Z"/>
|
||||
<path class="cls-13" d="M117.5,170l16-23.5h12.5L74.5,39.3h132.5L183,0h-92.5l-25.5,39.3H27.1l90.3,130.7h.1Z"/>
|
||||
<path class="cls-13" d="M207,39.3h9L243.5,0h-60.5l24,39.3Z"/>
|
||||
<path class="cls-13" d="M216,39.3h156L338,0h-94.5l-27.5,39.3Z"/>
|
||||
<path class="cls-13" d="M267.5,146.5h14l-69.9-100.5-65.6,100.5h121.5Z"/>
|
||||
<path class="cls-13" d="M353.5,39.3l-72,107.2h59.6l73.8-107.2h-61.4Z"/>
|
||||
<path class="cls-13" d="M372,39.3L396.5,0h-58.5l34,39.3Z"/>
|
||||
<path class="cls-13" d="M446,180.5v-108.5l-22.5-32.7h-8.6l-73.8,107.2h82.4l22.5,34h0Z"/>
|
||||
<path class="cls-13" d="M241.5,349.5h43.5l65-96-41.5-59.6-4.2,6.1,36.8,53.5h-165.8l66.3,96h-.1Z"/>
|
||||
<path class="cls-13" d="M206,253.5l65.6-101-4.1-6-66.1,100.3,4.6,6.7Z"/>
|
||||
<path class="cls-13" d="M308.5,193.9l-33-47.4-3.9,6,32.6,47.5,4.2-6.1h.1Z"/>
|
||||
<path class="cls-13" d="M275.5,146.5h-8l4.1,6,3.9-6Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-16" d="M445.6,94.7c-49.2,0-89.1,39.4-90.1,88.3v4.3c1.3,48.6,41.1,87.6,90,87.6s90.1-40.3,90.1-90.1-40.3-90.1-90.1-90.1h.1ZM352,184.8v-1.8c.9-50.9,42.5-91.9,93.6-91.9s93.6,41.9,93.6,93.6-41.9,93.6-93.6,93.6-92.3-40.6-93.6-91.1v-2.5.1Z"/>
|
||||
<path class="cls-16" d="M445.6,107.5c-42.7,0-77.3,34.6-77.3,77.3s34.6,77.3,77.3,77.3,77.3-34.6,77.3-77.3-34.6-77.3-77.3-77.3h0ZM366.9,184.8c0-43.5,35.2-78.7,78.7-78.7s78.7,35.2,78.7,78.7-35.2,78.7-78.7,78.7-78.7-35.2-78.7-78.7h0Z"/>
|
||||
<path class="cls-16" d="M445.6,120.3c-35.6,0-64.5,28.9-64.5,64.5s28.9,64.5,64.5,64.5,64.5-28.9,64.5-64.5-28.9-64.5-64.5-64.5ZM371.2,184.8c0-41.1,33.3-74.5,74.5-74.5s74.5,33.3,74.5,74.5-33.3,74.5-74.5,74.5-74.5-33.3-74.5-74.5h0Z"/>
|
||||
<path class="cls-16" d="M446,341.2c-1.1-3.2-2.1-10.3-2.1-13.1v-109.6c0-12.9-9.8-31.2-26.2-31.2h-125.9c-2.8,0-7.4-1.1-10.6-2.1,3.2-1.1,7.8-2.1,10.6-2.1h125.9c12.9,0,26.2-17.6,26.2-32.6V50.1h4.3v100.4c0,15,13.3,32.6,26.2,32.6h125.9c2.8,0,7.4,1.1,10.6,2.1-3.2,1.1-11.3,2.1-14.2,2.1h-122.4c-16.5,0-26.2,18.3-26.2,31.2v109.6c0,2.8-1.1,9.9-2.1,13.1h0ZM448.1,47.9v-23.8c0-2.8-1.1-9.9-2.1-13.1-1.1,3.2-2.1,10.3-2.1,13.1v23.8h4.3-.1ZM455.6,184.1c0,5.5-4.4,9.9-9.9,9.9s-9.9-4.4-9.9-9.9,4.4-9.9,9.9-9.9,9.9,4.4,9.9,9.9Z"/>
|
||||
<path class="cls-16" d="M445.6,87.6c57.6,0,104.3-9.4,104.3-20.9s-46.7-20.9-104.3-20.9-104.3,9.4-104.3,20.9,46.7,20.9,104.3,20.9h0ZM446,84.8c44.9,0,81.2-8.1,81.2-18.1s-36.4-18.1-81.2-18.1-81.2,8.1-81.2,18.1,36.4,18.1,81.2,18.1Z"/>
|
||||
<path class="cls-11" d="M400.6,142.6l3.5-3.5,8.9,11.7-.4.4-12.1-8.5.1-.1Z"/>
|
||||
<path class="cls-11" d="M400.3,227l3.5,3.5,8.9-11.7-.4-.4-12.1,8.5.1.1Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
BIN
.vitepress/theme/assets/banner/avatar.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
.vitepress/theme/assets/banner/banner.webp
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
.vitepress/theme/assets/banner/banner_dark.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
.vitepress/theme/assets/banner/banner_video.mp4
Normal file
BIN
.vitepress/theme/assets/banner/bgm.mp3
Normal file
18
.vitepress/theme/assets/icon/footLogo.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="487" height="177" viewBox="0 0 487 177" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.5 86.5L0.5 154.5H20L71.5 86.5H55L17 138L21.5 86.5H4.5Z" fill="#128AFA"/>
|
||||
<path d="M73.5 102.5L53 154.5H67.5L87.5 102.5H73.5Z" fill="#128AFA"/>
|
||||
<path d="M79.5 88.5L75.5 98H90L94 88.5H79.5Z" fill="#128AFA"/>
|
||||
<path d="M117 88.5L111.5 102.5H99.5L95 114.5H106L95 143.5C94.3333 147.167 94.3 154.5 99.5 154.5H118.5L124 142.5H113.5C112.167 142.5 109.9 141.8 111.5 139C113.1 136.2 118.5 122.167 121 115.5H135.5L141 102.5H126.5L132 88.5H117Z" fill="#128AFA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M150 134.5H187L195.5 114.5C196.833 110.833 197.5 103.3 189.5 102.5C181.5 101.7 166.167 102.167 159.5 102.5C156.333 102.667 148.9 105.3 144.5 114.5C140.1 123.7 135.333 135.333 133.5 140C132.5 144.833 132.8 154.5 142 154.5H180L185 142.5H150C148.667 141.333 146.8 138.1 150 134.5ZM177 123H155C153.8 119.4 157.5 115.833 159.5 114.5C164.833 114 176 113.5 178 115.5C180 117.5 178.167 121.333 177 123Z" fill="#128AFA"/>
|
||||
<path d="M158 11L190.5 26L196.5 28.5L260 57.5L196.5 177L266.5 60.5L337.5 89L344 91.5L353.5 95L344 90.5L337.5 87.5L269.5 56L278.5 38.5L279.5 36.5L296 0.5L275 34.5L274 36L263 52.5L199.5 27L193 24.5L158 11Z" fill="#128AFA"/>
|
||||
<path d="M428 102.5H391C388.5 102.667 382.6 104.9 379 112.5L374 123.5C372.5 127.333 372.2 135 383 135H399.5C401.333 136.167 403.6 139.2 398 142H366.5L361.5 154.5H401C403.167 154 408.3 151.9 411.5 147.5C414.7 143.1 416.833 136.333 417.5 133.5C418.667 129.833 419.4 122.5 413 122.5H390C388.667 120.833 387.5 117 393.5 115H422.5L428 102.5Z" fill="#2B2B2B"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M320.891 135.442H357.891L366.391 115.442C367.725 111.775 368.391 104.242 360.391 103.442C352.391 102.642 337.058 103.108 330.391 103.442C327.225 103.608 319.791 106.242 315.391 115.442C310.991 124.642 306.225 136.275 304.391 140.942C303.391 145.775 303.691 155.442 312.891 155.442H350.891L355.891 143.442H320.891C319.558 142.275 317.691 139.042 320.891 135.442ZM347.891 123.942H325.891C324.691 120.342 328.391 116.775 330.391 115.442C335.725 114.942 346.891 114.442 348.891 116.442C350.891 118.442 349.058 122.275 347.891 123.942Z" fill="#2B2B2B"/>
|
||||
<path d="M263.5 154.5L278 115.5C280.167 111.5 286 103.4 292 103C298 102.6 307.833 102.833 312 103L308 114.5L295 115.5C294 115.833 291.6 117.8 290 123C288.4 128.2 281.333 146.167 278 154.5H263.5Z" fill="#2B2B2B"/>
|
||||
<path d="M250.5 98.5L257.5 86H267C270.167 86.8333 276 90.3 274 97.5C272 104.7 266.5 117.5 264 123C262.5 126.833 256.6 134.5 245 134.5H227.5L235 121.5H246C247.667 121.333 251.3 120.2 252.5 117C253.7 113.8 255.333 109.333 256 107.5C256.667 105.833 257.6 102 256 100C254.4 98 251.667 98.1667 250.5 98.5Z" fill="#2B2B2B"/>
|
||||
<path d="M218.5 86H237.5L231.5 98.5H227.5L219.5 123L203 154.5H191.5L218.5 86Z" fill="#2B2B2B"/>
|
||||
<path d="M190.5 26C164.5 39.2 199.667 67.5 220.5 80H225.5C206.5 71 185.897 50.5 186.5 41C186.982 33.4 193.368 29.5 196.5 28.5L190.5 26Z" fill="#2B2B2B"/>
|
||||
<path d="M275 34.5C235.338 18.7587 201.934 20.6892 193 24.5L199.5 27C206.167 24 232.4 20.8 274 36L275 34.5Z" fill="#2B2B2B"/>
|
||||
<path d="M344 90.5C340.782 68.5268 299.404 45.2411 279.5 36.5L278.5 38.5C326.147 61.2911 337.49 82.0562 337.5 87.5L344 90.5Z" fill="#2B2B2B"/>
|
||||
<path d="M342.5 96L344 91.5L337.5 89C338.7 90.6 335.667 94.3333 334 96H342.5Z" fill="#2B2B2B"/>
|
||||
<path d="M486.5 103H449.5C447 103.167 441.1 105.4 437.5 113L432.5 124C431 127.833 430.7 135.5 441.5 135.5H458C459.833 136.667 462.1 139.7 456.5 142.5H425L420 155H459.5C461.667 154.5 466.8 152.4 470 148C473.2 143.6 475.333 136.833 476 134C477.167 130.333 477.9 123 471.5 123H448.5C447.167 121.333 446 117.5 452 115.5H481L486.5 103Z" fill="#2B2B2B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
82
.vitepress/theme/assets/icon/iconfont.css
Normal file
@@ -0,0 +1,82 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4542225 */
|
||||
src: url('iconfont.woff2?t=1719369583369') format('woff2'),
|
||||
url('iconfont.woff?t=1719369583369') format('woff'),
|
||||
url('iconfont.ttf?t=1719369583369') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-netease_music:before {
|
||||
content: "\e76a";
|
||||
}
|
||||
|
||||
.icon-qq:before {
|
||||
content: "\e76c";
|
||||
}
|
||||
|
||||
.icon-coolapk:before {
|
||||
content: "\E760";
|
||||
}
|
||||
|
||||
.icon-reddit:before {
|
||||
content: "\e76d";
|
||||
}
|
||||
|
||||
.icon-wechat:before {
|
||||
content: "\e775";
|
||||
}
|
||||
|
||||
.icon-arrow:before {
|
||||
content: "\e604";
|
||||
}
|
||||
|
||||
.icon-search:before {
|
||||
content: "\e602";
|
||||
}
|
||||
|
||||
.icon-tag:before {
|
||||
content: "\e885";
|
||||
}
|
||||
|
||||
.icon-bilibili:before {
|
||||
content: "\e75c";
|
||||
}
|
||||
|
||||
.icon-github:before {
|
||||
content: "\e761";
|
||||
}
|
||||
|
||||
.icon-tw:before {
|
||||
content: "\e773";
|
||||
}
|
||||
|
||||
.icon-weibo:before {
|
||||
content: "\e776";
|
||||
}
|
||||
|
||||
.icon-continue:before {
|
||||
content: "\1112";
|
||||
}
|
||||
|
||||
.icon-stop:before {
|
||||
content: "\1111";
|
||||
}
|
||||
|
||||
.icon-downarrow:before {
|
||||
content: "\E802";
|
||||
}
|
||||
|
||||
.icon-pinned:before {
|
||||
content: "\E777";
|
||||
}
|
||||
|
||||
.icon-time:before {
|
||||
content: "\E778";
|
||||
}
|
||||
BIN
.vitepress/theme/assets/icon/iconfont.ttf
Normal file
BIN
.vitepress/theme/assets/icon/iconfont.woff
Normal file
BIN
.vitepress/theme/assets/icon/iconfont.woff2
Normal file
7
.vitepress/theme/assets/icon/navLogo.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="96" height="97" viewBox="0 0 96 97" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 3L10 13L17 19.5L39.5 42L28.5 57.5L22.5 66.5L3.5 96.5L28 71L35 63.5L48.5 48.5L74 68L81 72.5L95.5 81L83.5 69L78 63.5L53.5 41L64.5 25L69.5 17.5L79.5 0L65 14L58.5 20.5L46 34L19.5 15.5L12 10.5L0 3Z" fill="#128AFA"/>
|
||||
<path d="M57.5 81.5C68.7 81.5 75 78.5 81 72.5L74 68C61.3607 79.6749 43 68.5 35 63.5L28 71C37 78.5 51.3333 81.5 57.5 81.5Z" fill="#2B2B2B"/>
|
||||
<path d="M87.5 53.5C87.5 36 75 22.5 69.5 17.5L64.5 25C83.7 43.8 81.6667 58.3333 78 63.5L83.5 69C87 64.5 87.5 56 87.5 53.5Z" fill="#2B2B2B"/>
|
||||
<path d="M34 2.5C21 2.5 15 7.5 12 10.5L19.5 15.5C33.5 4.3 51.6667 14.3333 58.5 20.5L65 14C57 8.5 47 2.5 34 2.5Z" fill="#2B2B2B"/>
|
||||
<path d="M5 30.5C5 48.9 17.5 61.5 22.5 66.5L28.5 57.5C24 52.5 11.5 40 17 19.5L10 13C6 18.6 4.66667 27.5 5 30.5Z" fill="#2B2B2B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 867 B |
16
.vitepress/theme/assets/toTop.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="87" height="77" viewBox="0 0 87 77" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.1208 1.34645L1.12084 19.1454C0.620843 20.1454 1.75825 21.3464 2.95825 21.3464C4.15825 21.3464 14.1208 9.63029 19.6209 3.64541C20.8146 2.34646 19.1208 0.146451 17.1208 1.34645Z" fill="#3A3845"/>
|
||||
<path d="M17.4583 50.3464C22.2583 51.5464 23.9542 51.6454 24.1208 51.1454C23.027 51.1454 21.5923 49.3649 20.1208 46.8472C19.402 48.0139 18.3634 47.4174 17.1208 46.6454C16.2687 46.116 13.1459 38.6742 11.9583 34.3464C10.3583 34.3464 9.45826 33.3464 8.12086 35.1454C3.95826 40.7448 11.9542 51.3121 16.1208 55.1454C15.3208 53.1454 16.6249 50.8464 17.4583 50.3464Z" fill="#7071A9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.9583 9.84644C24.9583 11.0464 21.9583 14.4049 20.9583 15.53C19.3319 17.3596 15.9583 20.53 14.9583 24.03C15.2956 31.731 15.6208 34.3465 17.1208 35.8464C17.6208 27.6454 24.7957 30.5299 25.9583 33.53C26.6732 35.3747 24.9583 40.8464 17.9583 37.3464C18.4583 43.8464 33.4583 57.63 33.4583 60.03C41.3208 69.0573 46.8337 71.389 49.9583 71.3464C51.1256 71.3305 51.9596 70.9833 52.4583 70.53C48.4583 70.13 47.4583 64.3633 47.4583 61.53C57.0583 65.13 63.1249 60.03 64.9583 57.03C62.1583 57.03 62.7916 53.6966 63.4583 52.03C69.4583 52.03 70.9583 48.53 72.4583 49.53C74.8583 47.93 70.2916 41.1966 67.4583 38.53C66.4583 37.03 66.4583 36.53 68.4583 37.53C70.4583 39.13 72.9583 40.8633 73.9583 41.53C74.4583 40.0299 72.7583 37.73 73.9583 38.53C75.1583 39.33 74.2957 35.7309 75.9583 38.53C77.4849 35.4767 71.9583 32.03 68.4583 30.53C65.6583 29.33 61.6249 25.03 59.9583 23.03L47.4583 16.03C43.1851 14.9643 34.0595 11.6931 32.9583 9.84644ZM25.4583 26.5299C23.0583 28.016 24.7916 29.8158 25.9583 30.5299C26.6249 30.5299 28.0583 30.3299 28.4583 29.5299C28.9583 28.5299 28.4583 24.6723 25.4583 26.5299Z" fill="#7071A9"/>
|
||||
<path d="M37.6208 3.14543C34.8208 2.74543 33.7875 6.31209 33.6208 8.14543C37.2875 8.97875 44.9583 12.3464 46.9583 13.8464C47.6249 14.513 48.2583 14.83 47.4583 16.03L59.9583 23.03C60.7583 19.83 55.7875 14.8121 53.6208 12.6454C54.4208 11.4454 56.9542 10.8121 58.1208 10.6454C56.9208 9.44541 54.6208 10.1454 53.6208 10.6454C47.1208 6.14541 41.1208 3.64543 37.6208 3.14543Z" fill="#7071A9"/>
|
||||
<path d="M25.9583 30.5299C24.7916 29.8158 23.0583 28.016 25.4583 26.5299C28.4583 24.6723 28.9583 28.5299 28.4583 29.5299C28.0583 30.3299 26.6249 30.5299 25.9583 30.5299Z" fill="#9CA2C9"/>
|
||||
<path d="M24.1208 51.1454C27.3208 54.7454 31.9583 58.53 33.4583 60.03C33.4583 57.63 18.4583 43.8464 17.9583 37.3464C24.9583 40.8464 26.6732 35.3747 25.9583 33.53C24.7957 30.5299 17.6208 27.6454 17.1208 35.8464C15.6208 34.3465 15.2956 31.731 14.9583 24.03C13.4583 27.03 13.4815 31.2495 14.1208 33.6454C16.2472 37.6583 19.4223 45.272 20.1208 46.8472C21.5923 49.3649 23.027 51.1454 24.1208 51.1454Z" fill="#B3B7DB"/>
|
||||
<path d="M14.1208 33.6454C12.8298 33.2634 13.0117 33.5871 11.9583 34.3464C13.1459 38.6742 16.2687 46.116 17.1208 46.6454C18.3634 47.4174 19.402 48.0139 20.1208 46.8472C19.4223 45.272 16.2472 37.6583 14.1208 33.6454Z" fill="#2C171E"/>
|
||||
<path d="M33.6208 8.14543C32.7355 8.7331 32.8713 8.88054 32.9583 9.84644C34.0595 11.6931 43.1851 14.9643 47.4583 16.03C48.2583 14.83 47.6249 14.513 46.9583 13.8464C44.9583 12.3464 37.2875 8.97875 33.6208 8.14543Z" fill="#2C171E"/>
|
||||
<path d="M75.1208 68.2716C72.6125 66.4314 69.236 65.3773 69.1209 64.6454L63.7022 69.0788C64.4238 68.8283 69.0508 71.6362 73.1208 73.3445C76.3208 71.5038 75.7875 69.1956 75.1208 68.2716Z" fill="#F4ECE7"/>
|
||||
<path d="M84.1209 58.4335C82.9677 56.7773 81.3461 54.722 79.1209 52.1454L75.1208 58.1454C76.3641 59.6218 78.274 61.7213 80.1209 63.4204C84.1209 63.6404 84.4542 60.1875 84.1209 58.4335Z" fill="#F4ECE7"/>
|
||||
<path d="M79.1209 74.1454C80.4494 72.8168 77.9655 70.3587 75.1208 68.2716C75.7875 69.1956 76.3208 71.5038 73.1208 73.3445C75.7616 74.4529 78.1678 75.0984 79.1209 74.1454Z" fill="#000105"/>
|
||||
<path d="M85.1209 66.1454C86.6524 64.9967 87.8907 63.8481 84.1209 58.4335C84.4542 60.1875 84.1209 63.6404 80.1209 63.4204C82.2352 65.3656 84.2668 66.7859 85.1209 66.1454Z" fill="#000105"/>
|
||||
<path d="M75.9583 38.53C74.2957 35.7309 75.1583 39.33 73.9583 38.53C72.7583 37.73 74.4583 40.0299 73.9583 41.53C72.9583 40.8633 70.4583 39.13 68.4583 37.53C66.4583 36.53 66.4583 37.03 67.4583 38.53C70.2916 41.1966 74.8583 47.93 72.4583 49.53C70.9583 48.53 69.4583 52.03 63.4583 52.03C62.7916 53.6966 62.1583 57.03 64.9583 57.03C63.1249 60.03 57.0583 65.13 47.4583 61.53C47.4583 64.3633 48.4583 70.13 52.4583 70.53C51.9596 70.9833 51.1256 71.3305 49.9583 71.3464C50.7879 72.7064 52.1209 75.1454 52.6209 75.6454C53.0834 76.1608 54.0398 76.0508 55.1209 75.6454C59.1208 74.1454 63.1209 70.1454 63.6209 69.1454C63.6365 69.1141 63.6638 69.0922 63.7022 69.0788L69.1209 64.6454C68.9282 63.4204 72.6208 60.1454 75.1208 58.1454L79.1209 52.1454C79.9542 50.8121 83.2209 46.4454 83.6209 45.6454C84.0209 44.8454 83.4542 43.4787 83.1209 43.1454C80.1213 40.589 79.1927 39.9473 75.9583 38.53Z" fill="#F9F8FD"/>
|
||||
<path d="M32.9583 9.84644C24.9583 11.0464 21.9583 14.4049 20.9583 15.53C19.3319 17.3596 15.9583 20.53 14.9583 24.03M32.9583 9.84644C32.8713 8.88054 32.7355 8.7331 33.6208 8.14543M32.9583 9.84644C34.0595 11.6931 43.1851 14.9643 47.4583 16.03M33.6208 8.14543C33.7875 6.31209 34.8208 2.74543 37.6208 3.14543C41.1208 3.64543 47.1208 6.14541 53.6208 10.6454C54.6208 10.1454 56.9208 9.44541 58.1208 10.6454C56.9542 10.8121 54.4208 11.4454 53.6208 12.6454C55.7875 14.8121 60.7583 19.83 59.9583 23.03M33.6208 8.14543C37.2875 8.97875 44.9583 12.3464 46.9583 13.8464C47.6249 14.513 48.2583 14.83 47.4583 16.03M59.9583 23.03C61.6249 25.03 65.6583 29.33 68.4583 30.53C71.9583 32.03 77.4849 35.4767 75.9583 38.53M59.9583 23.03L47.4583 16.03M75.9583 38.53C74.2957 35.7309 75.1583 39.33 73.9583 38.53C72.7583 37.73 74.4583 40.0299 73.9583 41.53C72.9583 40.8633 70.4583 39.13 68.4583 37.53C66.4583 36.53 66.4583 37.03 67.4583 38.53C70.2916 41.1966 74.8583 47.93 72.4583 49.53C70.9583 48.53 69.4583 52.03 63.4583 52.03C62.7916 53.6966 62.1583 57.03 64.9583 57.03C63.1249 60.03 57.0583 65.13 47.4583 61.53C47.4583 64.3633 48.4583 70.13 52.4583 70.53C51.9596 70.9833 51.1256 71.3305 49.9583 71.3464M75.9583 38.53C79.1927 39.9473 80.1213 40.589 83.1209 43.1454C83.4542 43.4787 84.0209 44.8454 83.6209 45.6454C83.2209 46.4454 79.9542 50.8121 79.1209 52.1454M33.4583 60.03C31.9583 58.53 27.3208 54.7454 24.1208 51.1454M33.4583 60.03C33.4583 57.63 18.4583 43.8464 17.9583 37.3464C24.9583 40.8464 26.6732 35.3747 25.9583 33.53C24.7957 30.5299 17.6208 27.6454 17.1208 35.8464C15.6208 34.3465 15.2956 31.731 14.9583 24.03M33.4583 60.03C41.3208 69.0573 46.8337 71.389 49.9583 71.3464M24.1208 51.1454C23.9542 51.6454 22.2583 51.5464 17.4583 50.3464C16.6249 50.8464 15.3208 53.1454 16.1208 55.1454C11.9542 51.3121 3.95826 40.7448 8.12086 35.1454C9.45826 33.3464 10.3583 34.3464 11.9583 34.3464M24.1208 51.1454C23.027 51.1454 21.5923 49.3649 20.1208 46.8472M11.9583 34.3464C13.0117 33.5871 12.8298 33.2634 14.1208 33.6454M11.9583 34.3464C13.1459 38.6742 16.2687 46.116 17.1208 46.6454C18.3634 47.4174 19.402 48.0139 20.1208 46.8472M14.1208 33.6454C16.2472 37.6583 19.4223 45.272 20.1208 46.8472M14.1208 33.6454C13.4815 31.2495 13.4583 27.03 14.9583 24.03M63.6209 69.1454C63.1209 70.1454 59.1208 74.1454 55.1209 75.6454C54.0398 76.0508 53.0834 76.1608 52.6209 75.6454C52.1209 75.1454 50.7879 72.7064 49.9583 71.3464M63.6209 69.1454C63.6365 69.1141 63.6638 69.0922 63.7022 69.0788M63.6209 69.1454L63.7022 69.0788M69.1209 64.6454C68.9282 63.4204 72.6208 60.1454 75.1208 58.1454M69.1209 64.6454C69.236 65.3773 72.6125 66.4314 75.1208 68.2716M69.1209 64.6454L63.7022 69.0788M75.1208 58.1454C76.3641 59.6218 78.274 61.7213 80.1209 63.4204M75.1208 58.1454L79.1209 52.1454M79.1209 52.1454C81.3461 54.722 82.9677 56.7773 84.1209 58.4335M75.1208 68.2716C77.9655 70.3587 80.4494 72.8168 79.1209 74.1454C78.1678 75.0984 75.7616 74.4529 73.1208 73.3445M75.1208 68.2716C75.7875 69.1956 76.3208 71.5038 73.1208 73.3445M73.1208 73.3445C69.0508 71.6362 64.4238 68.8283 63.7022 69.0788M84.1209 58.4335C87.8907 63.8481 86.6524 64.9967 85.1209 66.1454C84.2668 66.7859 82.2352 65.3656 80.1209 63.4204M84.1209 58.4335C84.4542 60.1875 84.1209 63.6404 80.1209 63.4204M17.1208 1.34645L1.12084 19.1454C0.620843 20.1454 1.75825 21.3464 2.95825 21.3464C4.15825 21.3464 14.1208 9.63029 19.6209 3.64541C20.8146 2.34646 19.1208 0.146451 17.1208 1.34645ZM25.9583 30.5299C24.7916 29.8158 23.0583 28.016 25.4583 26.5299C28.4583 24.6723 28.9583 28.5299 28.4583 29.5299C28.0583 30.3299 26.6249 30.5299 25.9583 30.5299Z" stroke="black" stroke-width="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
251
.vitepress/theme/components/Banner.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div
|
||||
class="banner"
|
||||
:class="{ postViewer: state.currPost.href, loadingComplete: !state.splashLoading }"
|
||||
>
|
||||
<slot></slot>
|
||||
|
||||
<canvas id="wave"></canvas>
|
||||
<video autoplay muted loop class="bg-video" v-if="videoBanner">
|
||||
<source src="../assets/banner/banner_video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<div class="bg-img" v-else></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
const themeConfig = useData().theme.value
|
||||
const videoBanner = themeConfig.videoBanner
|
||||
|
||||
import { useStore } from '../store'
|
||||
const { state } = useStore()
|
||||
import { onMounted } from 'vue'
|
||||
class SiriWave {
|
||||
K: number
|
||||
F: number
|
||||
speed: number
|
||||
noise: number
|
||||
phase: number
|
||||
devicePixelRatio: number
|
||||
width: number
|
||||
height: number
|
||||
MAX: number
|
||||
canvas: HTMLCanvasElement
|
||||
ctx: CanvasRenderingContext2D
|
||||
run: boolean
|
||||
animationFrameID: number | null
|
||||
|
||||
constructor() {
|
||||
this.K = 1
|
||||
this.F = 15
|
||||
this.speed = 0.1
|
||||
this.noise = 30
|
||||
this.phase = 0
|
||||
this.devicePixelRatio = window.devicePixelRatio || 1
|
||||
this.width = this.devicePixelRatio * window.innerWidth
|
||||
this.height = this.devicePixelRatio * 100
|
||||
this.MAX = this.height / 2
|
||||
this.canvas = document.getElementById('wave') as HTMLCanvasElement
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
this.canvas.style.width = this.width / this.devicePixelRatio + 'px'
|
||||
this.canvas.style.height = this.height / this.devicePixelRatio + 'px'
|
||||
this.ctx = this.canvas.getContext('2d')!
|
||||
this.run = false
|
||||
this.animationFrameID = null
|
||||
}
|
||||
|
||||
_globalAttenuationFn(x: number) {
|
||||
return Math.pow((this.K * 4) / (this.K * 4 + Math.pow(x, 4)), this.K * 2)
|
||||
}
|
||||
|
||||
_drawLine(attenuation: number, color: string, width: number, noise: number, F: number) {
|
||||
this.ctx.moveTo(0, 0)
|
||||
this.ctx.beginPath()
|
||||
this.ctx.strokeStyle = color
|
||||
this.ctx.lineWidth = width || 1
|
||||
F = F || this.F
|
||||
noise = noise * this.MAX || this.noise
|
||||
for (let i = -this.K; i <= this.K; i += 0.01) {
|
||||
i = parseFloat(i.toFixed(2))
|
||||
const x = this.width * ((i + this.K) / (this.K * 2))
|
||||
const y =
|
||||
this.height / 2 +
|
||||
noise * Math.pow(Math.sin(i * 10 * attenuation), 1) * Math.sin(F * i - this.phase)
|
||||
this.ctx.lineTo(x, y)
|
||||
}
|
||||
this.ctx.lineTo(this.width, this.height)
|
||||
this.ctx.lineTo(0, this.height)
|
||||
this.ctx.fillStyle = color
|
||||
this.ctx.fill()
|
||||
}
|
||||
|
||||
_clear() {
|
||||
this.ctx.globalCompositeOperation = 'destination-out'
|
||||
this.ctx.fillRect(0, 0, this.width, this.height)
|
||||
this.ctx.globalCompositeOperation = 'source-over'
|
||||
}
|
||||
|
||||
_draw() {
|
||||
if (!this.run) {
|
||||
return
|
||||
}
|
||||
this.phase = (this.phase + this.speed) % (Math.PI * 64)
|
||||
this._clear()
|
||||
// 获取计算后的 CSS 变量值
|
||||
const wave1Color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--wave-color1')
|
||||
.trim()
|
||||
const wave2Color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--wave-color2')
|
||||
.trim()
|
||||
|
||||
this._drawLine(0.5, wave1Color, 1, 0.35, 6)
|
||||
this._drawLine(1, wave2Color, 1, 0.25, 6)
|
||||
this.animationFrameID = requestAnimationFrame(this._draw.bind(this))
|
||||
}
|
||||
|
||||
start() {
|
||||
this.phase = 0
|
||||
this.run = true
|
||||
this._draw()
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.run = false
|
||||
this._clear()
|
||||
if (this.animationFrameID !== null) {
|
||||
cancelAnimationFrame(this.animationFrameID)
|
||||
this.animationFrameID = null
|
||||
}
|
||||
}
|
||||
|
||||
setNoise(v: number) {
|
||||
this.noise = Math.min(v, 1) * this.MAX
|
||||
}
|
||||
|
||||
setSpeed(v: number) {
|
||||
this.speed = v
|
||||
}
|
||||
|
||||
set(noise: number, speed: number) {
|
||||
this.setNoise(noise)
|
||||
this.setSpeed(speed)
|
||||
}
|
||||
}
|
||||
|
||||
let currentWave: SiriWave | null = null
|
||||
|
||||
function initAll() {
|
||||
if (currentWave) {
|
||||
currentWave.stop()
|
||||
}
|
||||
currentWave = new SiriWave()
|
||||
currentWave.setSpeed(0.01)
|
||||
currentWave.start()
|
||||
}
|
||||
|
||||
function debounce(func: () => void, wait: number) {
|
||||
let timeout: number | undefined
|
||||
return function () {
|
||||
clearTimeout(timeout)
|
||||
timeout = window.setTimeout(() => {
|
||||
func()
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initAll()
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
debounce(() => {
|
||||
if (currentWave) {
|
||||
currentWave.stop()
|
||||
}
|
||||
initAll()
|
||||
}, 100),
|
||||
)
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.banner {
|
||||
transform: translateZ(0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
mask: linear-gradient(to top, transparent, var(--general-background-color) 5%);
|
||||
perspective: 1000px;
|
||||
overflow: hidden;
|
||||
-webkit-user-drag: none;
|
||||
transition: height 0.3s;
|
||||
|
||||
&.loadingComplete {
|
||||
animation: fade-blur-in 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-fade {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-blur-in {
|
||||
from {
|
||||
filter: var(--blur-val);
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
to {
|
||||
filter: none;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.postViewer {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.bg-img {
|
||||
background-image: url(../assets/banner/banner.webp);
|
||||
html[theme='dark'] & {
|
||||
background-image: url(../assets/banner/banner_dark.webp), url(../assets/banner/banner.webp);
|
||||
}
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
filter: var(--img-brightness); /* 添加亮度过滤器 */
|
||||
transition: filter 0.5s, background-image 0.5s; /* 添加过渡效果 */
|
||||
}
|
||||
|
||||
.bg-video {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* 禁用视频拖动 */
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
#wave {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
</style>
|
||||
219
.vitepress/theme/components/Fireworks.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<canvas class="fireworks"></canvas>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, onUnmounted } from 'vue'
|
||||
import { useStore } from '../store'
|
||||
import anime from 'animejs'
|
||||
|
||||
interface MinMax {
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
interface FireworksConfig {
|
||||
colors: string[]
|
||||
numberOfParticles: number
|
||||
orbitRadius: MinMax
|
||||
circleRadius: MinMax
|
||||
diffuseRadius: MinMax
|
||||
animeDuration: MinMax
|
||||
}
|
||||
|
||||
interface Particle {
|
||||
x: number
|
||||
y: number
|
||||
color?: string
|
||||
radius?: number
|
||||
alpha?: number
|
||||
angle?: number
|
||||
lineWidth?: number
|
||||
endPos?: { x: number; y: number }
|
||||
draw?: () => void
|
||||
}
|
||||
|
||||
const { state } = useStore()
|
||||
|
||||
// 清理函数
|
||||
function cleanup() {
|
||||
document.removeEventListener('mousedown', handleMouseDown)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
// 将事件处理函数提取出来以便解绑
|
||||
let handleMouseDown: (e: MouseEvent) => void
|
||||
let handleResize: () => void
|
||||
|
||||
function createFireworks() {
|
||||
// 确保在重新创建前清理
|
||||
cleanup()
|
||||
|
||||
const lightColors = ['102, 167, 221', '62, 131, 225', '33, 78, 194']
|
||||
const darkColors = ['252, 146, 174', '202, 180, 190', '207, 198, 255']
|
||||
|
||||
const defaultConfig: FireworksConfig = {
|
||||
colors: state.darkMode === 'dark' ? darkColors : lightColors,
|
||||
numberOfParticles: 20,
|
||||
orbitRadius: { min: 50, max: 100 },
|
||||
circleRadius: { min: 10, max: 20 },
|
||||
diffuseRadius: { min: 50, max: 100 },
|
||||
animeDuration: { min: 900, max: 1500 },
|
||||
}
|
||||
|
||||
let pointerX = 0
|
||||
let pointerY = 0
|
||||
const colors = defaultConfig.colors
|
||||
|
||||
const canvasEl = document.querySelector('.fireworks') as HTMLCanvasElement
|
||||
const ctx = canvasEl.getContext('2d')!
|
||||
|
||||
function setCanvasSize(canvasEl: HTMLCanvasElement) {
|
||||
canvasEl.width = window.innerWidth
|
||||
canvasEl.height = window.innerHeight
|
||||
canvasEl.style.width = `${window.innerWidth}px`
|
||||
canvasEl.style.height = `${window.innerHeight}px`
|
||||
}
|
||||
|
||||
function updateCoords(e: MouseEvent | TouchEvent) {
|
||||
pointerX = e instanceof MouseEvent ? e.clientX : e.touches[0]?.clientX || e.changedTouches[0]?.clientX
|
||||
pointerY = e instanceof MouseEvent ? e.clientY : e.touches[0]?.clientY || e.changedTouches[0]?.clientY
|
||||
}
|
||||
|
||||
function setParticleDirection(p: Particle) {
|
||||
const angle = (anime.random(0, 360) * Math.PI) / 180
|
||||
const value = anime.random(defaultConfig.diffuseRadius.min, defaultConfig.diffuseRadius.max)
|
||||
const radius = [-1, 1][anime.random(0, 1)] * value
|
||||
return {
|
||||
x: p.x + radius * Math.cos(angle),
|
||||
y: p.y + radius * Math.sin(angle),
|
||||
}
|
||||
}
|
||||
|
||||
function createParticle(x: number, y: number): Particle {
|
||||
const p: Particle = {
|
||||
x,
|
||||
y,
|
||||
color: `rgba(${colors[anime.random(0, colors.length - 1)]},${anime.random(0.2, 0.8)})`,
|
||||
radius: anime.random(defaultConfig.circleRadius.min, defaultConfig.circleRadius.max),
|
||||
angle: anime.random(0, 360),
|
||||
endPos: setParticleDirection({ x, y }),
|
||||
draw() {
|
||||
ctx.save()
|
||||
ctx.translate(this.x, this.y)
|
||||
ctx.rotate((this.angle * Math.PI) / 180)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, -this.radius!)
|
||||
ctx.lineTo(this.radius! * Math.sin(Math.PI / 3), this.radius! * Math.cos(Math.PI / 3))
|
||||
ctx.lineTo(-this.radius! * Math.sin(Math.PI / 3), this.radius! * Math.cos(Math.PI / 3))
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = this.color!
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
},
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
function createCircle(x: number, y: number): Particle {
|
||||
const p: Particle = {
|
||||
x,
|
||||
y,
|
||||
color: state.darkMode === 'dark' ? 'rgb(233, 179, 237)' : 'rgb(106, 159, 255)',
|
||||
radius: 0.1,
|
||||
alpha: 0.5,
|
||||
lineWidth: 6,
|
||||
draw() {
|
||||
ctx.globalAlpha = this.alpha!
|
||||
ctx.beginPath()
|
||||
ctx.arc(this.x, this.y, this.radius!, 0, 2 * Math.PI, true)
|
||||
ctx.lineWidth = this.lineWidth!
|
||||
ctx.strokeStyle = this.color!
|
||||
ctx.stroke()
|
||||
ctx.globalAlpha = 1
|
||||
},
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
function renderParticle(anim: anime.AnimeInstance) {
|
||||
anim.animatables.forEach(animatable => {
|
||||
const target = animatable.target as unknown as Particle
|
||||
if (typeof target.draw === 'function') {
|
||||
target.draw()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function animateParticles(x: number, y: number) {
|
||||
const circle = createCircle(x, y)
|
||||
const particles: Particle[] = Array.from({ length: defaultConfig.numberOfParticles }, () => createParticle(x, y))
|
||||
|
||||
anime.timeline()
|
||||
.add({
|
||||
targets: particles,
|
||||
x(p: Particle) { return p.endPos!.x },
|
||||
y(p: Particle) { return p.endPos!.y },
|
||||
radius: 0,
|
||||
duration: anime.random(defaultConfig.animeDuration.min, defaultConfig.animeDuration.max),
|
||||
easing: 'easeOutExpo',
|
||||
update: renderParticle,
|
||||
})
|
||||
.add({
|
||||
targets: circle,
|
||||
radius: anime.random(defaultConfig.orbitRadius.min, defaultConfig.orbitRadius.max),
|
||||
lineWidth: 0,
|
||||
alpha: {
|
||||
value: 0,
|
||||
easing: 'linear',
|
||||
duration: anime.random(600, 800),
|
||||
},
|
||||
duration: anime.random(1200, 1800),
|
||||
easing: 'easeOutExpo',
|
||||
update: renderParticle,
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const render = anime({
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
update: () => {
|
||||
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)
|
||||
},
|
||||
})
|
||||
|
||||
handleResize = () => setCanvasSize(canvasEl)
|
||||
handleMouseDown = (e: MouseEvent) => {
|
||||
render.play()
|
||||
updateCoords(e)
|
||||
animateParticles(pointerX, pointerY)
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleMouseDown)
|
||||
window.addEventListener('resize', handleResize)
|
||||
setCanvasSize(canvasEl)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
createFireworks()
|
||||
})
|
||||
|
||||
// 监听暗色模式变化
|
||||
watch(() => state.darkMode, () => {
|
||||
createFireworks()
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fireworks {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
68
.vitepress/theme/components/Footer.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<footer class="container">
|
||||
<div class="footer-info">
|
||||
<span>© {{ new Date().getFullYear() }} {{ footerName }} </span>
|
||||
<br />
|
||||
<span
|
||||
>Powered by
|
||||
<span class="powered-list" v-for="(obj, ind) in poweredList" :key="obj.url"
|
||||
><a :href="obj.url">{{ obj.name }}</a
|
||||
>{{ ind < poweredList.length - 1 ? ' & ' : '' }}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="footer-logo">
|
||||
<img @dragstart.prevent src="../assets/icon/footLogo.svg" alt="logo-vitepress" />
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
const themeConfig = useData().theme.value
|
||||
const footerName = themeConfig.footerName
|
||||
const poweredList = themeConfig.poweredList
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 72px;
|
||||
z-index: 100;
|
||||
margin-top: 50px;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 32px 32px 0 0;
|
||||
border-top: solid 2px var(--foreground-color);
|
||||
border-left: solid 2px var(--foreground-color);
|
||||
border-right: solid 2px var(--foreground-color);
|
||||
background: linear-gradient(0.75turn, transparent, var(--foreground-color) 25%),
|
||||
var(--triangle-background);
|
||||
backdrop-filter: var(--blur-val);
|
||||
box-shadow: 0px 0px 8px rgb(var(--blue-shadow-color), 0.8);
|
||||
}
|
||||
|
||||
.footer-info {
|
||||
a {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
img {
|
||||
height: 36px;
|
||||
filter: drop-shadow(0 0 8px #328cfa);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
.footer-logo {
|
||||
img {
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
.vitepress/theme/components/Gitalk.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="gitalk-container">
|
||||
<div id="gitalk-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import md5 from 'md5'
|
||||
const themeConfig = useData().theme.value
|
||||
declare var Gitalk: any
|
||||
onMounted(() => {
|
||||
const commentConfig = {
|
||||
clientID: themeConfig.clientID,
|
||||
clientSecret: themeConfig.clientSecret,
|
||||
repo: themeConfig.repo,
|
||||
owner: themeConfig.owner,
|
||||
admin: themeConfig.admin,
|
||||
id: md5(location.pathname).toString(),
|
||||
distractionFreeMode: false,
|
||||
}
|
||||
|
||||
const gitalk = new Gitalk(commentConfig)
|
||||
gitalk.render('gitalk-container')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.gt-container .gt-header-textarea {
|
||||
color: var(--font-color-grey);
|
||||
background-color: var(--general-background-color) !important;
|
||||
transition: background-color 0.5s, color 0.5s !important;
|
||||
|
||||
}
|
||||
.gt-container .gt-comment-content{
|
||||
background-color: var(--gitalk-background) !important;
|
||||
border-radius: 10px;
|
||||
p{
|
||||
color: var(--font-color-grey);
|
||||
}
|
||||
ol{
|
||||
color: var(--gitalk-font-color-ol);
|
||||
}
|
||||
.email-fragment {
|
||||
color: var(--font-color-grey);
|
||||
}
|
||||
.email-hidden-reply {
|
||||
color: var(--font-color-grey);
|
||||
}
|
||||
}
|
||||
|
||||
.gt-container .gt-comment-content:hover {
|
||||
-webkit-box-shadow: var(--gitalk-shadow) !important;
|
||||
box-shadow: var(--gitalk-shadow) !important;
|
||||
}
|
||||
|
||||
.gt-container .gt-comment-body {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
padding: 0 1em;
|
||||
border-left: var(--gitalk-border-left) !important;
|
||||
}
|
||||
</style>
|
||||
115
.vitepress/theme/components/Navbar/Dropdown-Menu.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="dropdown-menu" ref="dropdownMenu">
|
||||
<div class="menu-content">
|
||||
<div class="first-row">
|
||||
<MusicControl></MusicControl>
|
||||
<SearchButton></SearchButton>
|
||||
</div>
|
||||
<ToggleSwitch></ToggleSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import MusicControl from './Music-Control.vue'
|
||||
import SearchButton from './Search-Button.vue'
|
||||
import ToggleSwitch from './ToggleSwitch.vue'
|
||||
import { useStore } from '../../store'
|
||||
|
||||
const { state } = useStore()
|
||||
const dropdownMenu = ref<HTMLElement | null>(null)
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
const hamburgerEl = document.querySelector('.hamburger')
|
||||
|
||||
// 避免与展开按钮冲突
|
||||
if (hamburgerEl && hamburgerEl.contains(target)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (dropdownMenu.value && !dropdownMenu.value.contains(target) && state.showDropdownMenu) {
|
||||
state.showDropdownMenu = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.menu-content {
|
||||
position: relative;
|
||||
background-color: var(--foreground-color);
|
||||
border-radius: 2vw;
|
||||
padding: 1.2vw;
|
||||
gap: 0.8vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.first-row {
|
||||
display: flex;
|
||||
gap: 0.4vw;
|
||||
padding: 0.3vw;
|
||||
padding-bottom: 1vh;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px dashed var(--font-color-grey);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu[showmenu='true'] {
|
||||
opacity: 1;
|
||||
transform: translateY(15px);
|
||||
.menu-content {
|
||||
box-shadow: 0px 0px 8px rgb(var(--blue-shadow-color), 0.8);
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
transition: opacity 0.1s ease-in-out, transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.dropdown-menu[showmenu='false'] {
|
||||
opacity: 0;
|
||||
transform: translateY(2px);
|
||||
.menu-content {
|
||||
box-shadow: none;
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
transition: opacity 0.2s ease-in-out, transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dropdown-menu {
|
||||
.menu-content {
|
||||
border-radius: 3vh;
|
||||
padding: 2vh;
|
||||
gap: 1vh;
|
||||
}
|
||||
|
||||
.first-row {
|
||||
gap: 0.5vh;
|
||||
padding: 0.3vh;
|
||||
padding-bottom: 1vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
.vitepress/theme/components/Navbar/Music-Control.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<span class="music-control" @click="toggleMusic">
|
||||
<i :class="isPlaying ? 'iconfont icon-continue continue' : 'iconfont icon-stop stop'"></i>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const isPlaying = ref(false) // 音乐播放状态
|
||||
const music = ref<HTMLAudioElement | null>(null)
|
||||
|
||||
const toggleMusic = () => {
|
||||
if (music.value) {
|
||||
if (isPlaying.value) {
|
||||
music.value.pause()
|
||||
} else {
|
||||
music.value.play().catch((err) => console.log('播放失败: ', err))
|
||||
}
|
||||
isPlaying.value = !isPlaying.value
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
music.value = document.getElementById('background-music') as HTMLAudioElement
|
||||
if (music.value) {
|
||||
music.value.volume = 0.3 // 设置音量为30%
|
||||
music.value.pause() // 初始状态为暂停
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.iconfont {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2vw;
|
||||
color: var(--font-color-grey);
|
||||
cursor: pointer;
|
||||
transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.iconfont {
|
||||
font-size: 4vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
.vitepress/theme/components/Navbar/Search-Button.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<span class="iconfont icon-search search" @click="showDialog"></span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from '../../store'
|
||||
const { state } = useStore()
|
||||
|
||||
const showDialog = () => {
|
||||
state.searchDialog = true
|
||||
state.showDropdownMenu = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.search {
|
||||
cursor: pointer;
|
||||
font-size: 2vw;
|
||||
color: var(--font-color-grey);
|
||||
transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search {
|
||||
font-size: 4vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
292
.vitepress/theme/components/Navbar/Search-Dialog.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div class="search-dialog">
|
||||
<div class="dialog-cover" @click="closeDialog"></div>
|
||||
<div class="dialog-content">
|
||||
<button type="button" class="close-btn" @click="closeDialog">×</button>
|
||||
<span class="title">搜索</span>
|
||||
<input
|
||||
type="text"
|
||||
name=""
|
||||
id="search-input"
|
||||
placeholder="请输入关键字"
|
||||
v-model="searchStr"
|
||||
@input="search"
|
||||
/>
|
||||
<ul class="search-list">
|
||||
<span>{{ status }}</span>
|
||||
<li v-for="res in resultList" @click="closeDialog">
|
||||
<a :href="base + res.href">{{ res.title }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const emit = defineEmits(['closeDialog'])
|
||||
const closeDialog = (): void => {
|
||||
// 添加关闭动画
|
||||
const dialog = document.querySelector('.search-dialog') as HTMLElement
|
||||
if (dialog) {
|
||||
dialog.classList.add('hide-dialog')
|
||||
setTimeout(() => {
|
||||
emit('closeDialog')
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
import { data as posts } from '../../utils/posts.data'
|
||||
import MiniSearch, { SearchResult } from 'minisearch'
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const base = useData().site.value.base
|
||||
const miniSearch = new MiniSearch({
|
||||
fields: ['title', 'content'],
|
||||
storeFields: ['title', 'href'],
|
||||
searchOptions: {
|
||||
fuzzy: 0.3,
|
||||
},
|
||||
})
|
||||
miniSearch.addAll(posts)
|
||||
|
||||
const searchStr = defineModel<string>()
|
||||
const resultList = ref<SearchResult[]>([])
|
||||
const status = ref('这里空空的')
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function search(): void {
|
||||
status.value = '搜索中……'
|
||||
if (timerId) {
|
||||
clearTimeout(timerId)
|
||||
}
|
||||
timerId = setTimeout(() => {
|
||||
resultList.value = miniSearch.search(searchStr.value || '').slice(0, 5)
|
||||
if (resultList.value.length) {
|
||||
status.value = '搜到了~'
|
||||
} else {
|
||||
status.value = '这里空空的'
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const dialog = document.querySelector('.search-dialog') as HTMLElement
|
||||
if (dialog) {
|
||||
dialog.classList.add('show-dialog')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.search-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
animation: fadein 0.2s forwards;
|
||||
}
|
||||
// 遮罩
|
||||
.dialog-cover {
|
||||
background: rgba(0, 0, 0, 0.614);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
animation: cover-fadein 0.2s forwards;
|
||||
}
|
||||
// 搜索框
|
||||
.dialog-content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 768px;
|
||||
height: auto;
|
||||
background-color: var(--search-dialog-bg);
|
||||
border-radius: 16px;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
animation: pop-up 0.2s forwards;
|
||||
}
|
||||
|
||||
.dialog-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
border-bottom: 3px solid var(--search-dialog-border);
|
||||
background-color: var(--search-dialog-header-bg);
|
||||
background-image: var(--deco2);
|
||||
background-repeat: no-repeat;
|
||||
background-position: left;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 25px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 5px solid var(--font-color-gold);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
font-size: 36px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#search-input {
|
||||
color: var(--font-color-grey);
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
margin: 10px;
|
||||
padding: 0 16px;
|
||||
background-color: var(--search-input-bg);
|
||||
border: 3px solid var(--search-input-border);
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: 3px solid var(--search-input-border);
|
||||
}
|
||||
}
|
||||
|
||||
.search-list {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--search-list-bg);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
background-color: var(--search-item-bg);
|
||||
border-radius: 5px;
|
||||
box-shadow: 3px 3px 3px var(--search-item-shadow);
|
||||
padding: 10px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
color: var(--font-color-grey);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dialog-content {
|
||||
top: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadein {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cover-fadein {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pop-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cover-fadeout {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.show-dialog {
|
||||
animation: fadein 0.3s forwards;
|
||||
}
|
||||
|
||||
.hide-dialog {
|
||||
animation: fadeout 0.3s forwards;
|
||||
}
|
||||
|
||||
.dialog-cover.show-dialog {
|
||||
animation: cover-fadein 0.3s forwards;
|
||||
}
|
||||
|
||||
.dialog-cover.hide-dialog {
|
||||
animation: cover-fadeout 0.3s forwards;
|
||||
}
|
||||
|
||||
.dialog-content.show-dialog {
|
||||
animation: pop-up 0.3s forwards;
|
||||
}
|
||||
|
||||
.dialog-content.hide-dialog {
|
||||
animation: pop-down 0.3s forwards;
|
||||
}
|
||||
|
||||
@keyframes pop-down {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
230
.vitepress/theme/components/Navbar/ToggleSwitch.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="toggle-container">
|
||||
<div class="theme-select">
|
||||
<span class="label">主题</span>
|
||||
<div class="select-wrapper">
|
||||
<select v-model="selectedTheme" @change="changeTheme">
|
||||
<option value="light">Arona</option>
|
||||
<option value="dark">Plana</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
<span class="select-arrow">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(label, id) in toggles"
|
||||
:key="id"
|
||||
class="toggle-item">
|
||||
<span class="label">{{ label }}</span>
|
||||
<input type="checkbox"
|
||||
:id="id"
|
||||
:checked="state[id]"
|
||||
@change="toggleSwitch(id)">
|
||||
<label :for="id" class="toggleSwitch"></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from '../../store'
|
||||
import { onMounted, ref } from 'vue'
|
||||
const { state } = useStore()
|
||||
|
||||
const selectedTheme = ref('system')
|
||||
|
||||
const toggles = {
|
||||
fireworksEnabled: '烟花',
|
||||
SpinePlayerEnabled: 'Spine',
|
||||
}
|
||||
|
||||
let darkModeMediaQuery: MediaQueryList
|
||||
let handleSystemThemeChange: (e: MediaQueryListEvent | MediaQueryList) => void
|
||||
|
||||
// 页面加载时从 localStorage 读取状态
|
||||
onMounted(() => {
|
||||
// 读取主题设置,如果没有存储值则默认使用system
|
||||
const storedTheme = localStorage.getItem('darkMode')
|
||||
selectedTheme.value = storedTheme || 'system'
|
||||
|
||||
// 定义系统主题变化处理函数
|
||||
darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
handleSystemThemeChange = (e: MediaQueryListEvent | MediaQueryList) => {
|
||||
if (selectedTheme.value === 'system') {
|
||||
const theme = e.matches ? 'dark' : 'light'
|
||||
document.documentElement.setAttribute('theme', theme)
|
||||
state.darkMode = theme
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是system模式,启动监听
|
||||
if (selectedTheme.value === 'system') {
|
||||
handleSystemThemeChange(darkModeMediaQuery)
|
||||
darkModeMediaQuery.addEventListener('change', handleSystemThemeChange)
|
||||
}
|
||||
|
||||
applyTheme(selectedTheme.value)
|
||||
|
||||
// 读取其他开关状态
|
||||
Object.keys(toggles).forEach((key) => {
|
||||
const storedValue = localStorage.getItem(key);
|
||||
if (storedValue !== null) {
|
||||
state[key] = JSON.parse(storedValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const changeTheme = () => {
|
||||
state.darkMode = selectedTheme.value as 'system' | 'dark' | 'light';
|
||||
localStorage.setItem('darkMode', selectedTheme.value);
|
||||
|
||||
// 根据选择决定是否启用系统主题监听
|
||||
if (selectedTheme.value === 'system') {
|
||||
handleSystemThemeChange(darkModeMediaQuery)
|
||||
darkModeMediaQuery.addEventListener('change', handleSystemThemeChange)
|
||||
} else {
|
||||
darkModeMediaQuery.removeEventListener('change', handleSystemThemeChange)
|
||||
}
|
||||
|
||||
applyTheme(selectedTheme.value);
|
||||
};
|
||||
|
||||
const toggleSwitch = (key: string) => {
|
||||
const isChecked = state[key];
|
||||
state[key] = !isChecked;
|
||||
localStorage.setItem(key, JSON.stringify(!isChecked));
|
||||
};
|
||||
|
||||
const applyTheme = (theme: string) => {
|
||||
let effectiveTheme = theme
|
||||
if (theme === 'system') {
|
||||
effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
document.documentElement.setAttribute('theme', effectiveTheme)
|
||||
state.darkMode = effectiveTheme as 'light' | 'dark' | 'system'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.toggle-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toggle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: rgba(var(--blue-shadow-color), 0.05);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--blue-shadow-color), 0.06);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 15px;
|
||||
color: var(--font-color-grey);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggleSwitch {
|
||||
position: relative;
|
||||
width: 46px;
|
||||
height: 24px;
|
||||
background: rgba(82, 82, 82, 0.3);
|
||||
border-radius: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s ease;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--foreground-color);
|
||||
border-radius: 50%;
|
||||
transition: all 0.4s cubic-bezier(0.3, 1.5, 0.7, 1);
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + .toggleSwitch {
|
||||
background: rgb(66, 92, 139);
|
||||
|
||||
&::after {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
}
|
||||
|
||||
.cooling {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.theme-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: rgba(var(--blue-shadow-color), 0.05);
|
||||
border-radius: 12px;
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
width: 90px;
|
||||
margin-left: 10px;
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--font-color-grey);
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 6px 28px 6px 10px;
|
||||
font-size: 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(var(--blue-shadow-color), 0.15);
|
||||
background: var(--foreground-color);
|
||||
color: var(--font-color-grey);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--blue-shadow-color), 0.3);
|
||||
background: rgba(var(--blue-shadow-color), 0.03);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: rgba(var(--blue-shadow-color), 0.4);
|
||||
box-shadow: 0 0 0 2px rgba(var(--blue-shadow-color), 0.1);
|
||||
}
|
||||
|
||||
option {
|
||||
padding: 8px;
|
||||
background: var(--foreground-color);
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--blue-shadow-color), 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
191
.vitepress/theme/components/Navbar/index.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<header :class="{ postViewer: state.currPost.href }" class="container">
|
||||
<nav>
|
||||
<span class="logo">
|
||||
<img @dragstart.prevent src="../../assets/icon/navLogo.svg" alt="" />
|
||||
</span>
|
||||
<span class="menu">
|
||||
<ul>
|
||||
<li v-for="item in menuList">
|
||||
<a :href="base + item.url" @click="handleNavClick(item.url)">{{ item.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<div
|
||||
class="hamburger"
|
||||
:class="{ active: state.showDropdownMenu }"
|
||||
@click="toggleDropdownMenu"
|
||||
>
|
||||
<span class="line"></span>
|
||||
<span class="line"></span>
|
||||
<span class="line"></span>
|
||||
</div>
|
||||
<DropdownMenu :showMenu="state.showDropdownMenu"></DropdownMenu>
|
||||
</nav>
|
||||
</header>
|
||||
<SearchDialog v-if="state.searchDialog" @close-dialog="closeDialog"></SearchDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const base = useData().site.value.base
|
||||
const themeConfig = useData().theme.value
|
||||
const menuList = themeConfig.menuList
|
||||
|
||||
import { useStore } from '../../store'
|
||||
const { state } = useStore()
|
||||
|
||||
import SearchDialog from './Search-Dialog.vue'
|
||||
import DropdownMenu from './Dropdown-Menu.vue'
|
||||
|
||||
const closeDialog = () => {
|
||||
state.searchDialog = false
|
||||
}
|
||||
const toggleDropdownMenu = () => {
|
||||
state.showDropdownMenu = !state.showDropdownMenu
|
||||
}
|
||||
const resetPage = () => {
|
||||
state.currPage = 1
|
||||
}
|
||||
|
||||
const handleNavClick = (url: string) => {
|
||||
// 点击首页时重置页码
|
||||
if (url === '') {
|
||||
resetPage()
|
||||
}
|
||||
|
||||
// 点击标签时重置标签和页码
|
||||
if (url === 'tags/') {
|
||||
resetPage()
|
||||
state.currTag = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.postViewer {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
header {
|
||||
height: 75vh;
|
||||
position: relative;
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 72px;
|
||||
z-index: 100;
|
||||
box-sizing: border-box;
|
||||
padding: 0 16px;
|
||||
border-radius: 0 0 32px 32px;
|
||||
border-bottom: solid 2px var(--foreground-color);
|
||||
border-left: solid 2px var(--foreground-color);
|
||||
border-right: solid 2px var(--foreground-color);
|
||||
background: linear-gradient(0.25turn, transparent, var(--foreground-color) 25%),
|
||||
var(--triangle-background);
|
||||
backdrop-filter: var(--blur-val);
|
||||
box-shadow: 0px 0px 8px rgb(var(--blue-shadow-color), 0.8);
|
||||
}
|
||||
|
||||
.logo {
|
||||
img {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
filter: drop-shadow(0 0 8px #328cfa);
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
ul {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
margin: 0 64px;
|
||||
a {
|
||||
display: block;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--font-color-grey);
|
||||
transition: all 0.5s;
|
||||
transition: transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
|
||||
&:hover {
|
||||
color: var(--font-color-gold);
|
||||
background-color: var(--btn-background);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hamburger .line {
|
||||
display: block;
|
||||
width: 80%;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--font-color-grey);
|
||||
margin-bottom: 4px;
|
||||
-webkit-transition: all 0.3s ease-in-out;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.hamburger.active .line:nth-child(1) {
|
||||
transform: translateY(8px) rotate(45deg);
|
||||
}
|
||||
.hamburger.active .line:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
.hamburger.active .line:nth-child(3) {
|
||||
transform: translateY(-8px) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
nav {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
img {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
ul {
|
||||
li {
|
||||
margin: 0 10px;
|
||||
a {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
.vitepress/theme/components/NotFound.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="not-found">
|
||||
<img src="../assets/NotFound.webp" alt="" />
|
||||
<span>页面不存在</span>
|
||||
<span class="band"><a :href="base">回到主页</a></span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
const base = useData().site.value.base
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.not-found {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 50vw;
|
||||
margin: 100px auto;
|
||||
padding: 16px;
|
||||
border-radius: 32px;
|
||||
border: solid 2px var(--foreground-color);
|
||||
backdrop-filter: var(--blur-val);
|
||||
|
||||
img {
|
||||
width: 25vw;
|
||||
|
||||
& + span {
|
||||
font-style: italic;
|
||||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
|
||||
font-size: 30px;
|
||||
color: #fbfbfb;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #74d8f9;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.band {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 30vw;
|
||||
border-top: 2px solid #5e87b4;
|
||||
border-bottom: 2px solid #5e87b4;
|
||||
background-color: #1a2b51;
|
||||
padding: 5px 0;
|
||||
mask: linear-gradient(to left, transparent 0%, #1a2b51 50%, transparent 100%);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.not-found {
|
||||
img {
|
||||
width: 50vw;
|
||||
}
|
||||
}
|
||||
|
||||
.band {
|
||||
width: 50vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
.vitepress/theme/components/Post-InnerBanner.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="post-banner" v-show="state.currPost.title">
|
||||
<h1 class="title">{{ state.currPost.title }}</h1>
|
||||
<span class="status"
|
||||
>发布于
|
||||
{{
|
||||
Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(new Date(state.currPost.create))
|
||||
}}
|
||||
| 约{{ state.currPost.wordCount }}字</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useStore } from '../store'
|
||||
const { state } = useStore()
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.post-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--post-InnerBanner-color);
|
||||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
|
||||
z-index: 100;
|
||||
transition: color 0.5s;
|
||||
|
||||
.title {
|
||||
font-size: 4.5vw;
|
||||
margin-bottom: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 1vw;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 5vh;
|
||||
}
|
||||
.status {
|
||||
font-size: 1.5vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
563
.vitepress/theme/components/Post-Viewer.vue
Normal file
@@ -0,0 +1,563 @@
|
||||
<template>
|
||||
<div class="view-box container">
|
||||
<content class="content" />
|
||||
<Gitalk v-if="themeConfig.clientID"></Gitalk>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Gitalk from './Gitalk.vue'
|
||||
import { data as posts } from '../utils/posts.data'
|
||||
import { useData, useRoute } from 'vitepress'
|
||||
import { useStore } from '../store'
|
||||
const route = useRoute()
|
||||
const data = useData()
|
||||
const base = data.site.value.base
|
||||
const { state } = useStore()
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
function getCurrpost() {
|
||||
let currPost = posts.findIndex((p) => p.href === route.path.replace(base, ''))
|
||||
state.currPost = posts[currPost]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCurrpost()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
state.currPost = {
|
||||
id: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
href: '',
|
||||
create: 0,
|
||||
update: 0,
|
||||
tags: [],
|
||||
wordCount: 0,
|
||||
cover: '',
|
||||
excerpt: '',
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
getCurrpost()
|
||||
},
|
||||
)
|
||||
|
||||
const themeConfig = useData().theme.value
|
||||
</script>
|
||||
<style lang="less">
|
||||
.view-box {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
padding: 36px;
|
||||
border-radius: 32px;
|
||||
border: solid 2px var(--foreground-color);
|
||||
background: var(--foreground-color);
|
||||
box-shadow: 0px 0px 8px rgb(var(--blue-shadow-color), 0.8);
|
||||
transition: opacity 0.5s ease-out, transform 1s cubic-bezier(0.61, 0.15, 0.26, 1), border 0.5s,
|
||||
background 0.5s, box-shadow 0.5s;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-image: linear-gradient(90deg, rgba(159, 219, 252, 0.15) 3%, transparent 0),
|
||||
linear-gradient(1turn, rgba(159, 219, 252, 0.15) 3%, transparent 0);
|
||||
background-size: 20px 20px;
|
||||
background-position: 50%;
|
||||
html[theme='dark'] & {
|
||||
background-image: linear-gradient(90deg, rgba(207, 198, 254, 0.08) 3%, transparent 0),
|
||||
linear-gradient(1turn, rgba(207, 198, 254, 0.08) 3%, transparent 0);
|
||||
}
|
||||
/**
|
||||
* Paragraph and inline elements
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
p,
|
||||
summary {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 16px 0;
|
||||
border-left: 3px solid #5cd3ff;
|
||||
padding-left: 16px;
|
||||
background-color: #5cd4ff25;
|
||||
border-radius: 8px;
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(157, 124, 216, 0.1); // 更改引用块背景色
|
||||
border-left: 3px solid #9d7cd8; // 更改引用块边框颜色
|
||||
}
|
||||
}
|
||||
blockquote > p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: var(--color-blue);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: color 0.25s, opacity 0.25s;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', sans-serif;
|
||||
line-height: 0; // 修复行号对齐
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Headings
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 40px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 48px 0 16px;
|
||||
border-top: 2px solid #ced4da;
|
||||
padding-top: 24px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
|
||||
.header-anchor {
|
||||
top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 32px 0 0;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 28px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.header-anchor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-left: -0.87em;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
opacity: 0;
|
||||
text-decoration: none;
|
||||
transition: color 0.25s, opacity 0.25s;
|
||||
}
|
||||
|
||||
.header-anchor:before {
|
||||
content: '#';
|
||||
}
|
||||
|
||||
h1:hover .header-anchor,
|
||||
h1 .header-anchor:focus,
|
||||
h2:hover .header-anchor,
|
||||
h2 .header-anchor:focus,
|
||||
h3:hover .header-anchor,
|
||||
h3 .header-anchor:focus,
|
||||
h4:hover .header-anchor,
|
||||
h4 .header-anchor:focus,
|
||||
h5:hover .header-anchor,
|
||||
h5 .header-anchor:focus,
|
||||
h6:hover .header-anchor,
|
||||
h6 .header-anchor:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
h1 {
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 40px;
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorational elements
|
||||
* -------------------------------------------------------------------------- */
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 2px dashed #ced4da;
|
||||
html[theme='dark'] & {
|
||||
border-top: 2px dashed rgba(157, 124, 216, 0.3); // 更改分割线颜色
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1.25rem;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
li > ol,
|
||||
li > ul {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 2px solid #cad4d5;
|
||||
html[theme='dark'] & {
|
||||
border: 2px solid #383852; // 更改表格边框颜色
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px;
|
||||
color: #3c3e41;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #cad4d5;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #e7f6fa;
|
||||
color: var(--btn-hover);
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(157, 124, 216, 0.1);
|
||||
color: #e0e0e6;
|
||||
}
|
||||
}
|
||||
|
||||
th:nth-child(odd) {
|
||||
background-color: #e0f0f2;
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(157, 124, 216, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
background-color: #f7f7f6;
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(31, 31, 44, 0.6);
|
||||
color: #c8c8dc;
|
||||
}
|
||||
}
|
||||
|
||||
td:nth-child(odd) {
|
||||
background-color: #ececeb;
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(31, 31, 44, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Code
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
div[class*='language-'] {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
position: relative;
|
||||
background-color: #efefef;
|
||||
border: 2px solid var(--foreground-color);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 0px 5px #c1c1c1;
|
||||
overflow: hidden;
|
||||
padding-top: 48px;
|
||||
margin-bottom: 10px;
|
||||
html[theme='dark'] & {
|
||||
background-color: #1f1f2c;
|
||||
border: 2px solid #383852;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.lang {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -36px);
|
||||
left: 50%;
|
||||
user-select: none;
|
||||
font-weight: bold;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 5px solid var(--font-color-gold);
|
||||
}
|
||||
|
||||
button.copy {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
cursor: pointer;
|
||||
background-image: var(--vp-icon-copy);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
&.copied {
|
||||
background-image: var(--vp-icon-copied);
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 48px;
|
||||
border-bottom: 3px solid rgb(213, 217, 219);
|
||||
box-sizing: border-box;
|
||||
background-color: rgb(239, 242, 244);
|
||||
background-image: var(--deco2);
|
||||
background-repeat: no-repeat;
|
||||
background-position: left;
|
||||
background-size: contain;
|
||||
html[theme='dark'] & {
|
||||
border-bottom: 3px solid #383852;
|
||||
background-color: rgba(31, 31, 44, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.line-numbers-wrapper {
|
||||
border-right: 2px solid #dfdfdf;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
html[theme='dark'] & {
|
||||
border-right: 2px solid #383852;
|
||||
}
|
||||
}
|
||||
|
||||
pre,
|
||||
.line-numbers-wrapper {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.custom-block {
|
||||
transition: background-color 0.5s, border-color 0.5s, color 0.5s;
|
||||
|
||||
&.tip,
|
||||
&.info,
|
||||
&.warning,
|
||||
&.danger {
|
||||
margin: 1rem 0;
|
||||
border-left: 0.35rem solid;
|
||||
padding: 0.1rem 1.5rem;
|
||||
overflow-x: auto;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.custom-block-title {
|
||||
&::before {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.tip {
|
||||
background-color: #f1f6fa;
|
||||
border-color: #57b6f6;
|
||||
color: #005e86;
|
||||
|
||||
.custom-block-title {
|
||||
&::before {
|
||||
content: var(--icon-tip);
|
||||
}
|
||||
}
|
||||
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(158, 124, 216, 0.18);
|
||||
border-color: #9e7cd8ae;
|
||||
color: #e0e0e6;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: #f3f5f7;
|
||||
border-color: var(--font-color-grey);
|
||||
|
||||
.custom-block-title {
|
||||
&::before {
|
||||
content: var(--icon-info);
|
||||
}
|
||||
}
|
||||
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(108, 182, 255, 0.161);
|
||||
border-color: #6cb6ffcf;
|
||||
color: #e0e0e6;
|
||||
|
||||
.custom-block-title {
|
||||
color: #89c4ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-color: #e7c000;
|
||||
color: #6b5900;
|
||||
background-color: #fff7d0;
|
||||
|
||||
.custom-block-title {
|
||||
&::before {
|
||||
content: var(--icon-warning);
|
||||
}
|
||||
}
|
||||
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(231, 192, 0, 0.1);
|
||||
color: #f0d87d;
|
||||
|
||||
.custom-block-title {
|
||||
color: #e7c000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-color: #d58d86;
|
||||
color: #4d0000;
|
||||
background-color: #ffe6e6;
|
||||
|
||||
.custom-block-title {
|
||||
&::before {
|
||||
content: var(--icon-danger);
|
||||
}
|
||||
}
|
||||
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(213, 141, 134, 0.1);
|
||||
color: #ffc4c0;
|
||||
|
||||
.custom-block-title {
|
||||
color: #ff9b93;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.details {
|
||||
summary {
|
||||
font-weight: bold;
|
||||
}
|
||||
margin: 1rem 0;
|
||||
padding: 1rem 1.5rem;
|
||||
overflow-x: auto;
|
||||
border-radius: 16px;
|
||||
background-color: #f3f5f7;
|
||||
border-color: var(--font-color-grey);
|
||||
|
||||
html[theme='dark'] & {
|
||||
background-color: rgba(158, 124, 216, 0.168);
|
||||
border-color: #383852;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-block-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* others
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
filter: var(--img-brightness);
|
||||
transition: filter 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.view-box {
|
||||
padding: 24px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
.content {
|
||||
/**
|
||||
* Code
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
div[class*='language-'] {
|
||||
padding-top: 36px;
|
||||
font-size: 12px;
|
||||
|
||||
.lang {
|
||||
transform: translate(-50%, -32px);
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 4px solid var(--font-color-gold);
|
||||
}
|
||||
|
||||
button.copy {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
612
.vitepress/theme/components/Posts-List.vue
Normal file
@@ -0,0 +1,612 @@
|
||||
<template>
|
||||
<div class="container posts-content">
|
||||
<TransitionGroup class="posts-list" name="list" tag="div">
|
||||
<article class="post" v-for="post in postsList" :key="post.href">
|
||||
<span v-if="post.pinned" class="pinned"></span>
|
||||
<header class="post-header">
|
||||
<div v-if="post.cover" class="cover-container">
|
||||
<img
|
||||
:src="post.cover"
|
||||
class="cover-image"
|
||||
:alt="post.title + '-cover'"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="header-content">
|
||||
<div class="title">
|
||||
<div class="title-dot" v-if="!post.cover"></div>
|
||||
<h1 class="name">
|
||||
<a :href="base + post.href">{{ post.title }}</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="meta-info-bar">
|
||||
<span class="iconfont icon-time time"></span>
|
||||
<div class="time-info">
|
||||
<time datetime="">{{ formatDate(post.create) }}</time>
|
||||
</div>
|
||||
<div class="wordcount seperator">约{{ post.wordCount }}字</div>
|
||||
</div>
|
||||
<ul class="tags">
|
||||
<li v-for="tag in post.tags">
|
||||
<a :href="`${base}tags/`" @click="state.currTag = tag"
|
||||
><i class="iconfont icon-tag"></i> {{ tag }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="excerpt">
|
||||
<p>{{ post.excerpt }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</article>
|
||||
</TransitionGroup>
|
||||
<div v-if="totalPage != 1" class="pagination">
|
||||
<button
|
||||
:disabled="currPage === 1"
|
||||
:class="{ hide: currPage === 1 }"
|
||||
id="up"
|
||||
@click="goToPage(currPage - 1)"
|
||||
>
|
||||
<i class="iconfont icon-arrow"></i>
|
||||
</button>
|
||||
|
||||
<div class="page-numbers">
|
||||
<!-- 第一页 -->
|
||||
<button class="page-number" :class="{ active: currPage === 1 }" @click="goToPage(1)">
|
||||
1
|
||||
</button>
|
||||
|
||||
<!-- 页码省略号 -->
|
||||
<span v-if="showLeftEllipsis" class="ellipsis">...</span>
|
||||
|
||||
<!-- 当前页码 -->
|
||||
<button
|
||||
v-for="page in visiblePageNumbers"
|
||||
:key="page"
|
||||
class="page-number"
|
||||
:class="{ active: currPage === page }"
|
||||
@click="goToPage(page)"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
|
||||
<!-- 页码省略号 -->
|
||||
<span v-if="showRightEllipsis" class="ellipsis">...</span>
|
||||
|
||||
<!-- 尾页 -->
|
||||
<button
|
||||
v-if="totalPage > 1"
|
||||
class="page-number"
|
||||
:class="{ active: currPage === totalPage }"
|
||||
@click="goToPage(totalPage)"
|
||||
>
|
||||
{{ totalPage }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:disabled="currPage >= totalPage"
|
||||
:class="{ hide: currPage >= totalPage }"
|
||||
id="next"
|
||||
@click="goToPage(currPage + 1)"
|
||||
>
|
||||
<i class="iconfont icon-arrow"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { data as posts } from '../utils/posts.data'
|
||||
import { useStore } from '../store'
|
||||
const { state } = useStore()
|
||||
const { page } = useData()
|
||||
const base = useData().site.value.base
|
||||
|
||||
// 日期格式化
|
||||
function formatDate(timestamp: number): string {
|
||||
const date = new Date(timestamp)
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
// 文章传值
|
||||
const finalPosts = computed(() => {
|
||||
if (page.value.filePath === 'index.md') {
|
||||
return posts
|
||||
} else if (page.value.filePath === 'tags/index.md') {
|
||||
return state.selectedPosts
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
// 文章列表长度
|
||||
const pageSize = ref(5)
|
||||
|
||||
// 使用store中的currPage
|
||||
const currPage = computed({
|
||||
get: () => state.currPage,
|
||||
set: (value) => (state.currPage = value),
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 获取URL信息
|
||||
updatePageFromUrl()
|
||||
// 监听前进后退
|
||||
window.addEventListener('popstate', () => {
|
||||
updatePageFromUrl()
|
||||
})
|
||||
})
|
||||
function updatePageFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const pageParam = urlParams.get('page')
|
||||
if (
|
||||
pageParam &&
|
||||
!isNaN(parseInt(pageParam)) &&
|
||||
parseInt(pageParam) > 0 &&
|
||||
parseInt(pageParam) <= totalPage.value
|
||||
) {
|
||||
state.currPage = parseInt(pageParam)
|
||||
} else {
|
||||
state.currPage = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 更新页码逻辑
|
||||
function goToPage(page: number) {
|
||||
if (page < 1 || page > totalPage.value) return
|
||||
state.currPage = page
|
||||
|
||||
// 获取URL信息
|
||||
const url = new URL(window.location.href)
|
||||
|
||||
// 非首页时获取URL页码
|
||||
if (page > 1) {
|
||||
url.searchParams.set('page', page.toString())
|
||||
} else {
|
||||
url.searchParams.delete('page')
|
||||
}
|
||||
|
||||
// Tag页面页码逻辑
|
||||
const tagParam = url.searchParams.get('tag')
|
||||
if (tagParam) {
|
||||
url.searchParams.set('tag', tagParam)
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', url.toString())
|
||||
}
|
||||
|
||||
// 计算要显示的页码
|
||||
const maxVisiblePages = 3 // 省略号两边显示的页码按钮数量
|
||||
const visiblePageNumbers = computed(() => {
|
||||
if (totalPage.value <= 7)
|
||||
return Array.from({ length: totalPage.value - 2 }, (_, i) => i + 2).filter(
|
||||
(p) => p > 1 && p < totalPage.value,
|
||||
)
|
||||
|
||||
let startPage = Math.max(2, currPage.value - Math.floor(maxVisiblePages / 2))
|
||||
let endPage = Math.min(totalPage.value - 1, startPage + maxVisiblePages - 1)
|
||||
|
||||
if (endPage - startPage < maxVisiblePages - 1) {
|
||||
startPage = Math.max(2, endPage - maxVisiblePages + 1)
|
||||
}
|
||||
|
||||
return Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i)
|
||||
})
|
||||
|
||||
// 省略号显示逻辑
|
||||
const showLeftEllipsis = computed(() => {
|
||||
return totalPage.value > 7 && visiblePageNumbers.value[0] > 2
|
||||
})
|
||||
|
||||
const showRightEllipsis = computed(() => {
|
||||
return (
|
||||
totalPage.value > 7 &&
|
||||
visiblePageNumbers.value[visiblePageNumbers.value.length - 1] < totalPage.value - 1
|
||||
)
|
||||
})
|
||||
const postsList = computed(() => {
|
||||
return finalPosts.value.slice(
|
||||
(currPage.value - 1) * pageSize.value,
|
||||
currPage.value * pageSize.value,
|
||||
)
|
||||
})
|
||||
const totalPage = computed(() => {
|
||||
return Math.ceil(finalPosts.value.length / pageSize.value) || 1
|
||||
})
|
||||
|
||||
// 监听文章列表
|
||||
watch(
|
||||
() => state.selectedPosts,
|
||||
() => {
|
||||
// 标签页逻辑,获取URL页码
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const pageParam = urlParams.get('page')
|
||||
|
||||
// 标签更改时重置页码
|
||||
const newTotalPages = Math.ceil(state.selectedPosts.length / pageSize.value) || 1
|
||||
|
||||
if (!pageParam || currPage.value > newTotalPages) {
|
||||
currPage.value = 1
|
||||
|
||||
// 更新URL
|
||||
if (pageParam) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete('page')
|
||||
window.history.pushState({}, '', url.toString())
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.posts-content {
|
||||
article,
|
||||
h1,
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.posts-list {
|
||||
position: relative;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.post {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 0 50px 0;
|
||||
padding-bottom: 16px;
|
||||
background-color: var(--foreground-color);
|
||||
border-radius: 32px;
|
||||
border-left: solid 16px var(--pot-border-left);
|
||||
background-image: var(--deco1);
|
||||
background-size: contain;
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
box-shadow: 0px 0px 8px rgb(var(--blue-shadow-color), 0.8);
|
||||
transition: all 0.5s;
|
||||
.pinned {
|
||||
position: absolute;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
border-radius: 50px;
|
||||
background: var(--icon-pinned) no-repeat;
|
||||
background-size: contain;
|
||||
box-shadow: 0 0 6px rgba(var(--blue-shadow-color), 0.65);
|
||||
}
|
||||
|
||||
.post-header {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 32px 40px 0;
|
||||
// flex-direction: row-reverse;
|
||||
position: relative;
|
||||
align-items: stretch;
|
||||
|
||||
.cover-container {
|
||||
flex: 0 0 180px;
|
||||
height: 140px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-left: -8px;
|
||||
margin-bottom: 15px;
|
||||
align-self: center;
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
.title {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.excerpt {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.post-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px 20px 0;
|
||||
|
||||
.cover-container {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-header {
|
||||
padding: 32px 40px 0;
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.title-dot {
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 9.5px;
|
||||
background: var(--pot-border-left);
|
||||
border-radius: 2px;
|
||||
transition: background 0.5s;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--font-color-grey);
|
||||
transition: text-shadow 0.5s, color 0.5s;
|
||||
|
||||
&:hover {
|
||||
text-shadow: 0 0 3px var(--font-color-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meta-info-bar {
|
||||
display: flex;
|
||||
margin-bottom: 7px;
|
||||
opacity: 0.75;
|
||||
|
||||
.time {
|
||||
font-size: 13px;
|
||||
color: var(--font-color-grey);
|
||||
margin: 3px 2px 0 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.seperator::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
vertical-align: middle;
|
||||
background-color: var(--font-color-grey);
|
||||
margin: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 0;
|
||||
margin-bottom: 6px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 6px;
|
||||
margin-right: 16px;
|
||||
|
||||
a {
|
||||
color: var(--font-color-grey);
|
||||
padding: 3px 5px;
|
||||
color: var(--font-color-gold);
|
||||
background-color: var(--btn-background);
|
||||
border-radius: 5px;
|
||||
transition: all 0.5s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-hover);
|
||||
color: var(--font-color-gold);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 50px;
|
||||
padding: 0;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.icon-arrow {
|
||||
font-size: 36px;
|
||||
color: var(--icon-color);
|
||||
}
|
||||
|
||||
#up {
|
||||
animation: arrow-pre 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
#next {
|
||||
animation: arrow-next 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
color: var(--icon-color);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-hover);
|
||||
color: var(--font-color-gold);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--btn-hover);
|
||||
color: var(--font-color-gold);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
margin: 0 4px;
|
||||
color: var(--icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes arrow-pre {
|
||||
from {
|
||||
transform: translateX(0) rotate(-0.25turn);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(10px) rotate(-0.25turn);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes arrow-next {
|
||||
from {
|
||||
transform: translateX(0) rotate(0.25turn);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(-10px) rotate(0.25turn);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.posts-list {
|
||||
.post {
|
||||
margin: 0 8px 30px 8px;
|
||||
background-size: cover;
|
||||
border-left: solid 1.5vh var(--pot-border-left);
|
||||
.pinned {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
top: -2px;
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-header {
|
||||
padding: 20px 35px 0;
|
||||
.name {
|
||||
font-size: 24px;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 6px;
|
||||
|
||||
.title-dot {
|
||||
height: 18px;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
.meta-info-bar {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
.time {
|
||||
font-size: 8px !important;
|
||||
margin: 3px 2px 0 0 !important;
|
||||
}
|
||||
.seperator::before {
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tags {
|
||||
li {
|
||||
padding-top: 4px;
|
||||
margin-right: 8px;
|
||||
a {
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
.icon-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.excerpt {
|
||||
padding: 0;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pagination {
|
||||
margin-top: 32px;
|
||||
.icon-arrow {
|
||||
font-size: 32px;
|
||||
}
|
||||
.page-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.page-numbers {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
665
.vitepress/theme/components/Spine-Player/index.vue
Normal file
@@ -0,0 +1,665 @@
|
||||
<!--
|
||||
todo:
|
||||
1. 优化暗色描边
|
||||
2. 优化对话框三角布局
|
||||
3. 整理代码结构
|
||||
-->
|
||||
<template>
|
||||
<template v-if="state.SpinePlayerEnabled">
|
||||
<div
|
||||
ref="playerContainer"
|
||||
class="playerContainer"
|
||||
@click="handlePlayerClick"
|
||||
@touchstart="handlePlayerClick"
|
||||
></div>
|
||||
<transition name="fade">
|
||||
<div v-if="showDialog" class="chatdialog-container">
|
||||
<div class="chatdialog-triangle"></div>
|
||||
<div class="chatdialog">{{ currentDialog }}</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
import { onMounted, ref, watch, computed } from 'vue'
|
||||
|
||||
import { useStore } from '../../store'
|
||||
const { state } = useStore()
|
||||
|
||||
import { useData } from 'vitepress'
|
||||
const themeConfig = useData().theme.value
|
||||
const spineVoiceLang = themeConfig.spineVoiceLang
|
||||
|
||||
import { spine } from './spine-player.js'
|
||||
|
||||
// 定义两套spine资产信息
|
||||
const spineAssets = {
|
||||
arona: {
|
||||
skelUrl: "/spine_assets/arona/arona_spr.skel",
|
||||
atlasUrl: "/spine_assets/arona/arona_spr.atlas",
|
||||
idleAnimationName: "Idle_01",
|
||||
eyeCloseAnimationName: "Eye_Close_01",
|
||||
rightEyeBone: "R_Eye_01",
|
||||
leftEyeBone: "L_Eye_01",
|
||||
frontHeadBone: "Head_01",
|
||||
backHeadBone: "Head_Back",
|
||||
eyeRotationAngle: 76.307,
|
||||
voiceConfig: [
|
||||
{
|
||||
audio: `/spine_assets/arona/audio/${spineVoiceLang}/arona_01.ogg`,
|
||||
animation: '12',
|
||||
text: '您回来了?我等您很久啦!'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/arona/audio/${spineVoiceLang}/arona_02.ogg`,
|
||||
animation: '03',
|
||||
text: '嗯,不错,今天也是个好天气。'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/arona/audio/${spineVoiceLang}/arona_03.ogg`,
|
||||
animation: '02',
|
||||
text: '天空真是广啊……\n另一边会有些什么呢?'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/arona/audio/${spineVoiceLang}/arona_04.ogg`,
|
||||
animation: '18',
|
||||
text: '偶尔也要为自己的健康着想啊,\n老师,我会很担心的。'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/arona/audio/${spineVoiceLang}/arona_05.ogg`,
|
||||
animation: '25',
|
||||
text: '来,加油吧,老师!'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/arona/audio/${spineVoiceLang}/arona_06.ogg`,
|
||||
animation: '11',
|
||||
text: '今天又会有什么事情在等着我呢?'
|
||||
}
|
||||
]
|
||||
},
|
||||
plana: {
|
||||
skelUrl: "/spine_assets/plana/plana_spr.skel",
|
||||
atlasUrl: "/spine_assets/plana/plana_spr.atlas",
|
||||
idleAnimationName: "Idle_01",
|
||||
eyeCloseAnimationName: "Eye_Close_01",
|
||||
rightEyeBone: "R_Eye_01",
|
||||
leftEyeBone: "L_Eye_01",
|
||||
frontHeadBone: "Head_Rot",
|
||||
backHeadBone: "Head_Back",
|
||||
eyeRotationAngle: 97.331,
|
||||
voiceConfig: [
|
||||
{
|
||||
audio: `/spine_assets/plana/audio/${spineVoiceLang}/plana_02.ogg`,
|
||||
animation: '06',
|
||||
text: '我明白了,\n老师现在无事可做,很无聊。'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/plana/audio/${spineVoiceLang}/plana_01.ogg`,
|
||||
animation: '13',
|
||||
text: '混乱,该行动无法理解。\n请不要戳我,会出现故障。'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/plana/audio/${spineVoiceLang}/plana_03.ogg`,
|
||||
animation: '15',
|
||||
text: '确认连接。'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/plana/audio/${spineVoiceLang}/plana_04.ogg`,
|
||||
animation: '99',
|
||||
text: '正在待命,\n需要解决的任务还有很多。'
|
||||
},
|
||||
{
|
||||
audio: `/spine_assets/plana/audio/${spineVoiceLang}/plana_05.ogg`,
|
||||
animation: '17',
|
||||
text: '等您很久了。'
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const playerContainer = ref(null)
|
||||
let player = null
|
||||
let blinkInterval = null
|
||||
let isEyeControlDisabled = ref(false)
|
||||
let eyeControlTimer = null
|
||||
let currentAnimationState = null // 添加动画状态引用
|
||||
let currentCharacter = ref('arona')
|
||||
let audioPlayers = []
|
||||
|
||||
// 添加客户端就绪状态
|
||||
const clientReady = ref(false)
|
||||
|
||||
// 添加音频上下文管理器
|
||||
const AudioManager = {
|
||||
context: null,
|
||||
buffers: new Map(),
|
||||
currentSource: null,
|
||||
gainNode: null,
|
||||
|
||||
initialize() {
|
||||
if (!clientReady.value) return
|
||||
if (!this.context) {
|
||||
this.context = new (window.AudioContext || window.webkitAudioContext)();
|
||||
// 创建增益节点,设置音量为50%
|
||||
this.gainNode = this.context.createGain();
|
||||
this.gainNode.gain.value = 0.5;
|
||||
this.gainNode.connect(this.context.destination);
|
||||
}
|
||||
},
|
||||
|
||||
async loadAudioFile(url) {
|
||||
if (this.buffers.has(url)) {
|
||||
const entry = this.buffers.get(url);
|
||||
entry.lastUsed = Date.now();
|
||||
return entry.buffer;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
|
||||
this.buffers.set(url, { buffer: audioBuffer, lastUsed: Date.now() });
|
||||
return audioBuffer;
|
||||
} catch (error) {
|
||||
console.error('音频加载失败:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async playAudio(buffer) {
|
||||
if (this.currentSource) {
|
||||
this.currentSource.stop();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const source = this.context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
// 连接到增益节点而不是直接连接到目标
|
||||
source.connect(this.gainNode);
|
||||
source.onended = () => {
|
||||
if (this.currentSource === source) {
|
||||
this.currentSource = null;
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
this.currentSource = source;
|
||||
source.start();
|
||||
});
|
||||
},
|
||||
|
||||
clear() {
|
||||
if (this.currentSource) {
|
||||
this.currentSource.stop();
|
||||
this.currentSource = null;
|
||||
}
|
||||
this.buffers.clear();
|
||||
},
|
||||
|
||||
gc() {
|
||||
// 清除超过5分钟未使用的音频缓存
|
||||
const now = Date.now()
|
||||
for (const [url, entry] of this.buffers.entries()) {
|
||||
if (now - entry.lastUsed > 300000) { // 5分钟
|
||||
this.buffers.delete(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改预加载音频函数
|
||||
const preloadAudio = async () => {
|
||||
if (!currentAssets.value) return false
|
||||
|
||||
AudioManager.initialize()
|
||||
AudioManager.gc() // 清理过期缓存
|
||||
|
||||
const loadPromises = currentAssets.value.voiceConfig.map(pair =>
|
||||
AudioManager.loadAudioFile(pair.audio)
|
||||
)
|
||||
|
||||
return Promise.all(loadPromises).catch(error => {
|
||||
console.error('音频预加载失败:', error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!clientReady.value || !playerContainer.value) return
|
||||
const bottomReached = window.innerHeight + window.scrollY + 1 >= document.body.offsetHeight
|
||||
const chatDialog = document.querySelector('.chatdialog')
|
||||
|
||||
if (isMobileDevice()) {
|
||||
if (bottomReached) {
|
||||
playerContainer.value.style.left = '-50%'
|
||||
if (chatDialog) {
|
||||
chatDialog.style.left = '-50%'
|
||||
}
|
||||
} else {
|
||||
playerContainer.value.style.left = '0%'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isMobileDevice = () => {
|
||||
if (!clientReady.value) return false
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
let isPlaying = false // 添加播放状态标志
|
||||
|
||||
const showDialog = ref(false)
|
||||
const currentDialog = ref('')
|
||||
|
||||
let lastPlayedIndex = -1 // 添加上一次播放的索引记录
|
||||
|
||||
// 添加防抖处理
|
||||
const debounce = (fn, delay) => {
|
||||
let timer = null
|
||||
return function (...args) {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args)
|
||||
timer = null
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
// 在组件作用域添加重置状态引用
|
||||
const resetBonesState = ref(null)
|
||||
|
||||
// 点击处理函数
|
||||
const handlePlayerClick = debounce(async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// 检查是否正在播放
|
||||
if (!isPlaying) {
|
||||
isPlaying = true
|
||||
isEyeControlDisabled.value = true
|
||||
|
||||
// 点击时重置眼睛位置
|
||||
resetBonesState.value?.()
|
||||
|
||||
const currentConfig = spineAssets[currentCharacter.value].voiceConfig
|
||||
let randomIndex
|
||||
do {
|
||||
randomIndex = Math.floor(Math.random() * currentConfig.length)
|
||||
} while (randomIndex === lastPlayedIndex && currentConfig.length > 1)
|
||||
|
||||
lastPlayedIndex = randomIndex
|
||||
const selectedPair = currentConfig[randomIndex]
|
||||
|
||||
try {
|
||||
const buffer = await AudioManager.loadAudioFile(selectedPair.audio);
|
||||
if (!buffer) throw new Error('音频加载失败');
|
||||
|
||||
currentDialog.value = selectedPair.text;
|
||||
showDialog.value = true;
|
||||
|
||||
currentAnimationState.addAnimation(2, selectedPair.animation, false, 0);
|
||||
|
||||
// 播放音频并等待结束
|
||||
await AudioManager.playAudio(buffer);
|
||||
|
||||
// 音频播放结束后清理状态
|
||||
isPlaying = false;
|
||||
isEyeControlDisabled.value = false;
|
||||
currentAnimationState.setEmptyAnimation(2, 0);
|
||||
showDialog.value = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('音频播放失败:', error);
|
||||
isPlaying = false;
|
||||
isEyeControlDisabled.value = false;
|
||||
showDialog.value = false;
|
||||
}
|
||||
}
|
||||
}, 300)
|
||||
|
||||
// 提升 moveBones 函数到组件作用域以便在其他地方使用
|
||||
let moveBonesHandler = null
|
||||
|
||||
const initializeSpinePlayer = async (assets) => {
|
||||
try {
|
||||
// 清理旧的实例
|
||||
if (blinkInterval) {
|
||||
clearTimeout(blinkInterval);
|
||||
}
|
||||
|
||||
// 清理容器内容
|
||||
if (playerContainer.value) {
|
||||
playerContainer.value.innerHTML = '';
|
||||
}
|
||||
|
||||
player = new spine.SpinePlayer(playerContainer.value, {
|
||||
skelUrl: assets.skelUrl,
|
||||
atlasUrl: assets.atlasUrl,
|
||||
premultipliedAlpha: true,
|
||||
backgroundColor: '#00000000',
|
||||
alpha: true,
|
||||
showControls: false,
|
||||
success: function (playerInstance) {
|
||||
playerInstance.setAnimation(assets.idleAnimationName, true)
|
||||
const skeleton = playerInstance.skeleton
|
||||
const animationState = playerInstance.animationState
|
||||
currentAnimationState = animationState // 保存动画状态引用
|
||||
|
||||
const rightEyeBone = skeleton.findBone(assets.rightEyeBone)
|
||||
const leftEyeBone = skeleton.findBone(assets.leftEyeBone)
|
||||
const frontHeadBone = skeleton.findBone(assets.frontHeadBone)
|
||||
const backHeadBone = skeleton.findBone(assets.backHeadBone)
|
||||
|
||||
const rightEyeCenterX = rightEyeBone ? rightEyeBone.data.x : 0
|
||||
const rightEyeCenterY = rightEyeBone ? rightEyeBone.data.y : 0
|
||||
const leftEyeCenterX = leftEyeBone ? leftEyeBone.data.x : 0
|
||||
const leftEyeCenterY = leftEyeBone ? leftEyeBone.data.y : 0
|
||||
const frontHeadCenterX = frontHeadBone ? frontHeadBone.data.x : 0
|
||||
const frontHeadCenterY = frontHeadBone ? frontHeadBone.data.y : 0
|
||||
const backHeadCenterX = backHeadBone ? backHeadBone.data.x : 0
|
||||
const backHeadCenterY = backHeadBone ? backHeadBone.data.y : 0
|
||||
|
||||
// 骨骼移动限制
|
||||
const maxRadius = 15
|
||||
const frontHeadMaxRadius = 2
|
||||
const backHeadMaxRadius = 1
|
||||
|
||||
function rotateVector(x, y, angle) {
|
||||
const cos = Math.cos(angle)
|
||||
const sin = Math.sin(angle)
|
||||
return {
|
||||
x: x * cos - y * sin,
|
||||
y: x * sin + y * cos
|
||||
}
|
||||
}
|
||||
|
||||
function moveBones(event) {
|
||||
// 如果眼睛控制被禁用,直接返回
|
||||
if (isEyeControlDisabled.value) return
|
||||
|
||||
const containerRect = playerContainer.value.getBoundingClientRect()
|
||||
|
||||
const mouseX = event.clientX - (containerRect.right - containerRect.width / 2)
|
||||
const mouseY = event.clientY - (containerRect.bottom - containerRect.height * 4 / 5)
|
||||
|
||||
// 将鼠标坐标偏移量进行逆旋转
|
||||
const eyeRotation = assets.eyeRotationAngle * (Math.PI / 180) // 眼睛旋转角度
|
||||
const rotatedMouse = rotateVector(mouseX, mouseY, -eyeRotation)
|
||||
const offsetX = rotatedMouse.x
|
||||
const offsetY = rotatedMouse.y
|
||||
const distance = Math.sqrt(offsetX * offsetX + offsetY * offsetY)
|
||||
|
||||
const angle = Math.atan2(offsetY, offsetX)
|
||||
const maxDistance = Math.min(distance, maxRadius)
|
||||
const dx = -maxDistance * Math.cos(angle)
|
||||
const dy = maxDistance * Math.sin(angle)
|
||||
|
||||
// 眼睛移动
|
||||
if (rightEyeBone) {
|
||||
rightEyeBone.x = rightEyeCenterX + dx
|
||||
rightEyeBone.y = rightEyeCenterY + dy
|
||||
}
|
||||
|
||||
if (leftEyeBone) {
|
||||
leftEyeBone.x = leftEyeCenterX + dx
|
||||
leftEyeBone.y = leftEyeCenterY + dy
|
||||
}
|
||||
|
||||
// 头部轻微移动
|
||||
const frontHeadDx = Math.min(distance, frontHeadMaxRadius) * Math.cos(angle)
|
||||
const frontHeadDy = Math.min(distance, frontHeadMaxRadius) * Math.sin(angle)
|
||||
|
||||
const backHeadDx = Math.min(distance, backHeadMaxRadius) * Math.cos(angle)
|
||||
const backHeadDy = Math.min(distance, backHeadMaxRadius) * Math.sin(angle)
|
||||
|
||||
if (frontHeadBone) {
|
||||
frontHeadBone.x = frontHeadCenterX - frontHeadDx
|
||||
frontHeadBone.y = frontHeadCenterY + frontHeadDy
|
||||
}
|
||||
|
||||
if (backHeadBone) {
|
||||
backHeadBone.x = backHeadCenterX + backHeadDx
|
||||
backHeadBone.y = backHeadCenterY - backHeadDy
|
||||
}
|
||||
|
||||
skeleton.updateWorldTransform()
|
||||
}
|
||||
|
||||
function resetBones() {
|
||||
if (rightEyeBone) {
|
||||
rightEyeBone.x = rightEyeCenterX
|
||||
rightEyeBone.y = rightEyeCenterY
|
||||
}
|
||||
|
||||
if (leftEyeBone) {
|
||||
leftEyeBone.x = leftEyeCenterX
|
||||
leftEyeBone.y = leftEyeCenterY
|
||||
}
|
||||
|
||||
if (frontHeadBone) {
|
||||
frontHeadBone.x = frontHeadCenterX
|
||||
frontHeadBone.y = frontHeadCenterY
|
||||
}
|
||||
|
||||
if (backHeadBone) {
|
||||
backHeadBone.x = backHeadCenterX
|
||||
backHeadBone.y = backHeadCenterY
|
||||
}
|
||||
|
||||
skeleton.updateWorldTransform()
|
||||
}
|
||||
|
||||
// 保存重置函数引用
|
||||
resetBonesState.value = resetBones
|
||||
|
||||
function playBlinkAnimation() {
|
||||
const randomTime = Math.random() * 3 + 3 // 5-8秒的随机间隔
|
||||
const shouldDoubleBlink = Math.random() > 0.5 // 随机决定是否连续播放两次
|
||||
|
||||
animationState.setAnimation(1, assets.eyeCloseAnimationName, false) // 在轨道1上播放眨眼动画
|
||||
|
||||
if (shouldDoubleBlink) {
|
||||
animationState.addAnimation(1, assets.eyeCloseAnimationName, false, 0.1) // 短暂停留后再播放一次
|
||||
}
|
||||
|
||||
// 随机时间后再调用眨眼动画
|
||||
blinkInterval = setTimeout(playBlinkAnimation, randomTime * 1000)
|
||||
}
|
||||
|
||||
// 修改鼠标移动监听器的添加逻辑
|
||||
if (!isMobileDevice()) {
|
||||
moveBonesHandler = moveBones
|
||||
window.addEventListener('mousemove', moveBonesHandler)
|
||||
}
|
||||
playBlinkAnimation()
|
||||
},
|
||||
error: function (playerInstance, reason) {
|
||||
console.error('Spine加载失败: ' + reason)
|
||||
}
|
||||
})
|
||||
} catch(err) {
|
||||
console.error('Failed to initialize spine player:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 将需要监听的状态提取为响应式引用
|
||||
const isDarkMode = computed(() => state.darkMode === 'dark')
|
||||
const isEnabled = computed(() => state.SpinePlayerEnabled)
|
||||
const currentAssets = computed(() => spineAssets[currentCharacter.value])
|
||||
|
||||
// 事件委托
|
||||
const handleEvents = (event) => {
|
||||
if (event.type === 'scroll') {
|
||||
handleScroll()
|
||||
} else if (['mousemove', 'touchmove'].includes(event.type)) {
|
||||
moveBonesHandler?.(event)
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的清理函数
|
||||
const cleanup = () => {
|
||||
if (blinkInterval) clearTimeout(blinkInterval)
|
||||
if (eyeControlTimer) clearTimeout(eyeControlTimer)
|
||||
|
||||
// 清理监听事件
|
||||
window.removeEventListener('scroll', handleEvents)
|
||||
if (moveBonesHandler && !isMobileDevice()) {
|
||||
window.removeEventListener('mousemove', moveBonesHandler)
|
||||
moveBonesHandler = null
|
||||
}
|
||||
|
||||
if (playerContainer.value) {
|
||||
playerContainer.value.innerHTML = ''
|
||||
}
|
||||
if (player) {
|
||||
AudioManager.clear()
|
||||
player = null
|
||||
currentAnimationState = null
|
||||
}
|
||||
|
||||
// 使用 WeakRef 来管理音频资源
|
||||
if (clientReady.value && window.WeakRef) {
|
||||
audioPlayers = audioPlayers.filter(player => {
|
||||
const ref = new WeakRef(player)
|
||||
return ref.deref() !== null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
const initializeCharacter = async () => {
|
||||
cleanup()
|
||||
|
||||
if (!isEnabled.value || !playerContainer.value) return
|
||||
|
||||
currentCharacter.value = isDarkMode.value ? 'plana' : 'arona'
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
preloadAudio(),
|
||||
initializeSpinePlayer(currentAssets.value)
|
||||
])
|
||||
} catch (err) {
|
||||
console.error('初始化失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedInitialize = debounce(initializeCharacter, 300)
|
||||
|
||||
// 监听主题切换和spine开关
|
||||
watch([isDarkMode, isEnabled], async ([dark, enabled], [prevDark, prevEnabled]) => {
|
||||
if (enabled !== prevEnabled) {
|
||||
if (enabled) {
|
||||
// 启用时初始化
|
||||
debouncedInitialize()
|
||||
} else {
|
||||
// 禁用时清理资源
|
||||
cleanup()
|
||||
}
|
||||
} else if (enabled && dark !== prevDark) {
|
||||
// 主题变更且启用状态下重新初始化
|
||||
debouncedInitialize()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// 设置客户端就绪状态
|
||||
clientReady.value = true
|
||||
|
||||
const options = { passive: true }
|
||||
window.addEventListener('scroll', handleEvents, options)
|
||||
if (!isMobileDevice()) {
|
||||
window.addEventListener('mousemove', handleEvents, options)
|
||||
}
|
||||
|
||||
// 如果启用了Spine播放器,初始化
|
||||
if (state.SpinePlayerEnabled) {
|
||||
debouncedInitialize()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.playerContainer {
|
||||
position: fixed;
|
||||
bottom: 25px;
|
||||
left: 0%;
|
||||
z-index: 100;
|
||||
width: 12vw;
|
||||
height: 24vw;
|
||||
filter: drop-shadow(0 0 3px rgba(40, 42, 44, 0.42));
|
||||
transition: all 1s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chatdialog-container {
|
||||
position: fixed;
|
||||
bottom: 10vw;
|
||||
left: 2vw;
|
||||
z-index: 101;
|
||||
transition: all 1s;
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 0 3px rgba(36, 36, 36, 0.6));
|
||||
}
|
||||
|
||||
.chatdialog-triangle {
|
||||
position: absolute;
|
||||
left: 2vw;
|
||||
top: -10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-bottom: 10px solid rgba(255, 255, 255, 0.9);
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.chatdialog {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 25px;
|
||||
padding: 12px 24px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.4;
|
||||
color: #000000;
|
||||
font-size: 0.8vw;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// 添加淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chatdialog-container {
|
||||
left: 2vh;
|
||||
bottom: 10vh;
|
||||
}
|
||||
|
||||
.chatdialog {
|
||||
min-width: auto;
|
||||
padding: 12px 20px;
|
||||
font-size: 1vh;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.chatdialog-triangle {
|
||||
left: 35px;
|
||||
border-width: 8px;
|
||||
top: -8px;
|
||||
}
|
||||
|
||||
.playerContainer {
|
||||
width: 15vh;
|
||||
height: 30vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
408
.vitepress/theme/components/Spine-Player/spine-player.css
Normal file
@@ -0,0 +1,408 @@
|
||||
/** Player **/
|
||||
.spine-player {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spine-player * {
|
||||
box-sizing: border-box;
|
||||
font-family: "PT Sans",Arial,"Helvetica Neue",Helvetica,Tahoma,sans-serif;
|
||||
color: #dddddd;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.spine-player-error {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: black;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.spine-player-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/** Slider **/
|
||||
.spine-player-slider {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spine-player-slider-value {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background: rgba(98, 176, 238, 0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spine-player-slider:hover .spine-player-slider-value {
|
||||
height: 4px;
|
||||
background: rgba(98, 176, 238, 1);
|
||||
transition: height 0.2s;
|
||||
}
|
||||
|
||||
.spine-player-slider-value.hovering {
|
||||
height: 4px;
|
||||
background: rgba(98, 176, 238, 1);
|
||||
transition: height 0.2s;
|
||||
}
|
||||
|
||||
.spine-player-slider.big {
|
||||
height: 12px;
|
||||
background: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.spine-player-slider.big .spine-player-slider-value {
|
||||
height: 12px;
|
||||
background: rgba(98, 176, 238, 1);
|
||||
}
|
||||
|
||||
/** Column and row layout elements **/
|
||||
.spine-player-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spine-player-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/** List **/
|
||||
.spine-player-list {
|
||||
list-style: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.spine-player-list li {
|
||||
cursor: pointer;
|
||||
margin: 8px 8px;
|
||||
}
|
||||
|
||||
.spine-player-list .selectable {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 !important;
|
||||
padding: 2px 20px 2px 0 !important;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable:first-child {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
.spine-player-list li.selectable:last-child {
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable:hover {
|
||||
background: #6e6e6e;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable .selectable-circle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 6px;
|
||||
min-width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
align-self: center;
|
||||
opacity: 0;
|
||||
margin: 5px 10px;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable.selected .selectable-circle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable .selectable-text {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.spine-player-list li.selectable.selected .selectable-text, .spine-player-list li.selectable:hover .selectable-text {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
/** Switch **/
|
||||
.spine-player-switch {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 2px 10px;
|
||||
}
|
||||
|
||||
.spine-player-switch-text {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.spine-player-switch-knob-area {
|
||||
width: 30px; /* width of the switch */
|
||||
height: 10px;
|
||||
display: block;
|
||||
border-radius: 5px; /* must be half of height */
|
||||
background: #6e6e6e;
|
||||
position: relative;
|
||||
align-self: center;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
.spine-player-switch.active .spine-player-switch-knob-area {
|
||||
background: #5EAFF1;
|
||||
}
|
||||
|
||||
.spine-player-switch-knob {
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #9e9e9e;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: -2px;
|
||||
filter: drop-shadow(0 0 1px #333);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.spine-player-switch.active .spine-player-switch-knob {
|
||||
background: #fff;
|
||||
transform: translateX(18px);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/** Popup **/
|
||||
.spine-player-popup-parent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spine-player-popup {
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
z-index: 1;
|
||||
right: 2px;
|
||||
bottom: 40px;
|
||||
border-radius: 4px;
|
||||
max-height: 400%;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.spine-player-popup-title {
|
||||
margin: 4px 15px 2px 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spine-player-popup hr {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #cccccc70;
|
||||
}
|
||||
|
||||
/** Canvas **/
|
||||
.spine-player canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/** Player controls **/
|
||||
.spine-player-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
.spine-player-controls-hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
/** Player buttons **/
|
||||
.spine-player-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
padding: 2px 8px 3px;
|
||||
}
|
||||
|
||||
.spine-player-button {
|
||||
background: none;
|
||||
outline: 0;
|
||||
border: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-size: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
margin-right: 3px;
|
||||
padding-bottom: 3px;
|
||||
filter: drop-shadow(0 0 1px #333);
|
||||
}
|
||||
|
||||
.spine-player-button-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.spine-player-button-icon-play {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplay%3C%2Ftitle%3E%3Cg%20id%3D%22play%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2243%2023.3%204%2047%204%201%2043%2023.3%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-play:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplay%3C%2Ftitle%3E%3Cg%20id%3D%22play%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2243%2023.3%204%2047%204%201%2043%2023.3%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-play-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplay%3C%2Ftitle%3E%3Cg%20id%3D%22play%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2243%2023.3%204%2047%204%201%2043%2023.3%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-pause {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Epause%3C%2Ftitle%3E%3Cg%20id%3D%22pause%22%3E%3Crect%20class%3D%22cls-1%22%20x%3D%226%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3Crect%20class%3D%22cls-1%22%20x%3D%2228%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-pause:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Epause%3C%2Ftitle%3E%3Cg%20id%3D%22pause%22%3E%3Crect%20class%3D%22cls-1%22%20x%3D%226%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3Crect%20class%3D%22cls-1%22%20x%3D%2228%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-pause-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Epause%3C%2Ftitle%3E%3Cg%20id%3D%22pause%22%3E%3Crect%20class%3D%22cls-1%22%20x%3D%226%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3Crect%20class%3D%22cls-1%22%20x%3D%2228%22%20y%3D%221%22%20width%3D%2213%22%20height%3D%2246%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-speed {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20id%3D%22playback%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplayback%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M48%2C28V20l-4.7-1.18a20.16%2C20.16%2C0%2C0%2C0-2-4.81l2.49-4.15L38.14%2C4.2%2C34%2C6.69a20.16%2C20.16%2C0%2C0%2C0-4.81-2L28%2C0H20L18.82%2C4.7A20.16%2C20.16%2C0%2C0%2C0%2C14%2C6.7L9.86%2C4.2%2C4.2%2C9.86%2C6.69%2C14a20.16%2C20.16%2C0%2C0%2C0-2%2C4.81L0%2C20v8l4.7%2C1.18A20.16%2C20.16%2C0%2C0%2C0%2C6.7%2C34L4.2%2C38.14%2C9.86%2C43.8%2C14%2C41.31a20.16%2C20.16%2C0%2C0%2C0%2C4.81%2C2L20%2C48h8l1.18-4.7a20.16%2C20.16%2C0%2C0%2C0%2C4.81-2l4.15%2C2.49%2C5.66-5.66L41.31%2C34a20.16%2C20.16%2C0%2C0%2C0%2C2-4.81ZM24%2C38A14%2C14%2C0%2C1%2C1%2C38%2C24%2C14%2C14%2C0%2C0%2C1%2C24%2C38Z%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2234%2024%2018%2033%2018%2015%2034%2024%2034%2024%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-speed:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20id%3D%22playback%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplayback%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M48%2C28V20l-4.7-1.18a20.16%2C20.16%2C0%2C0%2C0-2-4.81l2.49-4.15L38.14%2C4.2%2C34%2C6.69a20.16%2C20.16%2C0%2C0%2C0-4.81-2L28%2C0H20L18.82%2C4.7A20.16%2C20.16%2C0%2C0%2C0%2C14%2C6.7L9.86%2C4.2%2C4.2%2C9.86%2C6.69%2C14a20.16%2C20.16%2C0%2C0%2C0-2%2C4.81L0%2C20v8l4.7%2C1.18A20.16%2C20.16%2C0%2C0%2C0%2C6.7%2C34L4.2%2C38.14%2C9.86%2C43.8%2C14%2C41.31a20.16%2C20.16%2C0%2C0%2C0%2C4.81%2C2L20%2C48h8l1.18-4.7a20.16%2C20.16%2C0%2C0%2C0%2C4.81-2l4.15%2C2.49%2C5.66-5.66L41.31%2C34a20.16%2C20.16%2C0%2C0%2C0%2C2-4.81ZM24%2C38A14%2C14%2C0%2C1%2C1%2C38%2C24%2C14%2C14%2C0%2C0%2C1%2C24%2C38Z%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2234%2024%2018%2033%2018%2015%2034%2024%2034%2024%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-speed-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20id%3D%22playback%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eplayback%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M48%2C28V20l-4.7-1.18a20.16%2C20.16%2C0%2C0%2C0-2-4.81l2.49-4.15L38.14%2C4.2%2C34%2C6.69a20.16%2C20.16%2C0%2C0%2C0-4.81-2L28%2C0H20L18.82%2C4.7A20.16%2C20.16%2C0%2C0%2C0%2C14%2C6.7L9.86%2C4.2%2C4.2%2C9.86%2C6.69%2C14a20.16%2C20.16%2C0%2C0%2C0-2%2C4.81L0%2C20v8l4.7%2C1.18A20.16%2C20.16%2C0%2C0%2C0%2C6.7%2C34L4.2%2C38.14%2C9.86%2C43.8%2C14%2C41.31a20.16%2C20.16%2C0%2C0%2C0%2C4.81%2C2L20%2C48h8l1.18-4.7a20.16%2C20.16%2C0%2C0%2C0%2C4.81-2l4.15%2C2.49%2C5.66-5.66L41.31%2C34a20.16%2C20.16%2C0%2C0%2C0%2C2-4.81ZM24%2C38A14%2C14%2C0%2C1%2C1%2C38%2C24%2C14%2C14%2C0%2C0%2C1%2C24%2C38Z%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2234%2024%2018%2033%2018%2015%2034%2024%2034%2024%22%2F%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-animations {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eanimations%3C%2Ftitle%3E%3Cg%20id%3D%22animations%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12%2C45V43.22a6.39%2C6.39%2C0%2C0%2C0%2C.63-.81%2C27.83%2C27.83%2C0%2C0%2C1%2C3.79-4.16c.93-.84%2C2.06-1.88%2C2.86-2.71a13.83%2C13.83%2C0%2C0%2C0%2C1.53-1.9l3.9-5.24c1-1.17.95-1.1%2C2.11%2C0l3%2C2.24a4%2C4%2C0%2C0%2C0-2.29%2C2.38c-1.37%2C3-2.39%2C4-2.68%2C4.22l-.23.18c-.54.39-1.81%2C1-1.7%2C1.54l.8%2C1.49a4.5%2C4.5%2C0%2C0%2C1%2C.39%2C1l.57%2C2.15a.69.69%2C0%2C0%2C0%2C.58.48c.47.08%2C1%2C.5%2C1.33.53%2C1.29.1%2C1.79%2C0%2C1.42-.54L26.7%2C42.72a.86.86%2C0%2C0%2C1-.2-.24%2C3.64%2C3.64%2C0%2C0%2C1-.42-2.2A5.39%2C5.39%2C0%2C0%2C1%2C26.61%2C39c1.84-2%2C6.74-6.36%2C6.74-6.36%2C1.71-1.81%2C1.4-2.52.81-3.84a27.38%2C27.38%2C0%2C0%2C0-2-3c-.41-.61-2.08-2.38-2.85-3.28-.43-.5.38-2.08.87-2.82.18-.12-.41.05%2C1.72.07a23.32%2C23.32%2C0%2C0%2C0%2C3.56-.19l1.63.61c.28%2C0%2C1.18-.09%2C1.31-.35l.12-.78c.18-.39.31-1.56-.05-1.75l-.6-.52a2.28%2C2.28%2C0%2C0%2C0-1.61.07l-.2.44c-.14.15-.52.37-.71.29l-2.24%2C0c-.5.12-1.18-.42-1.81-.73L32.05%2C15a8%2C8%2C0%2C0%2C0%2C.8-3.92%2C1.22%2C1.22%2C0%2C0%2C0-.28-.82%2C7.87%2C7.87%2C0%2C0%2C0-1.15-1.06l.11-.73c-.12-.49%2C1-.82%2C1.52-.82l.76-.33c.32%2C0%2C.68-.89.78-1.21L34.94%2C4a11.26%2C11.26%2C0%2C0%2C0%2C0-1.61C34.57.08%2C30.06-1.42%2C28.78%2C2c-.14.38-.62.77.34%2C3.21a1.55%2C1.55%2C0%2C0%2C1-.3%2C1.2L28.4%2C7a4%2C4%2C0%2C0%2C1-1.19.49c-.79%2C0-1.59-.75-4%2C.54C21%2C9.16%2C18.59%2C13%2C17.7%2C14.22a3.21%2C3.21%2C0%2C0%2C0-.61%2C1.58c-.05%2C1.16.7%2C3.74.87%2C5.75.13%2C1.53.21%2C2.52.72%2C3.06%2C1.07%2C1.14%2C2.1-.18%2C2.61-1a2.74%2C2.74%2C0%2C0%2C0-.14-1.86l-.74-.1c-.15-.15-.4-.42-.39-.64-.05-3.48-.22-3.14-.18-5.39%2C1.74-1.46%2C2.4-2.45%2C2.3-2-.2%2C1.15.28%2C2.83.09%2C4.35a6.46%2C6.46%2C0%2C0%2C1-.7%2C2.58s-2.11%2C4.22-2.14%2C4.27l-1.26%2C5.6-.7%2C1.44s-.71.54-1.59%2C1.21a9.67%2C9.67%2C0%2C0%2C0-2.27%2C3.18%2C20.16%2C20.16%2C0%2C0%2C1-1.42%2C2.83l-.87%2C1.31a1.72%2C1.72%2C0%2C0%2C1-.6.61l-1.83%2C1.1a1.39%2C1.39%2C0%2C0%2C0-.16.93l.68%2C1.71a4.07%2C4.07%2C0%2C0%2C1%2C.27%2C1.07l.17%2C1.56a.75.75%2C0%2C0%2C0%2C.71.59%2C18.13%2C18.13%2C0%2C0%2C0%2C3.26-.5c.27-.09-.29-.78-.53-1s-.45-.36-.45-.36A12.78%2C12.78%2C0%2C0%2C1%2C12%2C45Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
|
||||
}
|
||||
|
||||
.spine-player-button-icon-animations:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eanimations%3C%2Ftitle%3E%3Cg%20id%3D%22animations%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12%2C45V43.22a6.39%2C6.39%2C0%2C0%2C0%2C.63-.81%2C27.83%2C27.83%2C0%2C0%2C1%2C3.79-4.16c.93-.84%2C2.06-1.88%2C2.86-2.71a13.83%2C13.83%2C0%2C0%2C0%2C1.53-1.9l3.9-5.24c1-1.17.95-1.1%2C2.11%2C0l3%2C2.24a4%2C4%2C0%2C0%2C0-2.29%2C2.38c-1.37%2C3-2.39%2C4-2.68%2C4.22l-.23.18c-.54.39-1.81%2C1-1.7%2C1.54l.8%2C1.49a4.5%2C4.5%2C0%2C0%2C1%2C.39%2C1l.57%2C2.15a.69.69%2C0%2C0%2C0%2C.58.48c.47.08%2C1%2C.5%2C1.33.53%2C1.29.1%2C1.79%2C0%2C1.42-.54L26.7%2C42.72a.86.86%2C0%2C0%2C1-.2-.24%2C3.64%2C3.64%2C0%2C0%2C1-.42-2.2A5.39%2C5.39%2C0%2C0%2C1%2C26.61%2C39c1.84-2%2C6.74-6.36%2C6.74-6.36%2C1.71-1.81%2C1.4-2.52.81-3.84a27.38%2C27.38%2C0%2C0%2C0-2-3c-.41-.61-2.08-2.38-2.85-3.28-.43-.5.38-2.08.87-2.82.18-.12-.41.05%2C1.72.07a23.32%2C23.32%2C0%2C0%2C0%2C3.56-.19l1.63.61c.28%2C0%2C1.18-.09%2C1.31-.35l.12-.78c.18-.39.31-1.56-.05-1.75l-.6-.52a2.28%2C2.28%2C0%2C0%2C0-1.61.07l-.2.44c-.14.15-.52.37-.71.29l-2.24%2C0c-.5.12-1.18-.42-1.81-.73L32.05%2C15a8%2C8%2C0%2C0%2C0%2C.8-3.92%2C1.22%2C1.22%2C0%2C0%2C0-.28-.82%2C7.87%2C7.87%2C0%2C0%2C0-1.15-1.06l.11-.73c-.12-.49%2C1-.82%2C1.52-.82l.76-.33c.32%2C0%2C.68-.89.78-1.21L34.94%2C4a11.26%2C11.26%2C0%2C0%2C0%2C0-1.61C34.57.08%2C30.06-1.42%2C28.78%2C2c-.14.38-.62.77.34%2C3.21a1.55%2C1.55%2C0%2C0%2C1-.3%2C1.2L28.4%2C7a4%2C4%2C0%2C0%2C1-1.19.49c-.79%2C0-1.59-.75-4%2C.54C21%2C9.16%2C18.59%2C13%2C17.7%2C14.22a3.21%2C3.21%2C0%2C0%2C0-.61%2C1.58c-.05%2C1.16.7%2C3.74.87%2C5.75.13%2C1.53.21%2C2.52.72%2C3.06%2C1.07%2C1.14%2C2.1-.18%2C2.61-1a2.74%2C2.74%2C0%2C0%2C0-.14-1.86l-.74-.1c-.15-.15-.4-.42-.39-.64-.05-3.48-.22-3.14-.18-5.39%2C1.74-1.46%2C2.4-2.45%2C2.3-2-.2%2C1.15.28%2C2.83.09%2C4.35a6.46%2C6.46%2C0%2C0%2C1-.7%2C2.58s-2.11%2C4.22-2.14%2C4.27l-1.26%2C5.6-.7%2C1.44s-.71.54-1.59%2C1.21a9.67%2C9.67%2C0%2C0%2C0-2.27%2C3.18%2C20.16%2C20.16%2C0%2C0%2C1-1.42%2C2.83l-.87%2C1.31a1.72%2C1.72%2C0%2C0%2C1-.6.61l-1.83%2C1.1a1.39%2C1.39%2C0%2C0%2C0-.16.93l.68%2C1.71a4.07%2C4.07%2C0%2C0%2C1%2C.27%2C1.07l.17%2C1.56a.75.75%2C0%2C0%2C0%2C.71.59%2C18.13%2C18.13%2C0%2C0%2C0%2C3.26-.5c.27-.09-.29-.78-.53-1s-.45-.36-.45-.36A12.78%2C12.78%2C0%2C0%2C1%2C12%2C45Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
|
||||
}
|
||||
|
||||
.spine-player-button-icon-animations-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eanimations%3C%2Ftitle%3E%3Cg%20id%3D%22animations%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12%2C45V43.22a6.39%2C6.39%2C0%2C0%2C0%2C.63-.81%2C27.83%2C27.83%2C0%2C0%2C1%2C3.79-4.16c.93-.84%2C2.06-1.88%2C2.86-2.71a13.83%2C13.83%2C0%2C0%2C0%2C1.53-1.9l3.9-5.24c1-1.17.95-1.1%2C2.11%2C0l3%2C2.24a4%2C4%2C0%2C0%2C0-2.29%2C2.38c-1.37%2C3-2.39%2C4-2.68%2C4.22l-.23.18c-.54.39-1.81%2C1-1.7%2C1.54l.8%2C1.49a4.5%2C4.5%2C0%2C0%2C1%2C.39%2C1l.57%2C2.15a.69.69%2C0%2C0%2C0%2C.58.48c.47.08%2C1%2C.5%2C1.33.53%2C1.29.1%2C1.79%2C0%2C1.42-.54L26.7%2C42.72a.86.86%2C0%2C0%2C1-.2-.24%2C3.64%2C3.64%2C0%2C0%2C1-.42-2.2A5.39%2C5.39%2C0%2C0%2C1%2C26.61%2C39c1.84-2%2C6.74-6.36%2C6.74-6.36%2C1.71-1.81%2C1.4-2.52.81-3.84a27.38%2C27.38%2C0%2C0%2C0-2-3c-.41-.61-2.08-2.38-2.85-3.28-.43-.5.38-2.08.87-2.82.18-.12-.41.05%2C1.72.07a23.32%2C23.32%2C0%2C0%2C0%2C3.56-.19l1.63.61c.28%2C0%2C1.18-.09%2C1.31-.35l.12-.78c.18-.39.31-1.56-.05-1.75l-.6-.52a2.28%2C2.28%2C0%2C0%2C0-1.61.07l-.2.44c-.14.15-.52.37-.71.29l-2.24%2C0c-.5.12-1.18-.42-1.81-.73L32.05%2C15a8%2C8%2C0%2C0%2C0%2C.8-3.92%2C1.22%2C1.22%2C0%2C0%2C0-.28-.82%2C7.87%2C7.87%2C0%2C0%2C0-1.15-1.06l.11-.73c-.12-.49%2C1-.82%2C1.52-.82l.76-.33c.32%2C0%2C.68-.89.78-1.21L34.94%2C4a11.26%2C11.26%2C0%2C0%2C0%2C0-1.61C34.57.08%2C30.06-1.42%2C28.78%2C2c-.14.38-.62.77.34%2C3.21a1.55%2C1.55%2C0%2C0%2C1-.3%2C1.2L28.4%2C7a4%2C4%2C0%2C0%2C1-1.19.49c-.79%2C0-1.59-.75-4%2C.54C21%2C9.16%2C18.59%2C13%2C17.7%2C14.22a3.21%2C3.21%2C0%2C0%2C0-.61%2C1.58c-.05%2C1.16.7%2C3.74.87%2C5.75.13%2C1.53.21%2C2.52.72%2C3.06%2C1.07%2C1.14%2C2.1-.18%2C2.61-1a2.74%2C2.74%2C0%2C0%2C0-.14-1.86l-.74-.1c-.15-.15-.4-.42-.39-.64-.05-3.48-.22-3.14-.18-5.39%2C1.74-1.46%2C2.4-2.45%2C2.3-2-.2%2C1.15.28%2C2.83.09%2C4.35a6.46%2C6.46%2C0%2C0%2C1-.7%2C2.58s-2.11%2C4.22-2.14%2C4.27l-1.26%2C5.6-.7%2C1.44s-.71.54-1.59%2C1.21a9.67%2C9.67%2C0%2C0%2C0-2.27%2C3.18%2C20.16%2C20.16%2C0%2C0%2C1-1.42%2C2.83l-.87%2C1.31a1.72%2C1.72%2C0%2C0%2C1-.6.61l-1.83%2C1.1a1.39%2C1.39%2C0%2C0%2C0-.16.93l.68%2C1.71a4.07%2C4.07%2C0%2C0%2C1%2C.27%2C1.07l.17%2C1.56a.75.75%2C0%2C0%2C0%2C.71.59%2C18.13%2C18.13%2C0%2C0%2C0%2C3.26-.5c.27-.09-.29-.78-.53-1s-.45-.36-.45-.36A12.78%2C12.78%2C0%2C0%2C1%2C12%2C45Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
|
||||
}
|
||||
|
||||
.spine-player-button-icon-skins {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eskins%3C%2Ftitle%3E%3Cg%20id%3D%22skins%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M36%2C12.54l-6.92%2C1-.79%2C1.2c-1%2C.25-2-.62-3-.55V12.33a1.35%2C1.35%2C0%2C0%2C1%2C.55-1.07c3-2.24%2C3.28-3.75%2C3.28-5.34A5.06%2C5.06%2C0%2C0%2C0%2C24%2C.76c-2.54%2C0-4.38.71-5.49%2C2.13a5.74%2C5.74%2C0%2C0%2C0-.9%2C4.57l2.48-.61a3.17%2C3.17%2C0%2C0%2C1%2C.45-2.4c.6-.75%2C1.75-1.13%2C3.42-1.13%2C2.56%2C0%2C2.56%2C1.24%2C2.56%2C2.56%2C0%2C.92%2C0%2C1.65-2.26%2C3.34a3.92%2C3.92%2C0%2C0%2C0-1.58%2C3.12v1.86c-1-.07-2%2C.8-3%2C.55l-.79-1.2-6.92-1c-2.25%2C0-4.35%2C2.09-5.64%2C3.93L1%2C24c3.83%2C5.11%2C10.22%2C5.11%2C10.22%2C5.11V41.93c0%2C2.34%2C2.68%2C3.88%2C5.59%2C4.86a22.59%2C22.59%2C0%2C0%2C0%2C14.37%2C0c2.91-1%2C5.59-2.52%2C5.59-4.86V29.15S43.17%2C29.15%2C47%2C24l-5.33-7.57C40.38%2C14.63%2C38.27%2C12.54%2C36%2C12.54ZM23.32%2C20.09%2C21%2C17l1.8-.6a3.79%2C3.79%2C0%2C0%2C1%2C2.4%2C0L27%2C17l-2.32%2C3.09A.85.85%2C0%2C0%2C1%2C23.32%2C20.09Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.spine-player-button-icon-skins:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eskins%3C%2Ftitle%3E%3Cg%20id%3D%22skins%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M36%2C12.54l-6.92%2C1-.79%2C1.2c-1%2C.25-2-.62-3-.55V12.33a1.35%2C1.35%2C0%2C0%2C1%2C.55-1.07c3-2.24%2C3.28-3.75%2C3.28-5.34A5.06%2C5.06%2C0%2C0%2C0%2C24%2C.76c-2.54%2C0-4.38.71-5.49%2C2.13a5.74%2C5.74%2C0%2C0%2C0-.9%2C4.57l2.48-.61a3.17%2C3.17%2C0%2C0%2C1%2C.45-2.4c.6-.75%2C1.75-1.13%2C3.42-1.13%2C2.56%2C0%2C2.56%2C1.24%2C2.56%2C2.56%2C0%2C.92%2C0%2C1.65-2.26%2C3.34a3.92%2C3.92%2C0%2C0%2C0-1.58%2C3.12v1.86c-1-.07-2%2C.8-3%2C.55l-.79-1.2-6.92-1c-2.25%2C0-4.35%2C2.09-5.64%2C3.93L1%2C24c3.83%2C5.11%2C10.22%2C5.11%2C10.22%2C5.11V41.93c0%2C2.34%2C2.68%2C3.88%2C5.59%2C4.86a22.59%2C22.59%2C0%2C0%2C0%2C14.37%2C0c2.91-1%2C5.59-2.52%2C5.59-4.86V29.15S43.17%2C29.15%2C47%2C24l-5.33-7.57C40.38%2C14.63%2C38.27%2C12.54%2C36%2C12.54ZM23.32%2C20.09%2C21%2C17l1.8-.6a3.79%2C3.79%2C0%2C0%2C1%2C2.4%2C0L27%2C17l-2.32%2C3.09A.85.85%2C0%2C0%2C1%2C23.32%2C20.09Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-skins-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eskins%3C%2Ftitle%3E%3Cg%20id%3D%22skins%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M36%2C12.54l-6.92%2C1-.79%2C1.2c-1%2C.25-2-.62-3-.55V12.33a1.35%2C1.35%2C0%2C0%2C1%2C.55-1.07c3-2.24%2C3.28-3.75%2C3.28-5.34A5.06%2C5.06%2C0%2C0%2C0%2C24%2C.76c-2.54%2C0-4.38.71-5.49%2C2.13a5.74%2C5.74%2C0%2C0%2C0-.9%2C4.57l2.48-.61a3.17%2C3.17%2C0%2C0%2C1%2C.45-2.4c.6-.75%2C1.75-1.13%2C3.42-1.13%2C2.56%2C0%2C2.56%2C1.24%2C2.56%2C2.56%2C0%2C.92%2C0%2C1.65-2.26%2C3.34a3.92%2C3.92%2C0%2C0%2C0-1.58%2C3.12v1.86c-1-.07-2%2C.8-3%2C.55l-.79-1.2-6.92-1c-2.25%2C0-4.35%2C2.09-5.64%2C3.93L1%2C24c3.83%2C5.11%2C10.22%2C5.11%2C10.22%2C5.11V41.93c0%2C2.34%2C2.68%2C3.88%2C5.59%2C4.86a22.59%2C22.59%2C0%2C0%2C0%2C14.37%2C0c2.91-1%2C5.59-2.52%2C5.59-4.86V29.15S43.17%2C29.15%2C47%2C24l-5.33-7.57C40.38%2C14.63%2C38.27%2C12.54%2C36%2C12.54ZM23.32%2C20.09%2C21%2C17l1.8-.6a3.79%2C3.79%2C0%2C0%2C1%2C2.4%2C0L27%2C17l-2.32%2C3.09A.85.85%2C0%2C0%2C1%2C23.32%2C20.09Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-settings {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Esettings%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M40%2C3H8A5%2C5%2C0%2C0%2C0%2C3%2C8V40a5%2C5%2C0%2C0%2C0%2C5%2C5H40a5%2C5%2C0%2C0%2C0%2C5-5V8A5%2C5%2C0%2C0%2C0%2C40%2C3ZM16%2C40H9V33h7Zm0-12H9V21h7Zm0-12H9V9h7ZM39%2C38H20V35H39Zm0-12H20V23H39Zm0-12H20V11H39Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.spine-player-button-icon-settings:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Esettings%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M40%2C3H8A5%2C5%2C0%2C0%2C0%2C3%2C8V40a5%2C5%2C0%2C0%2C0%2C5%2C5H40a5%2C5%2C0%2C0%2C0%2C5-5V8A5%2C5%2C0%2C0%2C0%2C40%2C3ZM16%2C40H9V33h7Zm0-12H9V21h7Zm0-12H9V9h7ZM39%2C38H20V35H39Zm0-12H20V23H39Zm0-12H20V11H39Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-settings-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Esettings%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M40%2C3H8A5%2C5%2C0%2C0%2C0%2C3%2C8V40a5%2C5%2C0%2C0%2C0%2C5%2C5H40a5%2C5%2C0%2C0%2C0%2C5-5V8A5%2C5%2C0%2C0%2C0%2C40%2C3ZM16%2C40H9V33h7Zm0-12H9V21h7Zm0-12H9V9h7ZM39%2C38H20V35H39Zm0-12H20V23H39Zm0-12H20V11H39Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-fullscreen {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23fff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eexpand%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2230.14%208%2040%208%2040%2017.86%2044.5%2017.86%2044.5%203.5%2030.14%203.5%2030.14%208%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%228%2017.86%208%208%2017.86%208%2017.86%203.5%203.5%203.5%203.5%2017.86%208%2017.86%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2240%2030.14%2040%2040%2030.14%2040%2030.14%2044.5%2044.5%2044.5%2044.5%2030.14%2040%2030.14%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2217.86%2040%208%2040%208%2030.14%203.5%2030.14%203.5%2044.5%2017.86%2044.5%2017.86%2040%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.spine-player-button-icon-fullscreen:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eexpand%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2230.14%208%2040%208%2040%2017.86%2044.5%2017.86%2044.5%203.5%2030.14%203.5%2030.14%208%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%228%2017.86%208%208%2017.86%208%2017.86%203.5%203.5%203.5%203.5%2017.86%208%2017.86%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2240%2030.14%2040%2040%2030.14%2040%2030.14%2044.5%2044.5%2044.5%2044.5%2030.14%2040%2030.14%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2217.86%2040%208%2040%208%2030.14%203.5%2030.14%203.5%2044.5%2017.86%2044.5%2017.86%2040%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-fullscreen-selected {
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%2362B0EE%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Eexpand%3C%2Ftitle%3E%3Cg%20id%3D%22settings%22%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2230.14%208%2040%208%2040%2017.86%2044.5%2017.86%2044.5%203.5%2030.14%203.5%2030.14%208%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%228%2017.86%208%208%2017.86%208%2017.86%203.5%203.5%203.5%203.5%2017.86%208%2017.86%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2240%2030.14%2040%2040%2030.14%2040%2030.14%2044.5%2044.5%2044.5%2044.5%2030.14%2040%2030.14%22%2F%3E%3Cpolygon%20class%3D%22cls-1%22%20points%3D%2217.86%2040%208%2040%208%2030.14%203.5%2030.14%203.5%2044.5%2017.86%2044.5%2017.86%2040%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.spine-player-button-icon-spine-logo {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin: 0 8px !important;
|
||||
align-self: center;
|
||||
border: none !important;
|
||||
width: auto !important;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
box-shadow: none !important;
|
||||
filter: drop-shadow(0 0 1px #333);
|
||||
}
|
||||
|
||||
.spine-player-button-icon-spine-logo:hover {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/** Speed slider **/
|
||||
.spine-player-speed-slider {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
/** Player editor **/
|
||||
.spine-player-editor-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spine-player-editor-code {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.spine-player-editor-player {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: black;
|
||||
}
|
||||
13442
.vitepress/theme/components/Spine-Player/spine-player.js
Normal file
142
.vitepress/theme/components/Splash.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div v-if="isVisible" class="splash-container" v-html="svgContent"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, onUnmounted } from 'vue'
|
||||
import anime from 'animejs'
|
||||
|
||||
const svgContent =
|
||||
ref(`<svg viewBox="0 0 1728 1117" preserveAspectRatio="xMinYMin slice" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2_5)">
|
||||
<g class="triangle-group">
|
||||
<path d="M606 -123L1061 666H151L606 -123Z"/>
|
||||
<path d="M190.15 144L67.3 -68.94L313 -68.94L190.15 144Z"/>
|
||||
<path d="M1424.17 339L1249.35 35.97L1599 35.97L1424.17 339Z"/>
|
||||
<path d="M-96.7 513.333L-216.4 305.853H23L-96.7 513.333Z"/>
|
||||
<path d="M502.825 603.83L391 410L614.65 410L502.825 603.83Z"/>
|
||||
<path d="M1228.45 648.333L1048.9 337.113L1408 337.113L1228.45 648.333Z"/>
|
||||
<path d="M246.375 925.667L96.75 666.317H396L246.375 925.667Z"/>
|
||||
<path d="M606.375 1048.45L504 871L708.75 871L606.375 1048.45Z"/>
|
||||
<path d="M1376.5 960L1219 687H1534L1376.5 960Z"/>
|
||||
<path d="M365.395 170L503.791 409.885H227L365.395 170Z"/>
|
||||
<path d="M1049.36 337L1198.71 595.886H900L1049.36 337Z"/>
|
||||
<path d="M1248.81 36L1340.61 195.132H1157L1248.81 36Z"/>
|
||||
<path d="M503.99 680.333L614.981 872.716H393L503.99 680.333Z"/>
|
||||
<path d="M870.433 698.333L997.866 919.218H743L870.433 698.333Z"/>
|
||||
<path d="M1419.1 487L1534.2 686.508H1304L1419.1 487Z"/>
|
||||
<path d="M312.914 809L445.828 1039.38H180L312.914 809Z"/>
|
||||
<path d="M1225.51 1053.67L1368.01 1300.68H1083L1225.51 1053.67Z"/>
|
||||
<path d="M1550.51 792L1693.01 1039.01H1408L1550.51 792Z"/>
|
||||
</g>
|
||||
<g id="breathingParts">
|
||||
<path class="circle-path" fill-rule="evenodd" clip-rule="evenodd" d="M864 769C979.98 769 1074 674.98 1074 559C1074 443.02 979.98 349 864 349C748.02 349 654 443.02 654 559C654 674.98 748.02 769 864 769ZM864 749.909C969.436 749.909 1054.91 664.436 1054.91 559C1054.91 453.564 969.436 368.091 864 368.091C758.564 368.091 673.091 453.564 673.091 559C673.091 664.436 758.564 749.909 864 749.909Z"/>
|
||||
<path class="led-path" class="led-path" d="M934.636 392.273H792.727L757.091 447H970.909L934.636 392.273Z"/>
|
||||
<path class="led-path" d="M969.636 450.182H897.727L934.636 504.273L969.636 450.182Z"/>
|
||||
<path class="led-path" d="M792.091 500.455L828.364 447L900.909 555.182L863.364 609.273L792.091 500.455Z"/>
|
||||
<path class="led-path" d="M900.909 667.182L865.909 612.455L903.5 558.364L973.455 667.182L937.818 721.909H796.545L760.273 667.182L796.545 612.455L832.818 667.182H900.909Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="4" result="blur"/>
|
||||
<feFlood class="glow-color" flood-opacity="2.5"/>
|
||||
<feComposite in2="blur" operator="in"/>
|
||||
<feComposite in="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
`)
|
||||
const isVisible = ref(true)
|
||||
import { useStore } from '../store'
|
||||
const { state } = useStore()
|
||||
|
||||
const createBreathingAnimation = () => {
|
||||
anime({
|
||||
targets: '#breathingParts',
|
||||
opacity: [0.3, 1],
|
||||
easing: 'easeInOutSine',
|
||||
duration: 500,
|
||||
direction: 'alternate',
|
||||
loop: true,
|
||||
})
|
||||
}
|
||||
|
||||
const fadeOutSplash = () => {
|
||||
anime({
|
||||
targets: '.splash-container',
|
||||
opacity: 0,
|
||||
duration: 500,
|
||||
easing: 'easeInOutQuad',
|
||||
complete: () => {
|
||||
isVisible.value = false // 隐藏splash
|
||||
},
|
||||
})
|
||||
state.splashLoading = false
|
||||
}
|
||||
|
||||
const preventDefault = (e) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
createBreathingAnimation()
|
||||
// 禁用滚轮事件
|
||||
window.addEventListener('wheel', preventDefault, { passive: false })
|
||||
setTimeout(() => {
|
||||
fadeOutSplash()
|
||||
// 启用滚轮事件
|
||||
window.removeEventListener('wheel', preventDefault)
|
||||
}, Math.floor(Math.random() * 300) + 1200) // 随机等待时间
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 确保组件卸载时移除事件监听
|
||||
window.removeEventListener('wheel', preventDefault)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.splash-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(#b9e6f6, #ece5f4);
|
||||
z-index: 999;
|
||||
|
||||
:global(.triangle-group path) {
|
||||
fill: white;
|
||||
fill-opacity: 0.15;
|
||||
}
|
||||
|
||||
:global(.circle-path) {
|
||||
fill: #E0F0FA;
|
||||
fill-opacity: 0.9;
|
||||
}
|
||||
|
||||
:global(.led-path) {
|
||||
fill: white;
|
||||
filter: url(#glow);
|
||||
}
|
||||
|
||||
:global(.glow-color) {
|
||||
flood-color: white;
|
||||
}
|
||||
|
||||
html[theme='dark'] & {
|
||||
background: linear-gradient(#c3bde9, #fee4ff);
|
||||
|
||||
:global(.circle-path) {
|
||||
fill: #fee4ff;
|
||||
}
|
||||
|
||||
:global(.glow-color) {
|
||||
flood-color: #efe0fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
144
.vitepress/theme/components/Tags.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<ul class="tags">
|
||||
<li :class="['item', { active: active === tag }]" v-for="(_, tag) in tagData">
|
||||
<a href="javascript:void(0)" @click="setTag(tag)"
|
||||
><i class="iconfont icon-tag"></i> {{ tag }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { data as posts, type PostData } from '../utils/posts.data'
|
||||
import { ref, watch, onUnmounted, onMounted } from 'vue'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const active = ref<string | null>(null)
|
||||
const tagData: Record<string, PostData[]> = {}
|
||||
const { state } = useStore()
|
||||
|
||||
const setTag = (tag: string) => {
|
||||
active.value = tag
|
||||
state.selectedPosts = tagData[tag] || []
|
||||
state.currTag = tag
|
||||
|
||||
const url = new URL(window.location.href)
|
||||
|
||||
// 设置URL的tag参数
|
||||
if (tag && tag.trim() !== '') {
|
||||
url.searchParams.set('tag', tag)
|
||||
} else {
|
||||
url.searchParams.delete('tag')
|
||||
}
|
||||
|
||||
// 清除page参数
|
||||
const pageParam = url.searchParams.get('page')
|
||||
if (!pageParam || pageParam === '1') {
|
||||
url.searchParams.delete('page')
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', url.toString())
|
||||
}
|
||||
|
||||
for (const post of posts) {
|
||||
if (!post.tags) continue
|
||||
for (const tag of post.tags) {
|
||||
if (!tagData[tag]) tagData[tag] = []
|
||||
tagData[tag].push(post)
|
||||
}
|
||||
}
|
||||
|
||||
// 从URL获取tag
|
||||
function getTagFromUrl(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const tagParam = urlParams.get('tag')
|
||||
if (tagParam && tagData[tagParam]) {
|
||||
return tagParam
|
||||
}
|
||||
}
|
||||
return state.currTag || ''
|
||||
}
|
||||
|
||||
// 挂载组件时获取URL的tag
|
||||
onMounted(() => {
|
||||
const tagFromUrl = getTagFromUrl()
|
||||
|
||||
if (tagFromUrl) {
|
||||
setTag(tagFromUrl)
|
||||
} else if (state.currTag) {
|
||||
setTag(state.currTag)
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
const tagFromUrl = getTagFromUrl()
|
||||
if (tagFromUrl !== active.value) {
|
||||
setTag(tagFromUrl)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
() => state.currTag,
|
||||
(newTag) => {
|
||||
if (newTag !== active.value) {
|
||||
setTag(newTag)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
setTag('')
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.active a {
|
||||
background-color: var(--btn-hover) !important;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
background-color: var(--infobox-background-initial);
|
||||
border-radius: 32px;
|
||||
border: solid 2px var(--foreground-color);
|
||||
backdrop-filter: var(--blur-val);
|
||||
width: 768px;
|
||||
z-index: 100;
|
||||
|
||||
li {
|
||||
margin: 8px;
|
||||
|
||||
a {
|
||||
color: var(--font-color-grey);
|
||||
padding: 3px 5px;
|
||||
color: var(--font-color-gold);
|
||||
background-color: var(--btn-background);
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.5s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tags {
|
||||
width: auto;
|
||||
li {
|
||||
margin: 4px;
|
||||
a {
|
||||
font-size: 12px;
|
||||
.icon-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
.vitepress/theme/components/ToTop.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<a href="#" class="totop" @click="toTop" :style="style" aria-label="to-top"
|
||||
><img @dragstart.prevent src="../assets/toTop.svg" alt=""
|
||||
/></a>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
const hide = `bottom: -25%; right: -25%; opacity: 0;`
|
||||
const style = ref(hide)
|
||||
const onScroll = () => {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (window.scrollY > 600) {
|
||||
style.value = `bottom: 50px`
|
||||
} 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 scoped lang="less">
|
||||
.totop {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
right: 3%;
|
||||
filter: drop-shadow(0 0 8px #7171a9);
|
||||
transition: all 0.5s;
|
||||
img {
|
||||
width: 85px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.totop {
|
||||
right: 5%;
|
||||
img {
|
||||
width: 8vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
241
.vitepress/theme/components/Welcome-Box.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div
|
||||
class="welcome-box"
|
||||
ref="welcomeBoxRef"
|
||||
@mousemove="parallax"
|
||||
@mouseleave="reset"
|
||||
:style="{ transform: `rotateY(${calcY}deg) rotateX(${calcX}deg)` }"
|
||||
>
|
||||
<span class="welcome-text">{{ welcomeText }}</span>
|
||||
<transition name="fade" appear>
|
||||
<div
|
||||
class="info-box"
|
||||
:style="{
|
||||
background: `linear-gradient(${angle}deg, var(--infobox-background-initial), var(--infobox-background-final))`,
|
||||
}"
|
||||
>
|
||||
<img @dragstart.prevent src="../assets/banner/avatar.webp" alt="" class="avatar" />
|
||||
<span class="name">{{ name }}</span>
|
||||
<span class="motto">
|
||||
{{ mottoText }}
|
||||
<span class="pointer"></span>
|
||||
</span>
|
||||
<ul>
|
||||
<li v-for="item in social" :key="item.url">
|
||||
<a :href="item.url" target="_blank" rel="noopener noreferrer">
|
||||
<i :class="`iconfont icon-${item.icon} social`"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const themeConfig = useData().theme.value
|
||||
const name = themeConfig.name
|
||||
const welcomeText = themeConfig.welcomeText
|
||||
const motto = themeConfig.motto
|
||||
const social = themeConfig.social
|
||||
|
||||
const multiple = 30
|
||||
const welcomeBoxRef = ref<HTMLElement | null>(null)
|
||||
const calcY = ref(0)
|
||||
const calcX = ref(0)
|
||||
const angle = ref(0)
|
||||
|
||||
const parallax = (e: MouseEvent) => {
|
||||
if (welcomeBoxRef.value) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const box = welcomeBoxRef.value!.getBoundingClientRect()
|
||||
calcY.value = (e.clientX - box.x - box.width / 2) / multiple
|
||||
calcX.value = -(e.clientY - box.y - box.height / 2) / multiple
|
||||
angle.value = Math.floor(
|
||||
getMouseAngle(e.clientY - box.y - box.height / 2, e.clientX - box.x - box.width / 2),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getMouseAngle = (x: number, y: number) => {
|
||||
const radians = Math.atan2(y, x)
|
||||
let angle = radians * (180 / Math.PI)
|
||||
|
||||
if (angle < 0) {
|
||||
angle += 360
|
||||
}
|
||||
|
||||
return angle
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
calcX.value = calcY.value = angle.value = 0
|
||||
}
|
||||
|
||||
let index = 0
|
||||
const mottoText = ref('')
|
||||
const randomMotto = motto[Math.floor(Math.random() * motto.length)]
|
||||
|
||||
const addNextCharacter = () => {
|
||||
if (index < randomMotto.length) {
|
||||
mottoText.value += randomMotto[index]
|
||||
index++
|
||||
setTimeout(addNextCharacter, Math.random() * 150 + 50)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addNextCharacter()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.welcome-box {
|
||||
margin-top: 4.2vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
transition: transform 0.2s, color 0.5s, text-shadow 0.5s;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 4.5vw;
|
||||
font-weight: bold;
|
||||
color: var(--welcome-text-color);
|
||||
text-shadow: var(--welcome-text-shadow);
|
||||
text-align: center;
|
||||
margin-bottom: 5vw;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 6vh 2vw 3vh;
|
||||
width: 40vw;
|
||||
border-radius: 3vw;
|
||||
box-shadow: var(--info-box-shadow);
|
||||
backdrop-filter: var(--blur-val) saturate(120%);
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 7.5vw;
|
||||
height: 7.5vw;
|
||||
border-radius: 50%;
|
||||
border: solid 3px var(--infobox-border-color);
|
||||
transition: transform 0.6s ease, box-shadow 0.4s ease, filter 0.5s;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
filter: var(--img-brightness);
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) rotate(1turn) scale(1.1);
|
||||
box-shadow: 0 0 7px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.5vw;
|
||||
margin-top: 3vh;
|
||||
}
|
||||
.motto {
|
||||
font-size: 1vw;
|
||||
font-weight: bold;
|
||||
animation: color-change 0.8s linear infinite;
|
||||
margin-top: 3vh;
|
||||
text-align: center;
|
||||
.pointer {
|
||||
display: inline-block;
|
||||
margin: -0.5vh 0 0;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
width: 2px;
|
||||
height: 1vw;
|
||||
animation: color-change 0.8s linear infinite;
|
||||
background-color: var(--pointerColor);
|
||||
}
|
||||
@keyframes color-change {
|
||||
0%,
|
||||
40% {
|
||||
--pointerColor: var(--font-color-grey);
|
||||
}
|
||||
|
||||
60%,
|
||||
100% {
|
||||
--pointerColor: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 3.5vh;
|
||||
width: 12vw;
|
||||
padding: 0;
|
||||
|
||||
.social {
|
||||
font-size: 1.5vw;
|
||||
font-weight: 600;
|
||||
transition: all 0.5s;
|
||||
color: var(--font-color-grey);
|
||||
|
||||
&:hover {
|
||||
filter: drop-shadow(0 0 5px var(--font-color-grey));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.welcome-text {
|
||||
font-size: 5vh;
|
||||
margin-bottom: 10vh;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 5vh 6vw 2vh;
|
||||
width: 75vw;
|
||||
border-radius: 4vh;
|
||||
|
||||
.avatar {
|
||||
width: 10vh;
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 2.5vh;
|
||||
margin-top: 1.8vh;
|
||||
}
|
||||
.motto {
|
||||
font-size: 1.5vh;
|
||||
margin-top: 1.5vh;
|
||||
.pointer {
|
||||
height: 1.5vh;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 1.8vh;
|
||||
width: 32vw;
|
||||
|
||||
.social {
|
||||
font-size: 2vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import Layout from './Layout.vue'
|
||||
import type { Theme } from 'vitepress'
|
||||
import 'normalize.css'
|
||||
import '@fontsource/jetbrains-mono'
|
||||
import './assets/icon/iconfont.css'
|
||||
import './styles/index.less'
|
||||
import './components/Spine-Player/spine-player.css'
|
||||
|
||||
export default {
|
||||
Layout,
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// ...
|
||||
},
|
||||
} satisfies Theme
|
||||
44
.vitepress/theme/store/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { reactive } from 'vue'
|
||||
import { PostData } from '../utils/posts.data'
|
||||
|
||||
interface StoreState {
|
||||
selectedPosts: PostData[]
|
||||
currTag: string
|
||||
currPost: PostData
|
||||
currPage: number
|
||||
searchDialog: boolean
|
||||
splashLoading: boolean
|
||||
fireworksEnabled: boolean
|
||||
SpinePlayerEnabled: boolean
|
||||
showDropdownMenu: boolean
|
||||
darkMode: 'light' | 'dark' | 'system'
|
||||
}
|
||||
|
||||
const state: StoreState = reactive({
|
||||
selectedPosts: [],
|
||||
currTag: '',
|
||||
currPost: {
|
||||
id: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
href: '',
|
||||
create: 0,
|
||||
update: 0,
|
||||
tags: [],
|
||||
wordCount: 0,
|
||||
cover: '',
|
||||
excerpt: '',
|
||||
pinned: false
|
||||
},
|
||||
currPage: 1,
|
||||
searchDialog: false,
|
||||
splashLoading: true,
|
||||
fireworksEnabled: true,
|
||||
SpinePlayerEnabled: true,
|
||||
showDropdownMenu: false,
|
||||
darkMode: 'system',
|
||||
})
|
||||
|
||||
export function useStore() {
|
||||
return { state }
|
||||
}
|
||||
24
.vitepress/theme/styles/icons.less
Normal file
2
.vitepress/theme/styles/index.less
Normal file
@@ -0,0 +1,2 @@
|
||||
@import url(./vars.less);
|
||||
@import url(./icons.less);
|
||||
106
.vitepress/theme/styles/vars.less
Normal file
@@ -0,0 +1,106 @@
|
||||
:root {
|
||||
// 常量
|
||||
--transition-time: 0.3s;
|
||||
--transition-curve: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// 浅色主题默认值
|
||||
--btn-hover: #33495d;
|
||||
--btn-background: #425c8b;
|
||||
--color-blue: #128afa;
|
||||
--font-color-gold: #ffe401;
|
||||
--font-color-grey: #4c5866;
|
||||
--icon-color: #466398;
|
||||
--blur-val: blur(15px);
|
||||
--general-background-color: #eaeff5; //最底色
|
||||
--foreground-color: white; //通用背景色
|
||||
--blue-shadow-color: 40, 135, 200; //通用阴影
|
||||
--wave-color1: rgba(234, 239, 245, 0.8); // 波浪背景色
|
||||
--wave-color2: rgba(234, 239, 245, 0.5); // 波浪背景色
|
||||
--pot-border-left: #c7e4f6; // 帖子列表左侧边框颜色
|
||||
--dot: rgba(0, 0, 0, 0.2); //点阵颜色
|
||||
--dot-active: #128afa; //点阵激活颜色
|
||||
--infobox-background-initial: rgba(255, 255, 255, 0.1); //infobox初始背景色
|
||||
--infobox-background-final: rgba(255, 255, 255, 0.5); //infobox渐变背景色
|
||||
--triangle-background: repeating-linear-gradient(
|
||||
60deg,
|
||||
rgba(190, 242, 255, 0.3),
|
||||
transparent 35px
|
||||
),
|
||||
repeating-linear-gradient(180deg, transparent, rgba(108, 230, 255, 0.3) 30px),
|
||||
repeating-linear-gradient(120deg, rgba(16, 179, 215, 0.3), transparent 46px);
|
||||
--img-brightness: brightness(100%); // 亮度控制变量
|
||||
--welcome-text-color: var(--foreground-color);
|
||||
--welcome-text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
|
||||
--info-box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
--post-InnerBanner-color: white;
|
||||
--infobox-border-color: white;
|
||||
--downarrow-color: white;
|
||||
--content-opacity: 0.25;
|
||||
|
||||
// 搜索对话框变量
|
||||
--search-dialog-bg: rgb(239, 239, 239);
|
||||
--search-dialog-header-bg: rgb(239, 242, 244);
|
||||
--search-dialog-border: rgb(213, 217, 219);
|
||||
--search-input-bg: rgb(230, 234, 235);
|
||||
--search-input-border: rgb(209, 213, 218);
|
||||
--search-list-bg: rgb(174, 193, 202);
|
||||
--search-item-bg: #fff;
|
||||
--search-item-shadow: rgba(69, 73, 78, 0.548);
|
||||
|
||||
// gitalk
|
||||
--gitalk-shadow: 0 0 1em 0 #c6d8e7;
|
||||
--gitalk-background: #f4f8ff;
|
||||
--gitalk-border-left: 0.25em solid #dfe2e5;
|
||||
--gitalk-font-color-ol: rgb(116, 125, 135);
|
||||
}
|
||||
|
||||
html[theme='dark'] {
|
||||
--btn-hover: #797995;
|
||||
--btn-background: #5c5c76bf;
|
||||
--color-blue: #705781;
|
||||
--font-color-gold: #cfc6ff;
|
||||
--font-color-grey: #c8c8dc;
|
||||
--icon-color: #9d7cd8;
|
||||
--blur-val: blur(8px);
|
||||
--general-background-color: #0f0f16; //最底色
|
||||
--foreground-color: #1f1f2c; //通用背景色
|
||||
--blue-shadow-color: 147, 113, 207; //通用阴影
|
||||
--wave-color1: rgba(30, 30, 50, 0.604); // 波浪背景色
|
||||
--wave-color2: rgba(21, 21, 28, 0.384);
|
||||
--pot-border-left: rgba(135, 112, 210, 0.687); // 帖子列表左侧边框颜色
|
||||
--dot: rgba(150, 149, 149, 0.2); //点阵颜色
|
||||
--dot-active: #624398; //点阵激活颜色
|
||||
--infobox-background-initial: rgba(32, 27, 38, 0.6);
|
||||
--infobox-background-final: rgba(32, 29, 42, 0.9);
|
||||
--triangle-background: repeating-linear-gradient(
|
||||
60deg,
|
||||
rgba(158, 124, 216, 0.15),
|
||||
transparent 35px
|
||||
),
|
||||
repeating-linear-gradient(180deg, transparent, rgba(157, 124, 216, 0.08) 30px),
|
||||
repeating-linear-gradient(120deg, rgba(157, 124, 216, 0.08), transparent 46px);
|
||||
--img-brightness: brightness(80%); // 暗色模式下降低亮度
|
||||
--welcome-text-color: #dcdce2;
|
||||
--welcome-text-shadow: 0 0 5px rgba(79, 45, 138, 0.4);
|
||||
--info-box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
--post-InnerBanner-color: #d9ddecef;
|
||||
--infobox-border-color: #2c2c39b5;
|
||||
--downarrow-color: rgba(255, 255, 255, 0.597);
|
||||
--content-opacity: 0.1;
|
||||
|
||||
// 搜索对话框暗色变量
|
||||
--search-dialog-bg: #1f1f2c;
|
||||
--search-dialog-header-bg: #2a2a3a;
|
||||
--search-dialog-border: #383852;
|
||||
--search-input-bg: #2a2a3a;
|
||||
--search-input-border: #383852;
|
||||
--search-list-bg: #2a2a3a;
|
||||
--search-item-bg: #1f1f2c;
|
||||
--search-item-shadow: rgba(0, 0, 0, 0.5);
|
||||
|
||||
// gitalk
|
||||
--gitalk-shadow: 0 0 1em 0 #40335c;
|
||||
--gitalk-background: #1a1a24;
|
||||
--gitalk-border-left: 0.25em solid #514a6a;
|
||||
--gitalk-font-color-ol: #7e7e8d;
|
||||
}
|
||||
14
.vitepress/theme/utils/posts.data.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface PostData {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
href: string
|
||||
create: number
|
||||
update: number
|
||||
tags?: string[]
|
||||
wordCount: number
|
||||
cover?: string
|
||||
excerpt: string
|
||||
pinned?: boolean
|
||||
}
|
||||
export declare const data: PostData[]
|
||||
92
.vitepress/theme/utils/posts.data.mts
Normal file
@@ -0,0 +1,92 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import matter from 'gray-matter'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { createMarkdownRenderer } from 'vitepress'
|
||||
// ES模块需要重新声明常量
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const cwd = process.cwd()
|
||||
|
||||
let id = 1
|
||||
|
||||
interface Post {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
href: string
|
||||
create: number
|
||||
update: number
|
||||
tags?: string[]
|
||||
wordCount: number
|
||||
cover?: string
|
||||
excerpt: string | undefined
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
// Post数据缓存
|
||||
const cache: Map<string, { timestamp: number; post: Post }> = new Map()
|
||||
|
||||
function countWords(text: string): number {
|
||||
// 将连续的英文字母串替换为单个字母
|
||||
const replacedText = text.replace(/[a-zA-Z]+/g, 'A')
|
||||
|
||||
const pattern = /[\u4E00-\u9FA5A]/g
|
||||
const matches = replacedText.match(pattern)
|
||||
|
||||
let count = 0
|
||||
if (matches) {
|
||||
count = matches.length
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
function getPost(md: any, file: string, postDir: string): Post {
|
||||
const fullPath = path.join(postDir, file)
|
||||
const timestamp = Math.floor(fs.statSync(fullPath).mtimeMs)
|
||||
// 缓存直接返回
|
||||
const cached = cache.get(fullPath)
|
||||
if (cached && timestamp === cached.timestamp) {
|
||||
return cached.post
|
||||
}
|
||||
|
||||
const src = fs.readFileSync(fullPath, 'utf-8')
|
||||
const { data, excerpt, content } = matter(src, { excerpt: true })
|
||||
const post: Post = {
|
||||
id: id++,
|
||||
title: data.title,
|
||||
content: content,
|
||||
href: `posts/${file.replace(/\.md$/, '.html')}`,
|
||||
create: +(new Date(data.date) || timestamp),
|
||||
update: timestamp,
|
||||
tags: data.tags,
|
||||
wordCount: countWords(content),
|
||||
cover: data.cover,
|
||||
excerpt: excerpt,
|
||||
pinned: !!data.pinned
|
||||
}
|
||||
|
||||
cache.set(fullPath, { timestamp, post })
|
||||
return post
|
||||
}
|
||||
|
||||
// 加载posts文件夹
|
||||
async function load() {
|
||||
const md = await createMarkdownRenderer(cwd)
|
||||
const postDir = path.join(cwd, 'posts')
|
||||
return fs
|
||||
.readdirSync(postDir)
|
||||
.filter((file) => file.endsWith('.md') && file !== 'index.md')
|
||||
.map((file) => getPost(md, file, postDir))
|
||||
.sort((a, b) => {
|
||||
// 先按置顶排序,再按创建时间排序
|
||||
if (a.pinned && !b.pinned) return -1
|
||||
if (!a.pinned && b.pinned) return 1
|
||||
return b.create - a.create
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
watch: path.relative(__dirname, cwd + '/posts/*.md').replace(/\\/g, '/'),
|
||||
load,
|
||||
}
|
||||
10
ConvertWebp.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
#todo 打包自动转换所有图片为webp
|
||||
|
||||
shopt -s extglob nullglob
|
||||
|
||||
[[ ! -d ./webp ]] && mkdir ./webp
|
||||
|
||||
for f in *.@(bmp|jpg|jpeg|png); do
|
||||
ffmpeg -i "$f" -c:v libwebp -quality 80 -compression_level 6 "./webp/${f%.*}.webp"
|
||||
done
|
||||
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "vitepress-theme-bluearchive",
|
||||
"version": "0.0.1",
|
||||
"description": "vitepress-theme-bluearchive",
|
||||
"type": "module",
|
||||
"main": ".vitepress/theme/Layout.vue",
|
||||
"author": "Alittfre",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/animejs": "^3.1.12",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.13.1",
|
||||
"less": "^4.2.2",
|
||||
"markdown-it-mathjax3": "^4.3.2",
|
||||
"vitepress": "^1.6.3",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev",
|
||||
"build": "vitepress build",
|
||||
"preview": "vitepress preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"animejs": "^3.2.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"markdown-it-custom-attrs": "^1.0.2",
|
||||
"md5": "^2.3.0",
|
||||
"minisearch": "^7.1.1",
|
||||
"normalize.css": "^8.0.1"
|
||||
}
|
||||
}
|
||||
2206
pnpm-lock.yaml
generated
Normal file
103
posts/hello-world.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: HelloWorld
|
||||
date: 2024-04-16
|
||||
tags: [HelloWorld, vue, vitepress]
|
||||
pinned: true
|
||||
head:
|
||||
- - meta
|
||||
- name: description
|
||||
content: vitepress-theme-bluearchive HelloWorld
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: vitepress theme bluearchive HelloWorld
|
||||
---
|
||||
|
||||
只是一个 HelloWorld
|
||||
|
||||
---
|
||||
|
||||
# Markdown Extension Examples
|
||||
|
||||
This page demonstrates some of the built-in markdown extensions provided by VitePress.
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
|
||||
|
||||
**Input**
|
||||
|
||||
````md
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Output**
|
||||
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Containers
|
||||
|
||||
**Input**
|
||||
|
||||
```md
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
||||
31
posts/images.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Images
|
||||
date: 2013-12-26
|
||||
tags: [image]
|
||||
head:
|
||||
- - meta
|
||||
- name: description
|
||||
content: vitepress-theme-bluearchive Images
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: vitepress theme bluearchive Images
|
||||
---
|
||||
|
||||
This is a image test post.
|
||||
|
||||
---
|
||||
|
||||
::: tip
|
||||
img 标签的图片灯箱效果需要加入属性选择器:
|
||||
|
||||
```md
|
||||
<img src="/image/wallpaper-2572384.webp" data-fancybox="gallery"/>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
> ``  `<img src="/image/wallpaper-2572384.webp" data-fancybox="gallery"/>` <img src="/image/wallpaper-2572384.webp" data-fancybox="gallery"/>
|
||||
|
||||

|
||||
|
||||

|
||||
3
posts/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: 文章详情
|
||||
---
|
||||
394
posts/markdown.md
Normal file
@@ -0,0 +1,394 @@
|
||||
---
|
||||
title: Markdown Style test
|
||||
date: 2018-07-24
|
||||
tags: [Foo, Bar]
|
||||
head:
|
||||
- - meta
|
||||
- name: description
|
||||
content: vitepress-theme-bluearchive Markdown Style test
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: vitepress theme bluearchive Markdown Style test
|
||||
---
|
||||
|
||||
This post is originated from https://gist.github.com/apackeer/4159268 and is used for testing markdown style. This post contains nearly every markdown usage. Make sure all the markdown elements below show up correctly.
|
||||
|
||||
---
|
||||
|
||||
## Headers
|
||||
|
||||
```markdown
|
||||
# H1
|
||||
|
||||
## H2
|
||||
|
||||
### H3
|
||||
|
||||
#### H4
|
||||
|
||||
##### H5
|
||||
|
||||
###### H6
|
||||
|
||||
Alternatively, for H1 and H2, an underline-ish style:
|
||||
|
||||
# Alt-H1
|
||||
|
||||
## Alt-H2
|
||||
```
|
||||
|
||||
# H1
|
||||
|
||||
## H2
|
||||
|
||||
### H3
|
||||
|
||||
#### H4
|
||||
|
||||
##### H5
|
||||
|
||||
###### H6
|
||||
|
||||
Alternatively, for H1 and H2, an underline-ish style:
|
||||
|
||||
# Alt-H1
|
||||
|
||||
## Alt-H2
|
||||
|
||||
## Emphasis
|
||||
|
||||
```markdown
|
||||
Emphasis, aka italics, with _asterisks_ or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or **underscores**.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
```
|
||||
|
||||
Emphasis, aka italics, with _asterisks_ or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or **underscores**.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
## Lists
|
||||
|
||||
```markdown
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
|
||||
- Unordered sub-list.
|
||||
|
||||
1. Actual numbers don't matter, just that it's a number
|
||||
1. Ordered sub-list
|
||||
1. And another item.
|
||||
|
||||
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
To have a line break without a paragraph, you will need to use two trailing spaces.
|
||||
Note that this line is separate, but within the same paragraph.
|
||||
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
- Unordered list can use asterisks
|
||||
|
||||
* Or minuses
|
||||
|
||||
- Or pluses
|
||||
|
||||
* Paragraph In unordered list
|
||||
|
||||
For example like this.
|
||||
|
||||
Common Paragraph with some text.
|
||||
And more text.
|
||||
```
|
||||
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
|
||||
- Unordered sub-list.
|
||||
|
||||
1. Actual numbers don't matter, just that it's a number
|
||||
1. Ordered sub-list
|
||||
1. And another item.
|
||||
|
||||
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
To have a line break without a paragraph, you will need to use two trailing spaces.
|
||||
Note that this line is separate, but within the same paragraph.
|
||||
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
- Unordered list can use asterisks
|
||||
|
||||
* Or minuses
|
||||
|
||||
- Or pluses
|
||||
|
||||
* Paragraph In unordered list
|
||||
|
||||
For example like this.
|
||||
|
||||
Common Paragraph with some text.
|
||||
And more text.
|
||||
|
||||
## Inline HTML
|
||||
|
||||
```markdown
|
||||
<p>To reboot your computer, press <kbd>ctrl</kbd>+<kbd>alt</kbd>+<kbd>del</kbd>.</p>
|
||||
```
|
||||
|
||||
<p>To reboot your computer, press <kbd>ctrl</kbd>+<kbd>alt</kbd>+<kbd>del</kbd>.</p>
|
||||
|
||||
```markdown
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
|
||||
</dl>
|
||||
```
|
||||
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
</dl>
|
||||
|
||||
## Links
|
||||
|
||||
```markdown
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][Arbitrary case-insensitive reference text]
|
||||
|
||||
[I'm a relative reference to a repository file](../blob/master/LICENSE)
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself]
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://hexo.io
|
||||
[1]: https://hexo.io/docs/
|
||||
[link text itself]: https://hexo.io/api/
|
||||
```
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][Arbitrary case-insensitive reference text]
|
||||
|
||||
[I'm a relative reference to a repository file](../blob/master/LICENSE)
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself]
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://hexo.io
|
||||
[1]: https://hexo.io/docs/
|
||||
[link text itself]: https://hexo.io/api/
|
||||
|
||||
## Images
|
||||
|
||||
```markdown
|
||||
hover to see the title text:
|
||||
|
||||
Inline-style:
|
||||
|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: https://hexo.io/icon/favicon-196x196.png 'Logo Title Text 2'
|
||||
```
|
||||
|
||||
hover to see the title text:
|
||||
|
||||
Inline-style:
|
||||
|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: https://hexo.io/icon/favicon-196x196.png 'Logo Title Text 2'
|
||||
|
||||
## Code and Syntax Highlighting
|
||||
|
||||
Inline `code` has `back-ticks around` it.
|
||||
|
||||
```javascript
|
||||
var s = 'JavaScript syntax highlighting'
|
||||
alert(s)
|
||||
```
|
||||
|
||||
```python
|
||||
s = "Python syntax highlighting"
|
||||
print s
|
||||
```
|
||||
|
||||
```
|
||||
No language indicated, so no syntax highlighting.
|
||||
But let's throw in a <b>tag</b>.
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
```markdown
|
||||
| | ASCII | HTML |
|
||||
| ---------------- | ------------------------------- | ----------------------------- |
|
||||
| Single backticks | `'Isn't this fun?'` | 'Isn't this fun?' |
|
||||
| Quotes | `"Isn't this fun?"` | "Isn't this fun?" |
|
||||
| Dashes | `-- is en-dash, --- is em-dash` | -- is en-dash, --- is em-dash |
|
||||
```
|
||||
|
||||
| | ASCII | HTML |
|
||||
| ---------------- | ------------------------------- | ----------------------------- |
|
||||
| Single backticks | `'Isn't this fun?'` | 'Isn't this fun?' |
|
||||
| Quotes | `"Isn't this fun?"` | "Isn't this fun?" |
|
||||
| Dashes | `-- is en-dash, --- is em-dash` | -- is en-dash, --- is em-dash |
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
```markdown
|
||||
| Tables | Are | Cool |
|
||||
| ------------- | :-----------: | ---: |
|
||||
| col 3 is | right-aligned | |
|
||||
| col 2 is | centered | |
|
||||
| zebra stripes | are neat |
|
||||
```
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- | :-----------: | ---: |
|
||||
| col 3 is | right-aligned | |
|
||||
| col 2 is | centered | |
|
||||
| zebra stripes | are neat | |
|
||||
|
||||
The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
```markdown
|
||||
| Markdown | Less | Pretty |
|
||||
| -------- | --------- | ---------- |
|
||||
| _Still_ | `renders` | **nicely** |
|
||||
| 1 | 2 | 3 |
|
||||
```
|
||||
|
||||
| Markdown | Less | Pretty |
|
||||
| -------- | --------- | ---------- |
|
||||
| _Still_ | `renders` | **nicely** |
|
||||
| 1 | 2 | 3 |
|
||||
|
||||
> You can find more information about **LaTeX** mathematical expressions [here](https://math.meta.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference).
|
||||
|
||||
## Blockquotes
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can _put_ **Markdown** into a blockquote.
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
Three or more...
|
||||
|
||||
```markdown
|
||||
---
|
||||
Hyphens
|
||||
---
|
||||
|
||||
Asterisks
|
||||
|
||||
---
|
||||
|
||||
Underscores
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Hyphens
|
||||
|
||||
---
|
||||
|
||||
Asterisks
|
||||
|
||||
---
|
||||
|
||||
Underscores
|
||||
|
||||
## Line Breaks
|
||||
|
||||
```markdown
|
||||
Here's a line for us to start with.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a _separate paragraph_.
|
||||
|
||||
This line is also a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the _same paragraph_.
|
||||
```
|
||||
|
||||
Here's a line for us to start with.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a _separate paragraph_.
|
||||
|
||||
This line is also a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the _same paragraph_.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
This is a regular paragraph.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Foo</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
This is another regular paragraph.
|
||||
```
|
||||
|
||||
This is a regular paragraph.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Foo</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
This is another regular paragraph.
|
||||
|
||||
## Youtube videos
|
||||
|
||||
```markdown
|
||||
<a href="https://www.youtube.com/watch?feature=player_embedded&v=ARted4RniaU
|
||||
" target="_blank"><img src="https://img.youtube.com/vi/ARted4RniaU/0.jpg"
|
||||
alt="IMAGE ALT TEXT HERE" width="240" height="180" border="10" /></a>
|
||||
|
||||
Pure markdown version:
|
||||
|
||||
[](https://www.youtube.com/watch?v=ARted4RniaU)
|
||||
```
|
||||
|
||||
<a href="https://www.youtube.com/watch?feature=player_embedded&v=ARted4RniaU
|
||||
" target="_blank"><img src="https://img.youtube.com/vi/ARted4RniaU/0.jpg"
|
||||
alt="IMAGE ALT TEXT HERE" width="240" height="180" border="10" /></a>
|
||||
|
||||
Pure markdown version:
|
||||
|
||||
[](https://www.youtube.com/watch?v=ARted4RniaU)
|
||||
182
posts/studycase1.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: StudyCase1
|
||||
date: 2025-11-25 15:35
|
||||
tags: [studycase, unity]
|
||||
pinned: true
|
||||
head:
|
||||
- - meta
|
||||
- name: description
|
||||
content: vitepress-theme-bluearchive StudyCase1
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: vitepress theme bluearchive StudyCase1
|
||||
---
|
||||
|
||||
# 第三人称角色控制器
|
||||
|
||||
## 编辑器
|
||||
- **Unity 2022.3.62f2**:
|
||||
- **Visual Studio 2022**
|
||||
|
||||
## 课程目标
|
||||
- **鼠标控制视角旋转**
|
||||
- **WASD控制模型移动**
|
||||
- **空格控制跳跃**
|
||||
|
||||
## 演示视频
|
||||
<video id="vdMain" controls muted poster="/video/studycase1/演示视频.png" playsinline>
|
||||
<source id="vSource" src="/video/studycase1/演示视频.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
[项目地址](http://home.gtuantuan.online:8300/TuanTuan/StudyCase)
|
||||
|
||||
## 课前准备
|
||||
### 新建项目
|
||||
- 创建URP项目
|
||||
|
||||
<img src="/image/studycase1/创建URP项目.png" data-fancybox="gallery"/>
|
||||
|
||||
- 移除Readme
|
||||
|
||||
<img src="/image/studycase1/移除Readme.png" data-fancybox="gallery"/>
|
||||
|
||||
- 新建场景或者修改初始场景为StudyCase1
|
||||
|
||||
<img src="/image/studycase1/创建StudyCase1场景.png" data-fancybox="gallery"/>
|
||||
|
||||
### 安装 Input System 与 Cinemachine
|
||||
- 打开 Package Manager
|
||||
|
||||
<img src="/image/studycase1/打开PackageManager.png" data-fancybox="gallery"/>
|
||||
|
||||
- 切换到 Unity Registry
|
||||
|
||||
<img src="/image/studycase1/切换到UnityRegistry.png" data-fancybox="gallery"/>
|
||||
|
||||
- 安装 `Input System` 与 `Cinemachine`
|
||||
|
||||
<img src="/image/studycase1/安装俩个包.png" data-fancybox="gallery"/>
|
||||
|
||||
- 导入示例
|
||||
|
||||
<img src="/image/studycase1/导入示例.png" data-fancybox="gallery"/>
|
||||
|
||||
- 创建初始场景(StudyCase1)
|
||||
|
||||
<img src="/image/studycase1/创建初始场景.png" data-fancybox="gallery"/>
|
||||
|
||||
- 从示例项目导入模型到场景
|
||||
|
||||
<img src="/image/studycase1/从示例项目导入模型到场景.png" data-fancybox="gallery"/>
|
||||
|
||||
> 提示:安装 Input System 后,Unity 会提示重启并切换至新输入系统,务必确认。
|
||||
|
||||
## 课堂任务
|
||||
### 添加虚拟相机根节点
|
||||
- 为相机添加 `CinemachineBrain`作为根节点以便统一控制
|
||||
|
||||
<img src="/image/studycase1/添加虚拟相机根节点.png" data-fancybox="gallery"/>
|
||||
|
||||
### 创建玩家节点
|
||||
- 创建 `Player` 空节点,创建`Forward`和`Model`空节点并作为`Player`子节点以及虚拟相机
|
||||
- 注意`Forward`和`Model`的尽量不要有偏移
|
||||
|
||||
<img src="/image/studycase1/创建玩家层级.png" data-fancybox="gallery"/>
|
||||
|
||||
- 创建胶囊体和正方体作为`Model`的子物体,可以去掉碰撞器,尽量不要有偏移
|
||||
|
||||
<img src="/image/studycase1/玩家节点.png" data-fancybox="gallery"/>
|
||||
|
||||
### 配置虚拟相机
|
||||
- 将虚拟相机 `Follow`、`LookAt` 指向玩家模型,调整 Body/ Aim,使镜头保持第三人称视角
|
||||
|
||||
<img src="/image/studycase1/配置虚拟相机.png" data-fancybox="gallery"/>
|
||||
|
||||
### 创建 InputActions
|
||||
- 在项目窗口右键 `Create > Input Actions`,命名为 `PlayerInputActions`
|
||||
|
||||
<img src="/image/studycase1/创建InputActions.png" data-fancybox="gallery"/>
|
||||
|
||||
- 创建 `Player` Action Map,添加 `Move`(Vector2)与 `Jump`(Button)
|
||||
|
||||
<img src="/image/studycase1/创建playerinputactions.png" data-fancybox="gallery"/>
|
||||
|
||||
- Move 绑定 `WASD` ;Jump 绑定 `space`
|
||||
|
||||
<img src="/image/studycase1/创建4向绑定.png" data-fancybox="gallery"/>
|
||||
<img src="/image/studycase1/绑定wasd.png" data-fancybox="gallery"/>
|
||||
|
||||
### 编写脚本`ThirdCharacterController.cs`
|
||||
- 在 `Assets\Scripts\StudyCase1` 下创建脚本 `ThirdCharacterController`
|
||||
```csharp
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace StudyCase1
|
||||
{
|
||||
public class ThirdCharacterController : MonoBehaviour
|
||||
{
|
||||
public CharacterController characterController;
|
||||
public Transform forward;
|
||||
public Transform model;
|
||||
public Cinemachine.CinemachineVirtualCamera vCam;
|
||||
public float moveSpeed = 5f;
|
||||
public float jumpSpeed = 2f;
|
||||
public float turnSpeed = 10f;
|
||||
public float gravity = 10f;
|
||||
Vector3 moveDir;
|
||||
Vector2 moveInput;
|
||||
private void Update()
|
||||
{
|
||||
moveDir = new Vector3(moveInput.x, moveDir.y, moveInput.y);
|
||||
forward.eulerAngles = new Vector3(0, vCam.transform.eulerAngles.y, 0);
|
||||
moveDir = forward.TransformDirection(moveDir);
|
||||
if (moveInput != Vector2.zero)
|
||||
{
|
||||
Quaternion target = Quaternion.LookRotation(new Vector3(moveDir.x, 0, moveDir.z));
|
||||
model.rotation = Quaternion.Slerp(model.rotation, target, turnSpeed * Time.deltaTime);
|
||||
}
|
||||
if (!characterController.isGrounded)
|
||||
moveDir.y -= gravity * Time.deltaTime;
|
||||
characterController.Move(moveDir * moveSpeed * Time.deltaTime);
|
||||
}
|
||||
public void OnMove(InputAction.CallbackContext context)
|
||||
{
|
||||
if (characterController.isGrounded)
|
||||
moveInput = context.ReadValue<Vector2>();
|
||||
else moveInput = Vector2.zero;
|
||||
}
|
||||
public void OnJump(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed && characterController.isGrounded)
|
||||
{
|
||||
moveDir.y = jumpSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码讲解
|
||||
- **namespace StudyCase1**:因为可能出现同名文件
|
||||
- **forward**:作为“参照物”同步虚拟相机的 Y 轴旋转,用来转换输入的坐标轴。
|
||||
- **moveDir**:在本地坐标系下计算移动向量,并通过 `CharacterController.Move` 驱动。
|
||||
- **OnMove/OnJump**:直接使用 Input System 回调,确保只有在贴地时才写入输入,避免空中漂移。
|
||||
- **旋转插值**:`Quaternion.Slerp` 让角色转身更加顺滑,可根据手感微调 `turnSpeed`。
|
||||
|
||||
### Player节点设置
|
||||
- 挂载`ThirdCharacterController`
|
||||
|
||||
<img src="/image/studycase1/挂载ThirdCharacterController.png" data-fancybox="gallery"/>
|
||||
|
||||
- 添加组件`PlayerInput`
|
||||
|
||||
<img src="/image/studycase1/添加组件PlayerInput.png" data-fancybox="gallery"/>
|
||||
|
||||
## 常见问题速查
|
||||
- **角色不动**:确认 `CharacterController` 已赋值且 `Move` Action Map 激活。
|
||||
- **镜头不同步**:检查 `forward` 对象是否正确引用虚拟相机的朝向,检查模型和`player`是否偏移过大。
|
||||
- **跳跃失效**:确认 `Jump` 绑定的动作类型为 `Button`,确认角色是否在地上。
|
||||
- **移动时会旋转镜头**:确认虚拟相机是在 `Player` 节点下,不是在 `Model` 节点下。
|
||||
|
||||
完成以上步骤,即可得到一个可拓展的第三人称基础模板。
|
||||
171
posts/studycase2.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: StudyCase2
|
||||
date: 2025-11-25 15:38
|
||||
tags: [studycase, unity]
|
||||
pinned: true
|
||||
head:
|
||||
- - meta
|
||||
- name: description
|
||||
content: vitepress-theme-bluearchive StudyCase2
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: vitepress theme bluearchive StudyCase2
|
||||
---
|
||||
|
||||
# 绑定动画
|
||||
|
||||
## 编辑器
|
||||
- **Unity 2022.3.62f2**:
|
||||
- **Visual Studio 2022**
|
||||
|
||||
## 课程目标
|
||||
- **控制器绑定动画**
|
||||
|
||||
## 演示视频
|
||||
<video id="vdMain" controls muted poster="/video/studycase2/演示视频.png" playsinline>
|
||||
<source id="vSource" src="/video/studycase2/演示视频.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
[项目地址](http://home.gtuantuan.online:8300/TuanTuan/StudyCase)
|
||||
|
||||
## 课前准备
|
||||
### 1. 导入FBX模型,导入shader
|
||||
- 新健文件夹导入FBX
|
||||
|
||||
<img src="/image/studycase2/新健文件夹导入FBX.png" data-fancybox="gallery"/>
|
||||
|
||||
- 导入shader
|
||||
|
||||
<img src="/image/studycase2/导入shader.png" data-fancybox="gallery"/>
|
||||
|
||||
## 课堂任务
|
||||
|
||||
### 模型处理
|
||||
- 导出FBX材质球到Mats文件夹方便修改
|
||||
|
||||
<img src="/image/studycase2/导出材质.png" data-fancybox="gallery"/>
|
||||
|
||||
- 替换模型材质Shader
|
||||
|
||||
<img src="/image/studycase2/嘴部材质特殊处理.png" data-fancybox="gallery"/>
|
||||
|
||||
- 嘴巴贴图特殊处理
|
||||
|
||||
<img src="/image/studycase2/嘴巴贴图特殊处理.png" data-fancybox="gallery"/>
|
||||
|
||||
- 属于脸部材质需勾选isFace,嘴部材质特殊处理
|
||||
|
||||
<img src="/image/studycase2/嘴部材质特殊处理.png" data-fancybox="gallery"/>
|
||||
|
||||
### 用模型替换之前的胶囊体
|
||||
- 替换模型
|
||||
|
||||
<img src="/image/studycase2/替换模型.png" data-fancybox="gallery"/>
|
||||
|
||||
- 调整模型大小和位置
|
||||
|
||||
<img src="/image/studycase2/调整模型大小和位置.png" data-fancybox="gallery"/>
|
||||
|
||||
### 创建动画控制器,添加Idle和Move动画片段
|
||||
- 创建动画控制器
|
||||
|
||||
<img src="/image/studycase2/创建动画控制器.png" data-fancybox="gallery"/>
|
||||
|
||||
- 模型节点添加动画控制器组件
|
||||
|
||||
<img src="/image/studycase2/模型节点添加动画控制器组件.png" data-fancybox="gallery"/>
|
||||
|
||||
- 打开动画控制器界面,Idle作为默认动画
|
||||
|
||||
<img src="/image/studycase2/Idle作为默认动画.png" data-fancybox="gallery"/>
|
||||
|
||||
- 添加移动动画,创建连接,创建bool参数
|
||||
|
||||
<img src="/image/studycase2/添加移动动画,创建连接,创建bool参数.png" data-fancybox="gallery"/>
|
||||
|
||||
- moving和idle设置为循环播放
|
||||
|
||||
<img src="/image/studycase2/moving和idle设置为循环播放.png" data-fancybox="gallery"/>
|
||||
|
||||
### 创建状态条件
|
||||
- 设置idle到moving的条件
|
||||
|
||||
<img src="/image/studycase2/设置idle到moving的条件.png" data-fancybox="gallery"/>
|
||||
|
||||
- 设置moving到move end的条件
|
||||
|
||||
<img src="/image/studycase2/设置moving到move end的条件.png" data-fancybox="gallery"/>
|
||||
|
||||
- 设置move end到moving的条件的条件
|
||||
|
||||
<img src="/image/studycase2/设置move end到moving的条件.png" data-fancybox="gallery"/>
|
||||
|
||||
### 编写脚本`ThirdCharacterController.cs`
|
||||
- 在 `Assets\Scripts\StudyCase2` 下创建脚本 `ThirdCharacterController`
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace StudyCase2
|
||||
{
|
||||
public class ThirdCharacterController : MonoBehaviour
|
||||
{
|
||||
public CharacterController characterController;
|
||||
public Animator animator;
|
||||
public Transform forward;
|
||||
public Transform model;
|
||||
public Cinemachine.CinemachineVirtualCamera vCam;
|
||||
public float moveSpeed = 5f;
|
||||
public float jumpSpeed = 2f;
|
||||
public float turnSpeed = 10f;
|
||||
public float gravity = 10f;
|
||||
Vector3 moveDir;
|
||||
Vector2 moveInput;
|
||||
private void Update()
|
||||
{
|
||||
moveDir = new Vector3(moveInput.x, moveDir.y, moveInput.y);
|
||||
forward.eulerAngles = new Vector3(0, vCam.transform.eulerAngles.y, 0);
|
||||
moveDir = forward.TransformDirection(moveDir);
|
||||
if (moveInput != Vector2.zero)
|
||||
{
|
||||
animator?.SetBool("Move", true);
|
||||
Quaternion target = Quaternion.LookRotation(new Vector3(moveDir.x, 0, moveDir.z));
|
||||
model.rotation = Quaternion.Slerp(model.rotation, target, turnSpeed * Time.deltaTime);
|
||||
}
|
||||
else animator?.SetBool("Move", false);
|
||||
if (!characterController.isGrounded)
|
||||
moveDir.y -= gravity * Time.deltaTime;
|
||||
characterController.Move(moveDir * moveSpeed * Time.deltaTime);
|
||||
}
|
||||
public void OnMove(InputAction.CallbackContext context)
|
||||
{
|
||||
if (characterController.isGrounded)
|
||||
moveInput = context.ReadValue<Vector2>();
|
||||
else moveInput = Vector2.zero;
|
||||
}
|
||||
public void OnJump(InputAction.CallbackContext context)
|
||||
{
|
||||
if (context.performed && characterController.isGrounded)
|
||||
{
|
||||
moveDir.y = jumpSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### 代码讲解
|
||||
```csharp
|
||||
//获取动画控制器
|
||||
public Animator animator;
|
||||
```
|
||||
```csharp
|
||||
//设置动画控制器参数
|
||||
//true:播放Moveing动画
|
||||
//false:播放Move end动画,然后回到Idle
|
||||
animator.SetBool("Move", true);
|
||||
```
|
||||
### Player节点设置
|
||||
- 新增 `Animator` ,其他同StudyCase1
|
||||
|
||||
完成以上步骤,即可得到一个带动画的第三人称控制器。
|
||||
16
posts/tags.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Tags
|
||||
date: 2013-12-24 23:29:53
|
||||
tags: [Foo, Bar, Baz]
|
||||
head:
|
||||
- - meta
|
||||
- name: description
|
||||
content: vitepress-theme-bluearchive Tags
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: vitepress theme bluearchive Tags
|
||||
---
|
||||
|
||||
This post contains 3 tags. Make sure your theme can display all of the tags.
|
||||
|
||||
---
|
||||
BIN
preview.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
preview_dark.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |