This commit is contained in:
NanoRocky 2024-11-12 10:52:01 +08:00
parent 081834c3b1
commit dd81d31748
46 changed files with 3508 additions and 2012 deletions

View File

@ -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"

View File

@ -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

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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;
}

View File

@ -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}`,
);

View File

@ -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");
};
};
//

View File

@ -5,8 +5,7 @@
<span>
<span :class="startYear < fullYear ? 'c-hidden' : 'hidden'">Copyright&nbsp;</span>
&copy;
<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>
&amp;
<!-- 这备那备的真的很扫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)">
<!-- &amp; -->
<!-- 逐字模块山 -->
<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;

View File

@ -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;

View File

@ -80,10 +80,6 @@ const jumpLink = (data) => {
window.open(data.link, "_blank");
}
};
onMounted(() => {
console.log(siteLinks);
});
</script>
<style lang="scss" scoped>

View File

@ -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;

View File

@ -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;

View File

@ -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>&nbsp;&nbsp;&nbsp; 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;
}
}

View File

@ -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;

View File

@ -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");
};
}
};

View File

@ -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");
};
});

View File

@ -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",
],
},
});

View File

@ -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
View 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(' ',"&nbsp;"));
frame.push(x);
frame.push(y);
y += 1;
stack.push(frame);
frame = [];
});
x += 1;
return [start, dur, stack];
});
}

View File

@ -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
View 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();
};
}
});
}

View File

@ -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;`,
},
},
},