Skip to content

Commit b8b7d4e

Browse files
authored
Merge pull request #5 from yyyCode/web-refactor
Web refactor
2 parents 40a4a65 + 2ab0582 commit b8b7d4e

7 files changed

Lines changed: 420 additions & 31 deletions

File tree

vue/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
-->
2222
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="any" />
2323
<link rel="apple-touch-icon" href="/favicon.ico" />
24+
<link rel="preconnect" href="https://fastly.jsdelivr.net" crossorigin />
25+
<link rel="dns-prefetch" href="https://fastly.jsdelivr.net" />
26+
<link rel="preconnect" href="https://v1.hitokoto.cn" crossorigin />
27+
<link rel="dns-prefetch" href="https://v1.hitokoto.cn" />
2428
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
2529
<title>OpenBlog</title>
2630
</head>

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

0 commit comments

Comments
 (0)