Skip to content

Commit a902fa5

Browse files
author
yqz
committed
feat:加上看板娘效果
1 parent fc54f56 commit a902fa5

5 files changed

Lines changed: 390 additions & 31 deletions

File tree

vue/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vue/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@element-plus/icons-vue": "^2.3.2",
1313
"dompurify": "^3.3.3",
1414
"marked": "^17.0.5",
15+
"oh-my-live2d": "^0.19.3",
1516
"vue": "^3.5.30",
1617
"vue-router": "^4.6.4"
1718
},

vue/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import WelcomeGate from './components/WelcomeGate.vue'
66
import BackToTop from './components/BackToTop.vue'
77
import WidgetsDrawer from './components/WidgetsDrawer.vue'
88
import RightDock from './components/RightDock.vue'
9+
import Live2dCharacter from './components/Live2dCharacter.vue'
910
import { postSiteVisit } from './api/site'
1011
import { fetchPublicProfile } from './api/profile'
1112
@@ -39,6 +40,7 @@ onMounted(async () => {
3940
<RightDock v-if="!isConsole" @toggle-profile="toggleWidgets" />
4041
<WidgetsDrawer v-if="!isConsole" :open="widgetsOpen" :profile="profile" @close="widgetsOpen = false" />
4142
<BackToTop v-if="!isConsole" />
43+
<Live2dCharacter v-if="!isConsole" />
4244
<router-view />
4345
</div>
4446
</template>
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<template>
2+
<div class="live2d-wrapper" :class="{ 'live2d-wrapper--hidden': dismissed }">
3+
<div ref="containerRef" class="live2d-container" />
4+
<button
5+
type="button"
6+
class="live2d-toggle"
7+
:aria-label="dismissed ? '召唤看板娘' : '隐藏看板娘'"
8+
:title="dismissed ? '召唤看板娘' : '隐藏看板娘'"
9+
@click="toggle"
10+
>
11+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
12+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
13+
<circle cx="8.5" cy="10" r="1.5" />
14+
<circle cx="15.5" cy="10" r="1.5" />
15+
<path d="M8 14c0 0 1.5 2 4 2s4-2 4-2" />
16+
</svg>
17+
</button>
18+
</div>
19+
</template>
20+
21+
<script setup>
22+
import { ref, onMounted } from 'vue'
23+
24+
const STORAGE_KEY = 'openblog-live2d-hidden'
25+
26+
function readHidden() {
27+
try {
28+
return sessionStorage.getItem(STORAGE_KEY) === '1'
29+
} catch {
30+
return false
31+
}
32+
}
33+
34+
function isDark() {
35+
if (typeof document === 'undefined') return false
36+
return document.documentElement.getAttribute('data-theme') === 'dark'
37+
}
38+
39+
const dismissed = ref(readHidden())
40+
const containerRef = ref(null)
41+
let oml2dInstance = null
42+
43+
function toggle() {
44+
dismissed.value = !dismissed.value
45+
try {
46+
sessionStorage.setItem(STORAGE_KEY, dismissed.value ? '1' : '0')
47+
} catch {
48+
/* ignore */
49+
}
50+
51+
if (!oml2dInstance) return
52+
if (dismissed.value) {
53+
oml2dInstance.stageSlideOut()
54+
} else {
55+
oml2dInstance.stageSlideIn()
56+
}
57+
}
58+
59+
function makeTipsStyle(dark) {
60+
return {
61+
width: 220,
62+
height: 70,
63+
fontSize: '13px',
64+
lineHeight: '1.5',
65+
borderRadius: '12px',
66+
background: dark ? 'rgba(34,34,36,0.88)' : 'rgba(255,255,255,0.85)',
67+
border: dark ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.08)',
68+
color: dark ? '#e8e8ea' : '#333',
69+
backdropFilter: 'saturate(1.1) blur(14px)',
70+
WebkitBackdropFilter: 'saturate(1.1) blur(14px)',
71+
boxShadow: dark
72+
? '0 4px 20px rgba(0,0,0,0.35)'
73+
: '0 4px 20px rgba(0,0,0,0.1)',
74+
}
75+
}
76+
77+
onMounted(async () => {
78+
if (!containerRef.value) return
79+
80+
const { loadOml2d } = await import('oh-my-live2d')
81+
82+
oml2dInstance = loadOml2d({
83+
dockedPosition: 'right',
84+
mobileDisplay: true,
85+
primaryColor: '#aa3bff',
86+
sayHello: false,
87+
transitionTime: 800,
88+
models: [
89+
{
90+
name: 'Pio',
91+
path: 'https://model.oml2d.com/Pio/model.json',
92+
position: [0, 10],
93+
scale: 0.1,
94+
stageStyle: {
95+
height: 340,
96+
},
97+
mobileScale: 0.08,
98+
mobilePosition: [0, 5],
99+
},
100+
{
101+
name: 'HK416',
102+
path: 'https://model.oml2d.com/HK416-1-normal/model.json',
103+
position: [0, 50],
104+
scale: 0.08,
105+
stageStyle: {
106+
height: 380,
107+
},
108+
mobileScale: 0.06,
109+
mobilePosition: [0, 30],
110+
},
111+
],
112+
tips: {
113+
messageLine: 3,
114+
style: makeTipsStyle(isDark()),
115+
mobileStyle: {
116+
width: 180,
117+
height: 56,
118+
fontSize: '12px',
119+
},
120+
welcomeTips: {
121+
duration: 5000,
122+
message: {
123+
daybreak: '早上好!又是元气满满的一天~',
124+
morning: '上午好!一起学习新知识吧~',
125+
noon: '中午好!记得按时吃饭哦~',
126+
afternoon: '下午好!来杯奶茶提提神~',
127+
dusk: '傍晚好!今天辛苦了~',
128+
night: '晚上好!欢迎来到我的博客~',
129+
lateNight: '夜深了,还在充电呢?注意休息哦~',
130+
weeHours: '已经凌晨了!答应我别熬夜好吗~',
131+
},
132+
},
133+
idleTips: {
134+
wordTheDay: true,
135+
duration: 6000,
136+
interval: 18000,
137+
},
138+
},
139+
statusBar: {
140+
enable: false,
141+
},
142+
menus: {
143+
disable: true,
144+
},
145+
parentElement: containerRef.value,
146+
})
147+
148+
if (dismissed.value) {
149+
const onLoad = () => {
150+
oml2dInstance?.stageSlideOut()
151+
oml2dInstance?.onStageSlideOut(() => {
152+
oml2dInstance?.onLoad(() => {})
153+
})
154+
}
155+
oml2dInstance.onLoad(onLoad)
156+
}
157+
})
158+
</script>
159+
160+
<style scoped>
161+
.live2d-wrapper {
162+
position: fixed;
163+
right: 0;
164+
bottom: 60px;
165+
z-index: 9995;
166+
pointer-events: none;
167+
}
168+
169+
.live2d-container {
170+
pointer-events: auto;
171+
transition: opacity 0.3s ease;
172+
}
173+
174+
.live2d-wrapper--hidden .live2d-container {
175+
opacity: 0;
176+
pointer-events: none;
177+
}
178+
179+
.live2d-toggle {
180+
position: fixed;
181+
right: max(18px, env(safe-area-inset-right, 0px));
182+
bottom: max(80px, calc(env(safe-area-inset-bottom, 0px) + 80px));
183+
z-index: 9999;
184+
width: 36px;
185+
height: 36px;
186+
border-radius: 50%;
187+
border: 1px solid var(--border);
188+
background: var(--card);
189+
color: var(--muted);
190+
cursor: pointer;
191+
display: flex;
192+
align-items: center;
193+
justify-content: center;
194+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
195+
backdrop-filter: saturate(1.05) blur(14px);
196+
-webkit-backdrop-filter: saturate(1.05) blur(14px);
197+
transition:
198+
color 0.15s ease,
199+
border-color 0.15s ease,
200+
transform 0.15s ease;
201+
pointer-events: auto;
202+
}
203+
204+
.live2d-toggle:hover {
205+
color: var(--accent);
206+
border-color: rgba(170, 59, 255, 0.45);
207+
transform: scale(1.1);
208+
}
209+
210+
.live2d-wrapper--hidden .live2d-toggle {
211+
animation: live2d-toggle-pulse 2s ease-in-out infinite;
212+
}
213+
214+
@keyframes live2d-toggle-pulse {
215+
0%,
216+
100% {
217+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
218+
}
219+
50% {
220+
box-shadow: 0 2px 18px rgba(170, 59, 255, 0.3);
221+
}
222+
}
223+
224+
@media (max-width: 680px) {
225+
.live2d-wrapper {
226+
bottom: 100px;
227+
right: -10px;
228+
}
229+
230+
.live2d-toggle {
231+
right: max(14px, env(safe-area-inset-right, 0px));
232+
bottom: max(120px, calc(env(safe-area-inset-bottom, 0px) + 120px));
233+
width: 32px;
234+
height: 32px;
235+
}
236+
}
237+
</style>

0 commit comments

Comments
 (0)