mirror of
https://github.com/imsyy/home.git
synced 2025-05-20 21:20:14 +09:00
beta
This commit is contained in:
parent
081834c3b1
commit
dd81d31748
10
.env.example
10
.env.example
@ -36,6 +36,7 @@ VITE_SITE_START = "2020-10-24"
|
||||
# ICP 备案号
|
||||
## 若不需要,请设为空即可
|
||||
VITE_SITE_ICP = "豫ICP备2022018134号-1"
|
||||
VITE_SITE_MPS = ""
|
||||
|
||||
# 歌曲 API 地址
|
||||
## 请参照 https://github.com/xizeyoupan/Meting-API#deno-deploy 进行 API 服务部署
|
||||
@ -48,4 +49,11 @@ VITE_SONG_SERVER = "netease"
|
||||
# 播放类型 ( song-歌曲, playlist-播放列表, album-专辑, search-搜索, artist-艺术家 )
|
||||
VITE_SONG_TYPE = "playlist"
|
||||
# 播放 ID ( 若无需播放器,请设为空即可 )
|
||||
VITE_SONG_ID = "9379831714"
|
||||
VITE_SONG_ID = "9379831714"
|
||||
|
||||
# 文字转语音 API 地址(请自行搭建)
|
||||
## 如果也使用 Azure ,您可直接使用 https://github.com/NanoRocky/AzureSpeechAPI-by-PHP 完成 API 部署
|
||||
## (更多参数设置可以修改 \src\utils\speech.js 或自行补充)
|
||||
VITE_TTS_API = "https://your.domain/speech/"
|
||||
VITE_TTS_Voice = "zh-CN-YunxiaNeural"
|
||||
VITE_TTS_Style = "cheerful"
|
42
package.json
42
package.json
@ -14,32 +14,38 @@
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@worstone/vue-aplayer": "^1.0.6",
|
||||
"@worstone/vue-aplayer": "^1.0.7",
|
||||
"aplayer": "^1.10.1",
|
||||
"axios": "^1.6.8",
|
||||
"dayjs": "^1.11.10",
|
||||
"element-plus": "^2.7.1",
|
||||
"axios": "^1.7.7",
|
||||
"dayjs": "^1.11.13",
|
||||
"element-plus": "^2.8.7",
|
||||
"fetch-jsonp": "^1.3.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"swiper": "^11.1.1",
|
||||
"vue": "^3.4.24"
|
||||
"pinia": "^2.2.5",
|
||||
"pinia-plugin-persistedstate": "^4.1.2",
|
||||
"pinia-plugin-persistedstate-2": "^2.0.24",
|
||||
"swiper": "^11.1.14",
|
||||
"three": "^0.170.0",
|
||||
"vue": "^3.5.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@icon-park/vue-next": "^1.4.2",
|
||||
"@vicons/fa": "^0.12.0",
|
||||
"@vicons/ionicons4": "^0.12.0",
|
||||
"@vicons/ionicons5": "^0.12.0",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vicons/tabler": "^0.12.0",
|
||||
"@vicons/utils": "^0.1.4",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.75.0",
|
||||
"terser": "^5.30.4",
|
||||
"unplugin-auto-import": "^0.11.5",
|
||||
"unplugin-vue-components": "^0.22.12",
|
||||
"vite": "^4.5.3",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-vue": "^9.30.0",
|
||||
"prettier": "^3.3.3",
|
||||
"sass": "^1.80.6",
|
||||
"terser": "^5.36.0",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-pwa": "^0.14.7"
|
||||
"vite-plugin-pwa": "^0.20.5"
|
||||
}
|
||||
}
|
||||
|
4392
pnpm-lock.yaml
generated
4392
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/font/MiSans-Regular.otf
Normal file
BIN
public/font/MiSans-Regular.otf
Normal file
Binary file not shown.
BIN
public/speechlocal/一言加载失败.mp3
Normal file
BIN
public/speechlocal/一言加载失败.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/位置信息获取失败.mp3
Normal file
BIN
public/speechlocal/位置信息获取失败.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/分辨率不足.mp3
Normal file
BIN
public/speechlocal/分辨率不足.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/壁纸加载失败.mp3
Normal file
BIN
public/speechlocal/壁纸加载失败.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/壁纸预览.mp3
Normal file
BIN
public/speechlocal/壁纸预览.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/天气信息无法计算.mp3
Normal file
BIN
public/speechlocal/天气信息无法计算.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/天气加载失败.mp3
Normal file
BIN
public/speechlocal/天气加载失败.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/惊讶.mp3
Normal file
BIN
public/speechlocal/惊讶.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/播放器加载失败.mp3
Normal file
BIN
public/speechlocal/播放器加载失败.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/播放器未知异常.mp3
Normal file
BIN
public/speechlocal/播放器未知异常.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/更换壁纸成功.mp3
Normal file
BIN
public/speechlocal/更换壁纸成功.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/更新提示.mp3
Normal file
BIN
public/speechlocal/更新提示.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎1.mp3
Normal file
BIN
public/speechlocal/欢迎1.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎10.mp3
Normal file
BIN
public/speechlocal/欢迎10.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎2.mp3
Normal file
BIN
public/speechlocal/欢迎2.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎3.mp3
Normal file
BIN
public/speechlocal/欢迎3.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎4.mp3
Normal file
BIN
public/speechlocal/欢迎4.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎5.mp3
Normal file
BIN
public/speechlocal/欢迎5.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎6.mp3
Normal file
BIN
public/speechlocal/欢迎6.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎7.mp3
Normal file
BIN
public/speechlocal/欢迎7.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎8.mp3
Normal file
BIN
public/speechlocal/欢迎8.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/欢迎9.mp3
Normal file
BIN
public/speechlocal/欢迎9.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/歌曲加载失败.mp3
Normal file
BIN
public/speechlocal/歌曲加载失败.mp3
Normal file
Binary file not shown.
BIN
public/speechlocal/鼠标右键.mp3
Normal file
BIN
public/speechlocal/鼠标右键.mp3
Normal file
Binary file not shown.
62
src/App.vue
62
src/App.vue
@ -17,12 +17,8 @@
|
||||
</section>
|
||||
</div>
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<Icon
|
||||
class="menu"
|
||||
size="24"
|
||||
v-show="!store.backgroundShow"
|
||||
@click="store.mobileOpenState = !store.mobileOpenState"
|
||||
>
|
||||
<Icon class="menu" size="24" v-show="!store.backgroundShow"
|
||||
@click="store.mobileOpenState = !store.mobileOpenState">
|
||||
<component :is="store.mobileOpenState ? CloseSmall : HamburgerButton" />
|
||||
</Icon>
|
||||
<!-- 页脚 -->
|
||||
@ -47,6 +43,7 @@ import Box from "@/views/Box/index.vue";
|
||||
import MoreSet from "@/views/MoreSet/index.vue";
|
||||
import cursorInit from "@/utils/cursor.js";
|
||||
import config from "@/../package.json";
|
||||
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
||||
|
||||
const store = mainStore();
|
||||
|
||||
@ -59,7 +56,7 @@ const getWidth = () => {
|
||||
const loadComplete = () => {
|
||||
nextTick(() => {
|
||||
// 欢迎提示
|
||||
helloInit();
|
||||
helloInit(store);
|
||||
// 默哀模式
|
||||
checkDays();
|
||||
});
|
||||
@ -87,6 +84,12 @@ onMounted(() => {
|
||||
grouping: true,
|
||||
duration: 2000,
|
||||
});
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("鼠标右键.mp3");
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
@ -98,6 +101,14 @@ onMounted(() => {
|
||||
message: `已${store.backgroundShow ? "开启" : "退出"}壁纸展示状态`,
|
||||
grouping: true,
|
||||
});
|
||||
if (store.webSpeech) {
|
||||
// 这部分原本是给禁用壁纸预览功能后提供的,如果有需要请自行修改喔!
|
||||
// 可以跟上面的 ElMessage 一样使用 store.backgroundShow 判断,调用两个不同的音频。
|
||||
// stopSpeech();
|
||||
// const voice = import.meta.env.VITE_TTS_Voice;
|
||||
// const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
// SpeechLocal("壁纸预览.mp3");
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@ -137,11 +148,13 @@ onBeforeUnmount(() => {
|
||||
transition: transform 0.3s;
|
||||
animation: fade-blur-main-in 0.65s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
animation-delay: 0.5s;
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: 0 0.5vw;
|
||||
|
||||
.all {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -151,6 +164,7 @@ onBeforeUnmount(() => {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.more {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -162,10 +176,12 @@ onBeforeUnmount(() => {
|
||||
z-index: 2;
|
||||
animation: fade 0.5s;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
padding: 0 2vw;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
@ -180,72 +196,96 @@ onBeforeUnmount(() => {
|
||||
border-radius: 6px;
|
||||
transition: transform 0.3s;
|
||||
animation: fade 0.5s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.i-icon {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
@media (min-width: 721px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 720px) {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.container {
|
||||
height: 721px;
|
||||
|
||||
.more {
|
||||
height: 721px;
|
||||
width: calc(100% + 6px);
|
||||
}
|
||||
|
||||
@media (min-width: 391px) {
|
||||
// w 1201px ~ max
|
||||
padding-left: 0.7vw;
|
||||
padding-right: 0.25vw;
|
||||
@media (max-width: 1200px) { // w 1101px ~ 1280px
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
// w 1101px ~ 1280px
|
||||
padding-left: 2.3vw;
|
||||
padding-right: 1.75vw;
|
||||
}
|
||||
@media (max-width: 1100px) { // w 993px ~ 1100px
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
// w 993px ~ 1100px
|
||||
padding-left: 2vw;
|
||||
padding-right: calc(2vw - 6px);
|
||||
}
|
||||
@media (max-width: 992px) { // w 901px ~ 992px
|
||||
|
||||
@media (max-width: 992px) {
|
||||
// w 901px ~ 992px
|
||||
padding-left: 2.3vw;
|
||||
padding-right: 1.7vw;
|
||||
}
|
||||
@media (max-width: 900px) { // w 391px ~ 900px
|
||||
|
||||
@media (max-width: 900px) {
|
||||
// w 391px ~ 900px
|
||||
padding-left: 2vw;
|
||||
padding-right: calc(2vw - 6px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
top: 605.64px; // 721px * 0.84
|
||||
left: 170.5px; // 391 * 0.5 - 25px
|
||||
|
||||
@media (min-width: 391px) {
|
||||
left: calc(50% - 25px);
|
||||
}
|
||||
}
|
||||
|
||||
.f-ter {
|
||||
top: 675px; // 721px - 46px
|
||||
|
||||
@media (min-width: 391px) {
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 390px) {
|
||||
overflow-x: auto;
|
||||
|
||||
.container {
|
||||
width: 391px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
left: 167.5px; // 391px * 0.5 - 28px
|
||||
}
|
||||
|
||||
.f-ter {
|
||||
width: 391px;
|
||||
}
|
||||
|
||||
@media (min-height: 721px) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import fetchJsonp from "fetch-jsonp";
|
||||
*/
|
||||
|
||||
// 获取音乐播放列表
|
||||
export const getPlayerList = async (server, type, id) => {
|
||||
export const getPlayerList = async (server, type, id, yrc) => {
|
||||
const res = await fetch(
|
||||
`${import.meta.env.VITE_SONG_API}?server=${server}&type=${type}&id=${id}`,
|
||||
);
|
||||
|
@ -26,6 +26,7 @@
|
||||
<script setup>
|
||||
import { mainStore } from "@/store";
|
||||
import { Error } from "@icon-park/vue-next";
|
||||
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
||||
|
||||
const store = mainStore();
|
||||
const bgUrl = ref(null);
|
||||
@ -77,6 +78,12 @@ const imgLoadError = () => {
|
||||
}),
|
||||
});
|
||||
bgUrl.value = `/images/background${bgRandom}.jpg`;
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("壁纸加载失败.mp3");
|
||||
};
|
||||
};
|
||||
|
||||
// 监听壁纸切换
|
||||
|
@ -5,8 +5,7 @@
|
||||
<span>
|
||||
<span :class="startYear < fullYear ? 'c-hidden' : 'hidden'">Copyright </span>
|
||||
©
|
||||
<span v-if="startYear < fullYear"
|
||||
class="site-start">
|
||||
<span v-if="startYear < fullYear" class="site-start">
|
||||
{{ startYear }}
|
||||
-
|
||||
</span>
|
||||
@ -26,13 +25,38 @@
|
||||
<a v-if="siteIcp" href="https://beian.miit.gov.cn" target="_blank">
|
||||
{{ siteIcp }}
|
||||
</a>
|
||||
&
|
||||
<!-- 这备那备的真的很扫(bushi) -->
|
||||
<a v-if="siteMps" href="https://beian.mps.gov.cn" target="_blank">
|
||||
{{ siteMps }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="lrc">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<Transition name="fade" mode="out-in" :id="`lrc-line-${store.playerLrc[0][2]}`"
|
||||
v-if="!(!store.yrcEnable || store.yrcTemp.length == 0 || store.yrcLoading)">
|
||||
<!-- & -->
|
||||
<!-- 逐字模块山 -->
|
||||
<div class="lrc-all"
|
||||
:key="store.playerLrc.length != 0 ? `lrc-line-${store.playerLrc[0][2]}` : `lrc-line-null`">
|
||||
<music-one theme="filled" size="18" fill="#efefef" />
|
||||
<span class="yrc-box">
|
||||
<span class="yrc-1 lrc-text text-hidden">
|
||||
<span v-for="i in store.playerLrc" :key="`lrc-char-${i[2]}-${i[3]}`"
|
||||
:style="`opacity: ${i[1] ? '1' : '0.6'}`"
|
||||
:class="`yrc-char ${i[0] ? 'fade-in' : 'fade-in-start'} ${i[0] > 1.5 ? 'long-tone' : 'fade-in-start'}`"
|
||||
:id="`lrc-char-${i[2]}-${i[3]}`" v-html="i[4]">
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<music-one theme="filled" size="18" fill="#efefef" />
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="fade" mode="out-in" v-else>
|
||||
<!-- 逐行模块 -->
|
||||
<div class="lrc-all" :key="store.getPlayerLrc">
|
||||
<music-one theme="filled" size="18" fill="#efefef" />
|
||||
<span class="lrc-text text-hidden" v-html="store.getPlayerLrc" />
|
||||
<span class="lrc-text text-hidden" v-html="store.getPlayerLrc[0][4]" :class="`lrc-char`" />
|
||||
<music-one theme="filled" size="18" fill="#efefef" />
|
||||
</div>
|
||||
</Transition>
|
||||
@ -52,8 +76,8 @@ const fullYear = new Date().getFullYear();
|
||||
// 加载配置数据
|
||||
// const siteStartDate = ref(import.meta.env.VITE_SITE_START);
|
||||
const startYear = ref(
|
||||
import.meta.env.VITE_SITE_START?.length >= 4 ?
|
||||
import.meta.env.VITE_SITE_START.substring(0, 4) : null
|
||||
import.meta.env.VITE_SITE_START?.length >= 4 ?
|
||||
import.meta.env.VITE_SITE_START.substring(0, 4) : null
|
||||
);
|
||||
const siteIcp = ref(import.meta.env.VITE_SITE_ICP);
|
||||
const siteAuthor = ref(import.meta.env.VITE_SITE_AUTHOR);
|
||||
@ -69,6 +93,100 @@ const siteUrl = computed(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.yrc-char {
|
||||
// 逐字部分
|
||||
display: inline-block;
|
||||
opacity: 0.3;
|
||||
transform: translateY(1px);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
font-family: MiSans-Regular;
|
||||
transition:
|
||||
opacity 0.3s linear,
|
||||
color 0.5s linear,
|
||||
transform 0.3s linear;
|
||||
|
||||
&.fade-in-start {
|
||||
text-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
|
||||
opacity: 0.3; // 初始显示的透明度
|
||||
transform: translateY(1px);
|
||||
transition:
|
||||
color 0.5s linear,
|
||||
opacity 0.3s linear,
|
||||
transform 0.3s linear;
|
||||
}
|
||||
|
||||
&.fade-in {
|
||||
opacity: 1;
|
||||
transform: translateY(-1px);
|
||||
animation: colorFade 0.7s ease-in-out forwards;
|
||||
transition:
|
||||
color 0.5s linear,
|
||||
opacity 0.3s linear,
|
||||
transform 0.3s linear;
|
||||
}
|
||||
|
||||
&.fade-enter-active {
|
||||
animation: float-up 0.3s linear forwards;
|
||||
}
|
||||
|
||||
&.long-tone {
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.9);
|
||||
animation: pulse 1.5s infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-up {
|
||||
from {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colorFade {
|
||||
from {
|
||||
color: #dfd9d9;
|
||||
opacity: 0.6;
|
||||
text-shadow: 0 0 3px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
to {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 6px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.8),
|
||||
0 0 20px rgba(255, 255, 255, 0.6),
|
||||
0 0 30px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
to {
|
||||
text-shadow: 0 0 15px rgba(255, 255, 255, 1),
|
||||
0 0 25px rgba(255, 255, 255, 0.8),
|
||||
0 0 35px rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.lrc-char {
|
||||
// 逐行部分
|
||||
display: inline;
|
||||
opacity: 1;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
text-shadow: 0 0 4px rgba(255, 255, 255, 0.8);
|
||||
font-family: MiSans-Regular;
|
||||
transition:
|
||||
opacity 0.3s linear,
|
||||
color 0.5s linear;
|
||||
}
|
||||
|
||||
#footer {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
@ -82,51 +200,89 @@ const siteUrl = computed(() => {
|
||||
// 文字不换行
|
||||
word-break: keep-all;
|
||||
white-space: nowrap;
|
||||
|
||||
.power {
|
||||
animation: fade 0.3s;
|
||||
}
|
||||
|
||||
.lrc {
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.lrc-all {
|
||||
width: 98%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.lrc-text {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.i-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.yrc-box {
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 0;
|
||||
|
||||
.yrc-1,
|
||||
.yrc-2 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.yrc-1 {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.yrc-2 {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.blur {
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgb(0 0 0 / 25%);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
font-size: 0.9rem;
|
||||
|
||||
&.blur {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.c-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hidden {
|
||||
display: none;
|
||||
|
@ -1,18 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="hitokoto cards"
|
||||
v-show="!store.musicOpenState"
|
||||
@mouseenter="openMusicShow = true"
|
||||
@mouseleave="openMusicShow = false"
|
||||
@click.stop
|
||||
>
|
||||
<div class="hitokoto cards" v-show="!store.musicOpenState" @mouseenter="openMusicShow = true"
|
||||
@mouseleave="openMusicShow = false" @click.stop>
|
||||
<!-- 打开音乐面板 -->
|
||||
<Transition name="el-fade-in-linear">
|
||||
<div
|
||||
class="open-music"
|
||||
v-show="openMusicShow && store.musicIsOk"
|
||||
@click="store.musicOpenState = true"
|
||||
>
|
||||
<div class="open-music" v-show="openMusicShow && store.musicIsOk" @click="store.musicOpenState = true">
|
||||
<music-menu theme="filled" size="18" fill="#efefef" />
|
||||
<span>打开音乐播放器</span>
|
||||
</div>
|
||||
@ -32,6 +23,7 @@ import { MusicMenu, Error } from "@icon-park/vue-next";
|
||||
import { getHitokoto } from "@/api";
|
||||
import { mainStore } from "@/store";
|
||||
import debounce from "@/utils/debounce.js";
|
||||
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
||||
|
||||
const store = mainStore();
|
||||
|
||||
@ -60,6 +52,12 @@ const getHitokotoData = async () => {
|
||||
});
|
||||
hitokotoData.text = "这里应该显示一句话";
|
||||
hitokotoData.from = "無名";
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("一言加载失败.mp3");
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -82,6 +80,7 @@ onMounted(() => {
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
animation: fade 0.5s;
|
||||
|
||||
.open-music {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
@ -93,21 +92,25 @@ onMounted(() => {
|
||||
background: #00000026;
|
||||
padding: 4px 0;
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
.i-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.text {
|
||||
font-size: 1.1rem;
|
||||
word-break: break-all;
|
||||
@ -117,6 +120,7 @@ onMounted(() => {
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.from {
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
|
@ -80,10 +80,6 @@ const jumpLink = (data) => {
|
||||
window.open(data.link, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log(siteLinks);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -34,6 +34,7 @@ import { Icon } from "@vicons/utils";
|
||||
import { QuoteLeft, QuoteRight } from "@vicons/fa";
|
||||
import { Error } from "@icon-park/vue-next";
|
||||
import { mainStore } from "@/store";
|
||||
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
||||
const store = mainStore();
|
||||
|
||||
// 主页站点logo
|
||||
@ -69,6 +70,12 @@ const changeBox = () => {
|
||||
fill: "#efefef",
|
||||
}),
|
||||
});
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("分辨率不足.mp3");
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -79,6 +86,12 @@ watch(
|
||||
if (value) {
|
||||
descriptionText.hello = import.meta.env.VITE_DESC_HELLO_OTHER;
|
||||
descriptionText.text = import.meta.env.VITE_DESC_TEXT_OTHER;
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("惊讶.mp3");
|
||||
};
|
||||
} else {
|
||||
descriptionText.hello = import.meta.env.VITE_DESC_HELLO;
|
||||
descriptionText.text = import.meta.env.VITE_DESC_TEXT;
|
||||
@ -95,10 +108,12 @@ watch(
|
||||
align-items: center;
|
||||
animation: fade 0.5s;
|
||||
max-width: 460px;
|
||||
|
||||
.logo-img {
|
||||
border-radius: 50%;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
padding-left: 22px;
|
||||
@ -112,17 +127,21 @@ watch(
|
||||
.sm {
|
||||
margin-left: 6px;
|
||||
font-size: 2rem;
|
||||
|
||||
@media (min-width: 721px) and (max-width: 789px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.logo-img {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.name {
|
||||
height: 128px;
|
||||
|
||||
.bg {
|
||||
font-size: 4.5rem;
|
||||
}
|
||||
@ -161,11 +180,13 @@ watch(
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
max-width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// @media (max-width: 390px) {
|
||||
// .logo {
|
||||
// flex-direction: column;
|
||||
|
@ -1,23 +1,8 @@
|
||||
<template>
|
||||
<APlayer
|
||||
v-if="playList[0]"
|
||||
ref="player"
|
||||
:audio="playList"
|
||||
:autoplay="store.playerAutoplay"
|
||||
:theme="theme"
|
||||
:autoSwitch="false"
|
||||
:loop="store.playerLoop"
|
||||
:order="store.playerOrder"
|
||||
:volume="volume"
|
||||
:showLrc="true"
|
||||
:listFolded="listFolded"
|
||||
:listMaxHeight="listMaxHeight"
|
||||
:noticeSwitch="false"
|
||||
@play="onPlay"
|
||||
@pause="onPause"
|
||||
@timeupdate="onTimeUp"
|
||||
@error="loadMusicError"
|
||||
/>
|
||||
<APlayer v-if="playList[0]" ref="player" :audio="playList" :autoplay="store.playerAutoplay" :theme="theme"
|
||||
:autoSwitch="false" :loop="store.playerLoop" :order="store.playerOrder" :volume="volume" :showLrc="true"
|
||||
:listFolded="listFolded" :listMaxHeight="listMaxHeight" :noticeSwitch="false" @play="onPlay" @pause="onPause"
|
||||
@timeupdate="onTimeUp" @error="loadMusicError" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -25,6 +10,11 @@ import { MusicOne, PlayWrong } from "@icon-park/vue-next";
|
||||
import { getPlayerList } from "@/api";
|
||||
import { mainStore } from "@/store";
|
||||
import APlayer from "@worstone/vue-aplayer";
|
||||
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
||||
import { decodeYrc } from "../utils/decodeYrc";
|
||||
|
||||
let lastTimestamp = Date.now();
|
||||
let webglRenderer;
|
||||
|
||||
const store = mainStore();
|
||||
|
||||
@ -88,14 +78,26 @@ onMounted(() => {
|
||||
nextTick(() => {
|
||||
try {
|
||||
getPlayerList(props.songServer, props.songType, props.songId).then((res) => {
|
||||
console.log(res);
|
||||
// 更改播放器加载状态
|
||||
store.musicIsOk = true;
|
||||
// 生成歌单
|
||||
playList.value = res;
|
||||
if ("mediaSession" in navigator) {
|
||||
// 设置 Media Session 操作
|
||||
navigator.mediaSession.setActionHandler("play", () => {
|
||||
player.value.play();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("pause", () => {
|
||||
player.value.pause();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("nexttrack", () => {
|
||||
changeSong(1); // 1 表示下一首
|
||||
});
|
||||
navigator.mediaSession.setActionHandler("previoustrack", () => {
|
||||
changeSong(0); // 0 表示上一首
|
||||
});
|
||||
};
|
||||
console.log("音乐加载完成");
|
||||
console.log(playList.value);
|
||||
console.log(playIndex.value, playList.value.length, props.volume);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -108,6 +110,12 @@ onMounted(() => {
|
||||
fill: "#efefef",
|
||||
}),
|
||||
});
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("播放器加载失败.mp3");
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -128,6 +136,38 @@ const onPlay = () => {
|
||||
fill: "#efefef",
|
||||
}),
|
||||
});
|
||||
|
||||
if ("mediaSession" in navigator) {
|
||||
// 更新 Media Session 元数据
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: store.getPlayerData.name,
|
||||
artist: store.getPlayerData.artist,
|
||||
artwork: [
|
||||
{
|
||||
src: playList.value[playIndex.value].cover, // 使用当前播放项的封面图像
|
||||
sizes: "512x512",
|
||||
type: "image/jpeg",
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
if (store.webSpeech) {
|
||||
if (store.playerSpeechName) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
Speech(
|
||||
"正在播放,“" +
|
||||
store.getPlayerData.artist +
|
||||
"”的歌曲,《" +
|
||||
store.getPlayerData.name +
|
||||
"》。",
|
||||
voice,
|
||||
vstyle,
|
||||
);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// 暂停
|
||||
@ -136,20 +176,150 @@ const onPause = () => {
|
||||
};
|
||||
|
||||
// 音频时间更新事件
|
||||
const onTimeUp = () => {
|
||||
let lyrics = player.value.aplayer.lyrics[playIndex.value];
|
||||
let lyricIndex = player.value.aplayer.lyricIndex;
|
||||
if (!lyrics || !lyrics[lyricIndex]) {
|
||||
return;
|
||||
function showYrc() {
|
||||
if (player.value == null) {
|
||||
return requestAnimationFrame(showYrc);
|
||||
}
|
||||
let lrc = lyrics[lyricIndex][1];
|
||||
if (lrc === "Loading") {
|
||||
lrc = "歌词加载中";
|
||||
} else if (lrc === "Not available") {
|
||||
lrc = "歌词加载失败";
|
||||
const aplayer = player.value.aplayer;
|
||||
const lyrics = aplayer.lyrics[playIndex.value];
|
||||
if (store.playerYrcShow != true) {
|
||||
store.yrcEnable = false;
|
||||
store.yrcTemp = [];
|
||||
store.yrcLoading = false;
|
||||
}
|
||||
store.setPlayerLrc(lrc);
|
||||
};
|
||||
else {
|
||||
if (store.yrcIndex != playIndex.value) {
|
||||
const yrcUrl = aplayer.audio[aplayer.index]["lrc"] + "&yrc=true";
|
||||
store.yrcIndex = playIndex.value;
|
||||
store.yrcLoading = true;
|
||||
fetch(yrcUrl)
|
||||
.then((i) => {
|
||||
if (i.status < 200 || i.status >= 400) {
|
||||
throw i.text();
|
||||
};
|
||||
return i.text();
|
||||
})
|
||||
.then((i) => {
|
||||
store.yrcIndex = playIndex.value;
|
||||
if (i.startsWith("[ch:0]")) {
|
||||
store.yrcEnable = true;
|
||||
store.yrcTemp = decodeYrc(i);
|
||||
store.yrcLoading = false;
|
||||
return;
|
||||
} else if (!store.playerYrcATDB) {
|
||||
store.yrcEnable = false;
|
||||
store.yrcTemp = [];
|
||||
store.yrcLoading = false;
|
||||
return;
|
||||
};
|
||||
// 接入 AMLL TTML Database
|
||||
const songIdMatch = yrcUrl.match(/netease.*?id=(.*?)&/);
|
||||
const songId = songIdMatch ? songIdMatch[1] : null;
|
||||
if (!songId) {
|
||||
return;
|
||||
};
|
||||
const amllUrl = `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.yrc`;
|
||||
return fetch(amllUrl)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw response.text()
|
||||
};
|
||||
return response.text();
|
||||
}).then((amllyrcfile) => {
|
||||
store.yrcEnable = true;
|
||||
store.yrcTemp = decodeYrc(amllyrcfile);
|
||||
store.yrcLoading = false;
|
||||
});
|
||||
}).catch(() => {
|
||||
store.yrcEnable = false;
|
||||
store.yrcTemp = [];
|
||||
store.yrcLoading = false;
|
||||
});
|
||||
};
|
||||
};
|
||||
if (!store.yrcEnable || store.yrcTemp.length == 0 || store.yrcLoading) {
|
||||
// 逐行模块
|
||||
let lyricIndex = player.value.aplayer.lyricIndex;
|
||||
if (lyrics === undefined || lyrics[lyricIndex] === undefined) {
|
||||
return requestAnimationFrame(showYrc);
|
||||
}
|
||||
let lrc = lyrics[lyricIndex][1];
|
||||
if (lrc === "Loading") {
|
||||
lrc = "歌词加载中";
|
||||
} else if (lrc === "Not available") {
|
||||
if (store.playerYrcATDB) {
|
||||
// 哈哈哈又是你()
|
||||
const lrcUrlw = aplayer.audio[aplayer.index]["lrc"];
|
||||
const songIdMatchlrc = lrcUrlw.match(/netease.*?id=(.*?)&/);
|
||||
const songIdlrc = songIdMatchlrc ? songIdMatchlrc[1] : null;
|
||||
if (songIdlrc) {
|
||||
const amllUrllrc = `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songIdlrc}.lrc`;
|
||||
fetch(amllUrllrc)
|
||||
.then((response) => {
|
||||
if (response.status === 404 || !response.ok) {
|
||||
lrc = "歌词加载失败";
|
||||
return;
|
||||
} else {
|
||||
return response.text();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
lrc = "歌词加载失败";
|
||||
});
|
||||
}
|
||||
} else {
|
||||
lrc = "歌词加载失败";
|
||||
};
|
||||
}
|
||||
const output = [[true, 1, lyricIndex, 0, lrc]];
|
||||
if (store.playerLrc.toString() != output.toString()) {
|
||||
store.setPlayerLrc(output);
|
||||
}
|
||||
return requestAnimationFrame(showYrc);
|
||||
}
|
||||
// 逐字模块
|
||||
if (store.playerYrcShowPro) {
|
||||
if (!webglRenderer) {
|
||||
const canvas = document.getElementById("lyricsCanvas");
|
||||
webglRenderer = new WebGLLyricsRenderer(canvas);
|
||||
}
|
||||
}
|
||||
const now = player.value.audioStatus.playedTime * 1000;
|
||||
const yrcFiltered = store.yrcTemp.filter((i) => i[0] < now);
|
||||
const yrc1 = document.querySelector(".yrc-1")
|
||||
if (yrc1 == null) {
|
||||
return requestAnimationFrame(showYrc);
|
||||
}
|
||||
const yrcLyric =
|
||||
yrcFiltered.length > 0
|
||||
? yrcFiltered.splice(-1)[0][2].map((it) => {
|
||||
const [[start, duration], word, line, row] = it;
|
||||
const isCurrent = now >= start && now <= start + duration;
|
||||
const isSungLyrics = start + duration < now;
|
||||
if (!isCurrent) {
|
||||
return [isCurrent, isSungLyrics, line, row, word, "auto"];
|
||||
}
|
||||
const thisDom = yrc1.querySelector(`#lrc-char-${line}-${row}`)
|
||||
if (thisDom == null) {
|
||||
return [isCurrent, isSungLyrics, line, row, word, "auto"];
|
||||
}
|
||||
const x = thisDom.offsetWidth * (now - start) / duration
|
||||
if (x == null || x == NaN) {
|
||||
return [isCurrent, isSungLyrics, line, row, word, "auto"];
|
||||
}
|
||||
return [isCurrent, isSungLyrics, line, row, word, `${x}px`]
|
||||
})
|
||||
: [[true, 1, 0, 0, `${store.playerTitle} - ${store.playerArtist}`]];
|
||||
|
||||
if (store.playerLrc.toString() != yrcLyric.toString()) {
|
||||
store.setPlayerLrc(yrcLyric);
|
||||
}
|
||||
if (store.playerYrcShowPro) {
|
||||
webglRenderer.render(yrcLyric);
|
||||
}
|
||||
requestAnimationFrame(showYrc);
|
||||
}
|
||||
requestAnimationFrame(showYrc);
|
||||
|
||||
// 切换播放暂停事件
|
||||
const playToggle = () => {
|
||||
@ -179,8 +349,20 @@ const loadMusicError = () => {
|
||||
let notice = "";
|
||||
if (playList.value.length > 1) {
|
||||
notice = "播放歌曲出现错误,播放器将在 2s 后进行下一首";
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("歌曲加载失败.mp3");
|
||||
};
|
||||
} else {
|
||||
notice = "播放歌曲出现错误";
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("播放器未知异常.mp3");
|
||||
};
|
||||
}
|
||||
ElMessage({
|
||||
message: notice,
|
||||
@ -205,78 +387,91 @@ defineExpose({ playToggle, changeVolume, changeSong, toggleList });
|
||||
width: 80%;
|
||||
border-radius: 6px;
|
||||
font-family: "HarmonyOS_Regular", sans-serif !important;
|
||||
|
||||
:deep(.aplayer-body) {
|
||||
background-color: transparent;
|
||||
|
||||
.aplayer-pic {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aplayer-info {
|
||||
margin-left: 0;
|
||||
background-color: #ffffff40;
|
||||
border-color: transparent !important;
|
||||
|
||||
.aplayer-music {
|
||||
flex-grow: initial;
|
||||
margin-bottom: 2px;
|
||||
overflow: initial;
|
||||
|
||||
.aplayer-title {
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.aplayer-author {
|
||||
color: #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
.aplayer-lrc {
|
||||
text-align: left;
|
||||
margin: 7px 0 6px 6px;
|
||||
height: 44px;
|
||||
mask: linear-gradient(
|
||||
#fff 15%,
|
||||
#fff 85%,
|
||||
hsla(0deg, 0%, 100%, 0.6) 90%,
|
||||
hsla(0deg, 0%, 100%, 0)
|
||||
);
|
||||
-webkit-mask: linear-gradient(
|
||||
#fff 15%,
|
||||
#fff 85%,
|
||||
hsla(0deg, 0%, 100%, 0.6) 90%,
|
||||
hsla(0deg, 0%, 100%, 0)
|
||||
);
|
||||
mask: linear-gradient(#fff 15%,
|
||||
#fff 85%,
|
||||
hsla(0deg, 0%, 100%, 0.6) 90%,
|
||||
hsla(0deg, 0%, 100%, 0));
|
||||
-webkit-mask: linear-gradient(#fff 15%,
|
||||
#fff 85%,
|
||||
hsla(0deg, 0%, 100%, 0.6) 90%,
|
||||
hsla(0deg, 0%, 100%, 0));
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #efefef;
|
||||
}
|
||||
|
||||
.aplayer-lrc-current {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.aplayer-controller {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.aplayer-list) {
|
||||
margin-top: 6px;
|
||||
height: v-bind(listHeight);
|
||||
background-color: transparent;
|
||||
|
||||
ol {
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
li {
|
||||
border-color: transparent;
|
||||
|
||||
&.aplayer-list-light {
|
||||
background: #ffffff40;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #ffffff26 !important;
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
|
||||
.aplayer-list-index,
|
||||
.aplayer-list-author {
|
||||
color: #efefef;
|
||||
|
@ -14,61 +14,26 @@
|
||||
<el-collapse-item title="个性化调整" name="2">
|
||||
<div class="item">
|
||||
<span class="text">建站日期显示</span>
|
||||
<el-switch
|
||||
v-model="siteStartShow"
|
||||
inline-prompt
|
||||
:active-icon="CheckSmall"
|
||||
:inactive-icon="CloseSmall"
|
||||
/>
|
||||
<el-switch v-model="siteStartShow" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="text">音乐点击是否打开面板</span>
|
||||
<el-switch
|
||||
v-model="musicClick"
|
||||
inline-prompt
|
||||
:active-icon="CheckSmall"
|
||||
:inactive-icon="CloseSmall"
|
||||
/>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="text">底栏歌词显示</span>
|
||||
<el-switch
|
||||
v-model="playerLrcShow"
|
||||
inline-prompt
|
||||
:active-icon="CheckSmall"
|
||||
:inactive-icon="CloseSmall"
|
||||
/>
|
||||
<el-switch v-model="musicClick" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="text">底栏背景模糊</span>
|
||||
<el-switch
|
||||
v-model="footerBlur"
|
||||
inline-prompt
|
||||
:active-icon="CheckSmall"
|
||||
:inactive-icon="CloseSmall"
|
||||
/>
|
||||
<el-switch v-model="footerBlur" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="播放器配置" name="3">
|
||||
<div class="item">
|
||||
<span class="text">自动播放</span>
|
||||
<el-switch
|
||||
v-model="playerAutoplay"
|
||||
inline-prompt
|
||||
:active-icon="CheckSmall"
|
||||
:inactive-icon="CloseSmall"
|
||||
/>
|
||||
<el-switch v-model="playerAutoplay" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="text">随机播放</span>
|
||||
<el-switch
|
||||
v-model="playerOrder"
|
||||
inline-prompt
|
||||
:active-icon="CheckSmall"
|
||||
:inactive-icon="CloseSmall"
|
||||
active-value="random"
|
||||
inactive-value="list"
|
||||
/>
|
||||
<el-switch v-model="playerOrder" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall"
|
||||
active-value="random" inactive-value="list" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="text">循环模式</span>
|
||||
@ -79,8 +44,37 @@
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="其他设置" name="4">
|
||||
<div>设置内容待增加</div>
|
||||
<el-collapse-item title="歌词设置" name="4">
|
||||
<div class="item">
|
||||
<span class="text">显示底栏歌词</span>
|
||||
<el-switch v-model="playerLrcShow" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
<div v-if="playerLrcShow" class="item">
|
||||
<span class="text" white-space="pre">允许调用 AMLL TTML Database 加载网易云没有的歌词<br> (在 Github
|
||||
不稳定的网络中可能导致歌词载入速度变慢)</span>
|
||||
<el-switch v-model="playerYrcATDB" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
<div v-if="playerLrcShow" class="item">
|
||||
<span class="text">逐字歌词解析总开关</span>
|
||||
<el-switch v-model="playerYrcShow" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
<div v-if="playerLrcShow && playerYrcShow" class="item">
|
||||
<span class="text">逐字效果增强开关(更高的性能要求)</span>
|
||||
<el-switch v-model="playerYrcShowPro" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="语音设置" name="5">
|
||||
<div class="item">
|
||||
<span class="text">网页语音交互总开关</span>
|
||||
<el-switch v-model="webSpeech" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
<div v-if="webSpeech" class="item">
|
||||
<span class="text">播报歌名</span>
|
||||
<el-switch v-model="playerSpeechName" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="其他设置" name="6">
|
||||
<div>暂时没有其它啦qwq</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
@ -90,6 +84,7 @@
|
||||
import { CheckSmall, CloseSmall, SuccessPicture } from "@icon-park/vue-next";
|
||||
import { mainStore } from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
||||
|
||||
const store = mainStore();
|
||||
const {
|
||||
@ -101,6 +96,11 @@ const {
|
||||
playerAutoplay,
|
||||
playerOrder,
|
||||
playerLoop,
|
||||
webSpeech,
|
||||
playerSpeechName,
|
||||
playerYrcShow,
|
||||
playerYrcShowPro,
|
||||
playerYrcATDB,
|
||||
} = storeToRefs(store);
|
||||
|
||||
// 默认选中项
|
||||
@ -115,6 +115,12 @@ const radioChange = () => {
|
||||
fill: "#efefef",
|
||||
}),
|
||||
});
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("更换壁纸成功.mp3");
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -139,16 +145,19 @@ const radioChange = () => {
|
||||
|
||||
.el-collapse-item__content {
|
||||
padding: 20px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
font-size: 14px;
|
||||
|
||||
.el-switch__core {
|
||||
border-color: transparent;
|
||||
background-color: #ffffff30;
|
||||
}
|
||||
|
||||
.el-radio-group {
|
||||
.el-radio {
|
||||
margin: 2px 10px 2px 0;
|
||||
@ -160,6 +169,7 @@ const radioChange = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-radio-group {
|
||||
justify-content: space-between;
|
||||
|
||||
@ -189,7 +199,7 @@ const radioChange = () => {
|
||||
border-color: #fff !important;
|
||||
}
|
||||
|
||||
& + .el-radio__label {
|
||||
&+.el-radio__label {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
@ -53,12 +53,14 @@ onBeforeUnmount(() => {
|
||||
<style lang="scss" scoped>
|
||||
.time-capsule {
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0.2rem 0 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
|
||||
.i-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -66,9 +68,11 @@ onBeforeUnmount(() => {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.all-capsule {
|
||||
.capsule-item {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.item-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -76,15 +80,18 @@ onBeforeUnmount(() => {
|
||||
justify-content: space-between;
|
||||
margin: 1rem 0rem 0.5rem 0rem;
|
||||
font-size: 0.95rem;
|
||||
|
||||
.remaining {
|
||||
opacity: 0.6;
|
||||
font-size: 0.85rem;
|
||||
font-style: oblique;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.start {
|
||||
.item-title {
|
||||
justify-content: center;
|
||||
|
@ -46,6 +46,12 @@ const getTemperature = (min, max) => {
|
||||
return Math.round(average);
|
||||
} catch (error) {
|
||||
console.error("计算温度出现错误:", error);
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("天气信息无法计算.mp3");
|
||||
};
|
||||
return "NaN";
|
||||
}
|
||||
};
|
||||
@ -74,6 +80,12 @@ const getWeatherData = async () => {
|
||||
const adCode = await getAdcode(mainKey);
|
||||
console.log(adCode);
|
||||
if (adCode.infocode !== "10000") {
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("位置信息获取失败.mp3");
|
||||
};
|
||||
throw "地区查询失败";
|
||||
}
|
||||
weatherData.adCode = {
|
||||
@ -92,6 +104,12 @@ const getWeatherData = async () => {
|
||||
} catch (error) {
|
||||
console.error("天气信息获取失败:" + error);
|
||||
onError("天气信息获取失败");
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("天气加载失败.mp3");
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
42
src/main.js
42
src/main.js
@ -1,15 +1,45 @@
|
||||
import { createApp } from "vue";
|
||||
import "@/style/style.scss";
|
||||
import App from "@/App.vue";
|
||||
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
||||
// 引入 pinia
|
||||
import { createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import { createPinia } from 'pinia';
|
||||
import { createPersistedStatePlugin } from 'pinia-plugin-persistedstate-2';
|
||||
// swiper
|
||||
import "swiper/css";
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
pinia.use(
|
||||
createPersistedStatePlugin({
|
||||
// 为升级 pinia 后的兼容性保留的一块巧克力(
|
||||
storage: sessionStorage,
|
||||
})
|
||||
);
|
||||
export default pinia;
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
// 这堆代码原本的意义是在于强制刷新这些本不需要被 pinia 缓存的变量,不知为什么这些变量只会在关闭页面重新输入域名访问才能恢复,导致刷新页面部分模块短时间内出现异常。
|
||||
// 但是貌似这堆代码也没能解决问题...罢了,先暂且留着叭()
|
||||
const store = mainStore();
|
||||
store.imgLoadStatus = false; // 壁纸加载状态
|
||||
store.innerWidth = null; // 当前窗口宽度
|
||||
store.musicIsOk = false; // 音乐是否加载完成
|
||||
store.musicOpenState = false; // 音乐面板开启状态
|
||||
store.backgroundShow = false; // 壁纸展示状态
|
||||
store.boxOpenState = false; // 盒子开启状态
|
||||
store.mobileOpenState = false; // 移动端开启状态
|
||||
store.mobileFuncState = false; // 移动端功能区开启状态
|
||||
store.setOpenState = false; // 设置页面开启状态
|
||||
store.playerState = false; // 当前播放状态
|
||||
store.playerTitle = null; // 当前播放歌曲名
|
||||
store.playerArtist = null; // 当前播放歌手名
|
||||
store.playerLrc = [[true, "猫猫正在翻找歌词..."]]; // 当前播放歌词
|
||||
store.yrcIndex = -1; // 逐字歌词进度存储
|
||||
store.yrcTemp = []; // 逐字歌词缓存
|
||||
store.yrcEnable = true;
|
||||
store.yrcLoading = false;
|
||||
});
|
||||
|
||||
app.use(pinia);
|
||||
app.mount("#app");
|
||||
@ -19,4 +49,10 @@ navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
// 弹出更新提醒
|
||||
console.log("站点已更新,刷新后生效");
|
||||
ElMessage("站点已更新,刷新后生效");
|
||||
if (store.webSpeech) {
|
||||
stopSpeech();
|
||||
const voice = import.meta.env.VITE_TTS_Voice;
|
||||
const vstyle = import.meta.env.VITE_TTS_Style;
|
||||
SpeechLocal("网站更新.mp3");
|
||||
};
|
||||
});
|
||||
|
@ -1,32 +1,39 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const mainStore = defineStore("main", {
|
||||
state: () => {
|
||||
return {
|
||||
imgLoadStatus: false, // 壁纸加载状态
|
||||
innerWidth: null, // 当前窗口宽度
|
||||
coverType: "0", // 壁纸种类
|
||||
siteStartShow: false, // 建站日期显示
|
||||
musicClick: false, // 音乐链接是否跳转
|
||||
musicIsOk: false, // 音乐是否加载完成
|
||||
musicVolume: 0, // 音乐音量;
|
||||
musicOpenState: false, // 音乐面板开启状态
|
||||
backgroundShow: false, // 壁纸展示状态
|
||||
boxOpenState: false, // 盒子开启状态
|
||||
mobileOpenState: false, // 移动端开启状态
|
||||
mobileFuncState: false, // 移动端功能区开启状态
|
||||
setOpenState: false, // 设置页面开启状态
|
||||
playerState: false, // 当前播放状态
|
||||
playerTitle: null, // 当前播放歌曲名
|
||||
playerArtist: null, // 当前播放歌手名
|
||||
playerLrc: "歌词加载中", // 当前播放歌词
|
||||
playerLrcShow: true, // 是否显示底栏歌词
|
||||
footerBlur: true, // 底栏模糊
|
||||
playerAutoplay: false, // 是否自动播放
|
||||
playerLoop: "all", // 循环播放 "all", "one", "none"
|
||||
playerOrder: "list", // 循环顺序 "list", "random"
|
||||
};
|
||||
},
|
||||
state: () => ({
|
||||
imgLoadStatus: false, // 壁纸加载状态
|
||||
innerWidth: null, // 当前窗口宽度
|
||||
coverType: "1", // 壁纸种类
|
||||
siteStartShow: true, // 建站日期显示
|
||||
musicClick: true, // 音乐链接是否跳转
|
||||
musicIsOk: false, // 音乐是否加载完成
|
||||
musicVolume: 0.7, // 音乐音量;
|
||||
musicOpenState: false, // 音乐面板开启状态
|
||||
backgroundShow: false, // 壁纸展示状态
|
||||
boxOpenState: false, // 盒子开启状态
|
||||
mobileOpenState: false, // 移动端开启状态
|
||||
mobileFuncState: false, // 移动端功能区开启状态
|
||||
setOpenState: false, // 设置页面开启状态
|
||||
playerState: false, // 当前播放状态
|
||||
playerTitle: null, // 当前播放歌曲名
|
||||
playerArtist: null, // 当前播放歌手名
|
||||
playerLrc: [[true, "歌词加载中"]], // 当前播放歌词
|
||||
playerLrcShow: true, // 是否显示底栏歌词
|
||||
footerBlur: true, // 底栏模糊
|
||||
playerAutoplay: true, // 是否自动播放
|
||||
playerLoop: "all", // 循环播放 "all", "one", "none"
|
||||
playerOrder: "random", // 循环顺序 "list", "random"
|
||||
webSpeech: false, // 网页语音交互总开关(包含播报歌名功能)
|
||||
playerSpeechName: true, // 播报歌名
|
||||
playerYrcShow: true, // 逐字歌词解析总开关
|
||||
playerYrcShowPro: false, // 逐字效果增强开关(更高的性能要求)
|
||||
playerYrcATDB: true, // 允许接入 AMLL TTML Database
|
||||
yrcIndex: -1, // 逐字歌词进度存储
|
||||
yrcTemp: [], // 逐字歌词缓存
|
||||
yrcEnable: true,
|
||||
yrcLoading: false,
|
||||
}),
|
||||
getters: {
|
||||
// 获取歌词
|
||||
getPlayerLrc(state) {
|
||||
@ -88,6 +95,11 @@ export const mainStore = defineStore("main", {
|
||||
"playerAutoplay",
|
||||
"playerLoop",
|
||||
"playerOrder",
|
||||
"webSpeech",
|
||||
"playerSpeechName",
|
||||
"playerYrcShow",
|
||||
"playerYrcShowPro",
|
||||
"playerYrcATDB",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
@ -56,6 +56,12 @@ a:hover {
|
||||
src: url("/font/UnidreamLED.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'MiSans-Regular';
|
||||
font-display: swap;
|
||||
src: url('/font/MiSans-Regular.otf') format('opentype');
|
||||
}
|
||||
|
||||
// 基础样式
|
||||
#app {
|
||||
position: fixed;
|
||||
|
41
src/utils/decodeYrc.js
Normal file
41
src/utils/decodeYrc.js
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Decode yrc text
|
||||
* @param {string} i - yrc input
|
||||
* @returns {[number,number,[[number,number],string,number,number][]]}
|
||||
*/
|
||||
export function decodeYrc(i) {
|
||||
let x = 0;
|
||||
return i
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((it) => it != "[ch:0]")
|
||||
.map((i) => {
|
||||
const line = i.split(/\[([0-9]+),([0-9]+)\](.+)/).slice(1, -1);
|
||||
const start = parseInt(line[0]);
|
||||
const dur = parseInt(line[1]);
|
||||
let frame = [];
|
||||
let stack = [];
|
||||
let y = 0;
|
||||
if (line[2] == undefined) {
|
||||
return;
|
||||
}
|
||||
line[2]
|
||||
.split(/(\([0-9]+,[0-9]+,[0-9]+\))/)
|
||||
.slice(1)
|
||||
.forEach((it) => {
|
||||
if (frame.length == 0) {
|
||||
const ir = it.split(/\(([0-9]+),([0-9]+),[0-9]+\)/).slice(1, -1);
|
||||
frame.push([parseInt(ir[0]), parseInt(ir[1])]);
|
||||
return;
|
||||
}
|
||||
frame.push(it.replace(' '," "));
|
||||
frame.push(x);
|
||||
frame.push(y);
|
||||
y += 1;
|
||||
stack.push(frame);
|
||||
frame = [];
|
||||
});
|
||||
x += 1;
|
||||
return [start, dur, stack];
|
||||
});
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { h } from "vue";
|
||||
import { SpaCandle } from "@icon-park/vue-next";
|
||||
import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// 时钟
|
||||
@ -69,30 +70,49 @@ export const getTimeCapsule = () => {
|
||||
};
|
||||
|
||||
// 欢迎提示
|
||||
export const helloInit = () => {
|
||||
export const helloInit = (store) => {
|
||||
const hour = new Date().getHours();
|
||||
let hello = null;
|
||||
if (hour < 6) {
|
||||
hello = "凌晨好";
|
||||
let hellosound = null;
|
||||
stopSpeech();
|
||||
if (hour < 5) {
|
||||
hello = "凌晨好,该睡了啦!";
|
||||
hellosound = "欢迎1.mp3";
|
||||
} else if (hour < 7) {
|
||||
hello = "早上好,起的真早哦~";
|
||||
hellosound = "欢迎2.mp3";
|
||||
} else if (hour < 9) {
|
||||
hello = "早上好";
|
||||
} else if (hour < 12) {
|
||||
hello = "上午好";
|
||||
hello = "早上好,又是新的一天~";
|
||||
hellosound = "欢迎3.mp3";
|
||||
} else if (hour < 11) {
|
||||
hello = "上午好!";
|
||||
hellosound = "欢迎4.mp3";
|
||||
} else if (hour < 14) {
|
||||
hello = "中午好";
|
||||
hello = "中午好,辛苦了一个上午,补充下能量吧~";
|
||||
hellosound = "欢迎5.mp3";
|
||||
} else if (hour < 17) {
|
||||
hello = "下午好";
|
||||
} else if (hour < 19) {
|
||||
hello = "傍晚好";
|
||||
hello = "下午好!";
|
||||
hellosound = "欢迎6.mp3";
|
||||
} else if (hour < 18) {
|
||||
hello = "傍晚好,吃顿美味的晚餐休息休息吧~";
|
||||
hellosound = "欢迎7.mp3";
|
||||
} else if (hour < 22) {
|
||||
hello = "晚上好";
|
||||
hello = "晚上好,娱乐一下,放松心情~";
|
||||
hellosound = "欢迎8.mp3";
|
||||
} else if (hour < 23) {
|
||||
hello = "深夜好!夜深了,晚安噢w";
|
||||
hellosound = "欢迎9.mp3";
|
||||
} else {
|
||||
hello = "夜深了";
|
||||
}
|
||||
hello = "深夜好!都快凌晨了啦,早点休息哦~";
|
||||
hellosound = "欢迎10.mp3";
|
||||
};
|
||||
ElMessage({
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: `<strong>${hello}</strong> 欢迎来到我的主页`,
|
||||
});
|
||||
if (store.webSpeech) {
|
||||
SpeechLocal(hellosound);
|
||||
};
|
||||
};
|
||||
|
||||
// 默哀模式
|
||||
|
173
src/utils/speech.js
Normal file
173
src/utils/speech.js
Normal file
@ -0,0 +1,173 @@
|
||||
let currentAudio = null;
|
||||
let audioQueue = [];
|
||||
let isPlaying = false;
|
||||
let controller = null; // 用于取消请求
|
||||
|
||||
export function Speech(
|
||||
// 该功能原为 Azure 设计,理应兼容大部分使用 post 传参的 api 。请自行根据要求修改!
|
||||
text,
|
||||
voice = "zh-CN-YunxiaNeural",
|
||||
style = "cheerful",
|
||||
role = "Boy",
|
||||
rate = "1",
|
||||
volume = "100",
|
||||
) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// 创建新的 AbortController 实例,并中断旧请求
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
}
|
||||
controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("text", text);
|
||||
formData.append("voice", voice);
|
||||
formData.append("style", style);
|
||||
formData.append("role", role);
|
||||
formData.append("rate", rate);
|
||||
formData.append("volume", volume);
|
||||
|
||||
try {
|
||||
const speechapi = import.meta.env.VITE_TTS_API;
|
||||
const response = await fetch(speechapi, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
signal, // 传递 AbortSignal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const audioUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 将新的音频对象添加到队列
|
||||
audioQueue.push(audioUrl);
|
||||
|
||||
if (!isPlaying) {
|
||||
playNext();
|
||||
}
|
||||
|
||||
function playNext() {
|
||||
if (audioQueue.length === 0) {
|
||||
isPlaying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isPlaying = true;
|
||||
|
||||
const nextAudioUrl = audioQueue.shift();
|
||||
if (currentAudio) {
|
||||
currentAudio.pause();
|
||||
currentAudio = null;
|
||||
}
|
||||
|
||||
const audio = new Audio();
|
||||
audio.src = nextAudioUrl;
|
||||
audio.play();
|
||||
|
||||
// 在音频播放结束时解析 Promise
|
||||
audio.onended = () => {
|
||||
resolve();
|
||||
playNext();
|
||||
};
|
||||
|
||||
// 如果发生错误,拒绝 Promise
|
||||
audio.onerror = (error) => {
|
||||
reject(error);
|
||||
playNext();
|
||||
};
|
||||
|
||||
// 将当前播放的语音赋值给全局变量
|
||||
currentAudio = audio;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Request canceled");
|
||||
} else {
|
||||
console.error("Error:", error.message);
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加停止播放的函数
|
||||
export function stopSpeech() {
|
||||
if (currentAudio) {
|
||||
currentAudio.pause();
|
||||
currentAudio = null;
|
||||
}
|
||||
audioQueue = [];
|
||||
isPlaying = false;
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function SpeechLocal(fileName) {
|
||||
// 考虑到生成延迟,所以加了这个,仅必要模块调用 api 实时生成,其它模块使用预先生成好的音频
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!fileName) {
|
||||
reject(new Error("No file name provided"));
|
||||
return;
|
||||
}
|
||||
|
||||
const audioUrl = `/speechlocal/${fileName}`;
|
||||
|
||||
// 清除之前的音频
|
||||
if (currentAudio) {
|
||||
currentAudio.pause();
|
||||
currentAudio = null;
|
||||
}
|
||||
|
||||
// 停止当前正在播放的语音
|
||||
audioQueue = [];
|
||||
isPlaying = false;
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
controller = null;
|
||||
}
|
||||
|
||||
// 添加新音频到队列并播放
|
||||
audioQueue.push(audioUrl);
|
||||
if (!isPlaying) {
|
||||
playNext();
|
||||
}
|
||||
|
||||
function playNext() {
|
||||
if (audioQueue.length === 0) {
|
||||
isPlaying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isPlaying = true;
|
||||
|
||||
const nextAudioUrl = audioQueue.shift();
|
||||
const audio = new Audio();
|
||||
audio.src = nextAudioUrl;
|
||||
|
||||
// 确保新的音频对象没有被中途替换
|
||||
audio.oncanplaythrough = () => {
|
||||
currentAudio = audio;
|
||||
currentAudio.play();
|
||||
};
|
||||
|
||||
// 在音频播放结束时解析 Promise
|
||||
audio.onended = () => {
|
||||
resolve();
|
||||
playNext();
|
||||
};
|
||||
|
||||
// 如果发生错误,拒绝 Promise
|
||||
audio.onerror = (error) => {
|
||||
reject(error);
|
||||
playNext();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
@ -107,7 +107,7 @@ export default ({ mode }) =>
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
charset: false,
|
||||
additionalData: `@import "./src/style/global.scss";`,
|
||||
additionalData: `@use "./src/style/global.scss" as global;`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user