Init
This commit is contained in:
251
.vitepress/theme/components/Banner.vue
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
13442
.vitepress/theme/components/Spine-Player/spine-player.js
Normal file
File diff suppressed because one or more lines are too long
142
.vitepress/theme/components/Splash.vue
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
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
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
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>
|
||||
Reference in New Issue
Block a user