suno/src/App.vue
2024-07-23 10:53:10 +08:00

1768 lines
58 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { RouterLink, RouterView } from "vue-router"
import { nextTick, onMounted, ref } from "vue"
import { getQrcode, getList, monitorState, Generate, monitorMusic, getDetails, lyricsGenerateHttp, lyricsMonitorHttp, quitLoginHttp, likeDetailsHttp } from "./utils/api"
import { ElMessage } from "element-plus"
import triangleIcon from "@/components/triangle-icon.vue"
onMounted(() => {
// getLoginQrcode()
getListData()
})
let list = ref([])
let listIndex = ref(null) // 列表选中的 Index
const getListData = () => {
getList().then(res => {
if (res.code == 401) {
getLoginQrcode()
return
}
if (res.code != 200) return
const data = res.data || []
let isNeedMonitor = false
data.forEach(element => {
if (element.status != "complete") {
isNeedMonitor = true
}
})
if (isNeedMonitor) {
monitorMusicState()
}
// data[0].audio_url = "./21be5d73-221f-4e7b-82f0-26c7c71b797e.mp3"
islogin.value = true
list.value = data || []
})
}
let wxloginVisible = ref(false) // 登录弹窗显示状态
let wxloginQrcodeLose = ref(false) // 登录二维码失效状态
let islogin = ref(false) // 登录状态
let token = ""
let qrcode = ref("")
// 获取二维码
const getLoginQrcode = () => {
getQrcode().then(res => {
if (res.code != 200) return
const data = res.data || {}
token = data.token || ""
qrcode.value = data.qrcode || ""
wxloginVisible.value = true
monitorLogin()
})
}
// 监听扫码状态
let monitorTimer = null
const monitorLogin = () => {
monitorTimer = setInterval(() => {
monitorState({ token: token }).then(res => {
if (res.code == 401) {
getLoginQrcode()
return
}
// code 200 为登录成功 400 是等待 201 是失效
if (res.code == 400) return
clearInterval(monitorTimer)
if (res.code == 201) {
wxloginQrcodeLose.value = true
}
if (res.code == 200) {
ElMessage({
showClose: true,
message: res.message,
type: "success",
})
wxloginVisible.value = false
getListData()
localStorage.setItem("token", res.data?.token)
}
})
}, 3000)
}
const handleClose = () => {
wxloginVisible.value = false
wxloginQrcodeLose.value = false
clearInterval(monitorTimer)
}
let options = ref([
{
label: "V2 (老款模型最长生成1分钟左右)",
value: "chirp-v2-xxl-alpha",
},
{
label: "V3 (广泛、多才多艺最多生成2分钟)",
value: "chirp-v3-0",
},
{
label: "V3.5 (最新模型更好的歌曲结构最长生成4分钟",
value: "chirp-v3-5",
},
])
let type = ref("inspiration") // inspiration 灵感 custom 自定义
const cutType = key => {
// 恢复默认值
obj.value = Object.assign({}, obj.value, {
"has_vocal": true,
"prompt": "",
"gpt_description_prompt": "",
"mv": "chirp-v3-5",
"tags": "",
})
// console.log(obj.value)
type.value = key
}
let obj = ref({
"gpt_description_prompt": "", // 描述
"mv": "chirp-v3-5",
"make_instrumental": false,
"tags": "", // 标签
"prompt": "", // 提示
"history": null,
"concat_history": null,
"type": "gen",
"duration": null, // 持续时间
"refund_credits": null, // 优惠
"stream": true, // 流派
"infill": null, // 填充物
"has_vocal": true, // 是否有声音
"error_type": null,
"error_message": null,
})
// 点击创建音乐
const creativeMusic = () => {
Generate({ ...obj.value }).then(res => {
if (res.code == 401) {
getLoginQrcode()
return
}
if (res.code != 200) {
ElMessage({
showClose: true,
message: res.message,
type: "error",
})
return
}
const data = res.data || {}
list.value = data.list.concat(list.value)
// list.value = list.value.unshift()
monitorMusicState(data.metadataid)
ElMessage({
showClose: true,
message: "提交成功",
type: "success",
})
})
}
// let styleVisible = ref(true) // 风格弹窗状态
// bass, drum, guitar, rock, pop, electro, electronic, metal, heavy metal, heavy metal, heavy metal, heavy metal
let stylelist = ["bass", "drum", "guitar", "rock", "pop", "electro", "electronic", "metal", "heavy metal", "Subscribe", "heavy metal", "beat", "synth", "hard rock", "rap", "catchy", "powerful", "indie pop"]
const addStyle = item => {
obj.value.tags = obj.value.tags.trim()
if (obj.value.tags) obj.value.tags += ","
obj.value.tags += item
}
const handleMusicList = index => {
let targetlist = list.value || []
let target = targetlist[index]
listIndex.value = index
// console.log(target.metadataid)
if (target.status !== "complete") {
monitorMusicState(target.metadataid || "没有metadataid")
}
getMusicDetails(target.id)
}
let monitorMusicTimer = null
// 监听音乐的生成状态
const monitorMusicState = metadataid => {
clearTimeout(monitorMusicTimer)
monitorMusicTimer = setTimeout(() => {
monitorMusic({ metadataid }).then(res => {
if (res.code == 401) {
getLoginQrcode()
return
}
if (res.code != 200) return
const data = res.data || {}
const dataList = data.list || []
if (data.status != "complete") {
monitorMusicState(metadataid)
}
let obj = {}
// let isnoneed = 0 // isnoneed > 0 需要继续监听的意思 submitted streaming 代表生产中
dataList.forEach((element, index) => {
// if (element.status != "complete") isnoneed++
// 打开详情并且 已经生成成功后刷新详情
if (element.status == "complete" && songInfo.value.sid == element.sid) {
getMusicDetails(element.id)
}
obj[element.sid] = index
})
// if (isnoneed > 0) {
// monitorMusicState(metadataid)
// }
let targetlist = list.value || []
targetlist.forEach((element, index) => {
if (obj[element.sid] >= 0) {
targetlist[index] = dataList[obj[element.sid]]
delete obj[element.sid]
}
})
for (const key in obj) {
targetlist.unshift(dataList[obj[key]])
}
list.value = targetlist
})
}, 10000)
}
let songInfo = ref({}) // 歌详情
const getMusicDetails = id => {
getDetails({ id }).then(res => {
if (res.code == 401) {
getLoginQrcode()
return
}
if (res.code != 200) {
ElMessage({
showClose: true,
message: res.message,
})
return
}
songInfo.value = res.data
previewState.value = true
})
}
const audio = ref(null)
const isPlaying = ref(false)
let audioSrc = ref("")
let previewState = ref(false)
const closeInfo = () => {
// previewState.value = false
songInfo.value = {}
}
const playAudio = index => {
if (index == undefined) {
index = listIndex.value || 0
}
let targetlist = list.value || []
let target = targetlist[index] || {}
const url = target.audio_url || ""
if (audioSrc.value != url) {
audio.value.src = url
currentTime.value = 0
}
audio.value.play()
listIndex.value = index
}
const togglePlayPause = () => {
if (isPlaying.value) {
pauseAudio()
} else {
playAudio()
}
}
const updatePlayStatus = () => {
isPlaying.value = !audio.value.paused
audioSrc.value = audio.value.src || ""
}
const pauseAudio = () => {
audio.value.pause()
}
const nextAudio = () => {
if (listIndex.value == null) return
let targetlist = list.value || []
let index = listIndex.value
index = (index + 1) % targetlist.length
let target = targetlist[index]
audio.value.src = target.audio_url
audio.value.play()
listIndex.value = index
}
const prevAudio = () => {
if (listIndex.value == null) return
let targetlist = list.value || []
let index = listIndex.value
index = (index - 1 + targetlist.length) % targetlist.length
let target = targetlist[index]
audio.value.src = target.audio_url
audio.value.play()
listIndex.value = index
}
const currentTime = ref(0)
const duration = ref(0)
const updateCurrentTime = () => {
// console.log("4454545")
currentTime.value = audio.value.currentTime
}
const updateDuration = () => {
duration.value = audio.value.duration
}
const seekAudio = value => {
if (value >= 0) {
audio.value.currentTime = value
}
// console.log(value)
if (isNaN(value)) {
currentTime.value = 0
// console.log(currentTime.value)
}
}
const formatTime = time => {
if (isNaN(time)) return "0:00"
if (time == Infinity) {
return "-- : --"
}
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`
}
// 生成类似
const generatingSimilar = index => {
let target = list.value[index]
// 判断是显示哪个模式
if (target.metadata.prompt) {
cutType("custom")
} else {
cutType("inspiration")
}
// console.log("target", target)
nextTick(() => {
// console.log("target.model_name", target.model_name)
obj.value = { ...target.metadata, mv: target.model_name, title: target.title }
})
}
// 判断风格是否已经在输入框了
const judgingExistence = item => {
const tags = obj.value?.tags || ""
return !tags.includes(item)
}
// 生成歌词
const lyricsGenerating = () => {
if (lyricsMonitorState.value) return
const prompt = obj.value.prompt || ""
lyricsMonitorState.value = true
lyricsGenerateHttp({ prompt })
.then(res => {
if (res.code == 401) {
getLoginQrcode()
lyricsMonitorState.value = false
return
}
if (res.code != 200) {
ElMessage({
showClose: true,
message: res.message,
type: "error",
})
lyricsMonitorState.value = false
return
}
const data = res.data
// console.log("data", data)
lyricsMonitor(data.id)
})
.catch(() => {
lyricsMonitorState.value = false
})
}
let lyricsMonitorTimer = null
let lyricsMonitorState = ref(false) // 生成状态
// 监听歌词生成
const lyricsMonitor = id => {
setTimeout(() => {
lyricsMonitorHttp({ id })
.then(res => {
if (res.code == 401) {
getLoginQrcode()
return
}
const data = res.data
if (data.status == "complete") {
obj.value.prompt = data.text
obj.value.title = data.title
lyricsMonitorState.value = false
} else {
lyricsMonitor(id)
}
})
.catch(err => {
console.log("err", err)
lyricsMonitor(id)
})
}, 3000)
}
// 退出登录
const logOut = () => {
localStorage.setItem("token", "")
islogin.value = false
quitLoginHttp()
songInfo.value = {}
list.value = []
}
// 登录
const register = () => {
getLoginQrcode()
}
// 点击列表 点赞
const handlelike = index => {
let targetlist = list.value || []
let target = targetlist[index] || {}
likeDetailsHttp({ id: target.id }).then(res => {
if (res.code != 200) {
ElMessage({
showClose: true,
message: res.message,
type: "error",
})
return
}
const data = res.data || {}
list.value[index]["is_upvote"] = data.is_upvote || 0
// 判断 详情预览和点赞的是不是同一首
if (target.sid == songInfo.value.sid) {
songInfo.value["is_upvote"] = data.is_upvote || 0
}
})
// target.is_liked = 1
}
// 切换纯音乐
const switchchange = value => {
// 当切换到纯音乐时去掉歌词
if (!value) {
obj.value.prompt = ""
}
}
</script>
<template>
<div>
<!-- <div class="flexflex" style="height: calc(100vh - 69px); overflow: auto;"> -->
<div class="flexflex">
<div class="left-box">
<img class="left-bj left-bj1" src="./assets/img/left-bj1.png" />
<img class="left-bj left-bj2" src="./assets/img/left-bj2.png" />
<img class="left-bj left-bj3" src="./assets/img/left-bj1.png" />
<img class="hemisphere-icon" src="./assets/img/hemisphere-icon.png" />
<img class="logo" src="./assets/img/logo-icon.png" />
<img class="star" src="./assets/img/star-icon.png" />
<div class="tab-box">
<div class="tab-item" :class="{ 'sel': type == 'inspiration' }" @click="cutType('inspiration')">灵感模式</div>
<div class="tab-item" :class="{ 'sel': type == 'custom' }" @click="cutType('custom')">自定义模式</div>
</div>
<template v-if="type == 'inspiration'">
<div class="description">
<div class="title-box flexacenter">
<div class="title-left flexacenter">
歌曲描述
<el-popover placement="right" :width="300" trigger="click" content='描述你想要的音乐风格和主题(例如"关于假日")。使用流派和氛围,而不是特定的艺术家和歌曲。' popper-class="popper-description">
<template #reference>
<img class="question-icon" src="./assets/img/question-icon.svg" />
</template>
</el-popover>
</div>
<div class="title-right"></div>
</div>
<el-input v-model="obj.gpt_description_prompt" class="description-input" style="width: 100%;" type="textarea" placeholder="请输入您的歌曲描述,例如:一首关于青春的冷酷的电子歌曲" maxlength="200" show-word-limit autosize resize="none" />
</div>
<div class="toggle-switch title-box flexacenter">
<div class="title-left flexacenter">
<el-switch v-model="obj.has_vocal" class="ml-2" style="--el-switch-on-color: #fbd38d; --el-switch-off-color: #ffffff3d;" :active-value="false" :inactive-value="true" />
纯音乐
<el-popover placement="right" :width="150" trigger="click" content="没有歌词的音乐。">
<template #reference>
<img class="question-icon" src="./assets/img/question-icon.svg" />
</template>
</el-popover>
</div>
</div>
<div class="section model">
<div class="title-box flexacenter">
<div class="title-left flexacenter">
音乐模型
<el-popover placement="right" :width="150" trigger="click" content="官方suno音乐模型">
<template #reference>
<img class="question-icon" src="./assets/img/question-icon.svg" />
</template>
</el-popover>
</div>
</div>
<el-select v-model="obj.mv" placeholder="选择模型" size="large" popper-class="model-select" :popper-append-to-body="false" :suffix-icon="triangleIcon">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="creation-btn flexcenter" @click="creativeMusic()">创作</div>
</template>
<template v-else>
<div class="toggle-switch title-box flexacenter">
<div class="title-left flexacenter">
<el-switch v-model="obj.has_vocal" style="--el-switch-on-color: #fbd38d; --el-switch-off-color: #ffffff3d;" :active-value="false" :inactive-value="true" @change="switchchange" />
纯音乐
<el-popover placement="right" :width="150" trigger="click" content="没有歌词的音乐。">
<template #reference>
<img class="question-icon" src="./assets/img/question-icon.svg" />
</template>
</el-popover>
</div>
</div>
<div class="lyric" v-if="obj.has_vocal">
<div class="title-box flexacenter">
<div class="title-left flexacenter">
歌词
<el-popover placement="right" :width="300" trigger="click" content="随机生成歌词自己写或者从Al那里得到一些帮助。使用两首诗8行获得最佳效果。">
<template #reference>
<img class="question-icon" src="./assets/img/question-icon.svg" />
</template>
</el-popover>
</div>
<div class="title-right flexacenter">
<!-- <el-popover popper-class="type-popover" placement="right" :width="280" trigger="click" :teleported="false">
<div class="lyric-type flexflex">
<div class="lyric-type-item flexcenter" v-for="item in 7" :key="item">主歌</div>
</div>
<template #reference>
<div class="meta-tags flexcenter">添加元标签</div>
</template>
</el-popover> -->
</div>
</div>
<div class="lyric-content">
<el-input v-model="obj.prompt" class="lyric-input" maxlength="3000" style="width: 100%;" :rows="4" type="textarea" placeholder="请输入你的歌词" autosize resize="none" />
<div class="random-btn flexcenter" @click="lyricsGenerating()">
<img v-if="lyricsMonitorState" class="loading-icon" src="./assets/img/loading-icon.svg" />
<template v-else>{{ obj.prompt ? "AI优化歌词" : "生成歌词" }}</template>
</div>
<div class="numberwords">{{ obj.prompt.length }}/3000</div>
</div>
</div>
<div class="style">
<div class="title-box flexacenter">
<div class="title-left flexacenter">
音乐风格
<el-popover placement="right" :width="300" trigger="click" content="描述你想要的音乐风格 例如流行、摇滚、史诗。Suno的模型无法识别具体的艺人名称但能理解音乐类型和氛围。">
<template #reference>
<img class="question-icon" src="./assets/img/question-icon.svg" />
</template>
</el-popover>
</div>
</div>
<div class="style-content">
<el-input v-model="obj.tags" class="style-input" maxlength="2000" style="width: 100%;" :rows="4" type="textarea" placeholder="输入音乐风格(英文)" autosize resize="none" />
<!-- <div class="random-btn flexcenter">选择音乐风格</div> -->
<div class="style-list">
<template v-for="item in stylelist">
<div :key="item" class="style-item" v-if="judgingExistence(item)" @click="addStyle(item)">{{ item }}</div>
</template>
</div>
</div>
</div>
<div class="name">
<div class="title-box flexacenter">
<div class="title-left flexacenter">
歌曲名称
<el-popover placement="right" trigger="click" content="为你的歌曲命名">
<template #reference>
<img class="question-icon" src="./assets/img/question-icon.svg" />
</template>
</el-popover>
</div>
</div>
<el-input class="name-input" v-model="obj.title" style="width: 300px;" maxlength="50" placeholder="请输入歌曲名称" />
</div>
<div class="section model">
<div class="title-box flexacenter">
<div class="title-left flexacenter">
音乐模型
<el-popover placement="right" :width="150" trigger="click" content="官方suno音乐模型">
<template #reference>
<img class="question-icon" src="./assets/img/question-icon.svg" />
</template>
</el-popover>
</div>
</div>
<el-select v-model="obj.mv" placeholder="选择模型" size="large" style="width: 100%;" popper-class="model-select" :suffix-icon="triangleIcon">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="creation-btn flexcenter" @click="creativeMusic()">创作</div>
</template>
<!-- <div class="flex1"></div> -->
<!-- <div class="user-box" v-if="islogin"> -->
<div class="user-box" v-if="false">
<img class="user-img" src="https://cdn1.suno.ai/defaultPink.jpg" />
<div class="user-text flex1">微信用户</div>
<el-popover popper-class="user-popover" placement="top" :width="300" trigger="click">
<div class="user-btn flexcenter" @click.stop="logOut()">退出登录</div>
<template #reference>
<div class="user-dot flexcenter">
<img class="user-dot-icon" src="./assets/img/dot.svg" />
</div>
</template>
</el-popover>
</div>
</div>
<div class="middle-box flex1">
<template v-if="list.length == 0">
<div class="nodata">暂无数据</div>
</template>
<template v-else>
<div class="list">
<div class="item flexflex" :class="{ 'sel': listIndex == index, 'playing': audioSrc == item.audio_url }" v-for="(item, index) in list" :key="index" @click="handleMusicList(index)">
<div class="progress-bar" v-if="item.status != 'complete'">生成中...</div>
<div class="img-box" @click="isPlaying && audioSrc == item.audio_url ? pauseAudio() : playAudio(index)">
<img v-if="item.image_url" class="img" :src="item.image_url" />
<img v-else class="img" src="https://suno.ansnid.com/attachment/Zvt57TuJSUvkyhw-xG7avCGHpt5oNTyM1Nwws44-UqaPEyOpLewjBmkLTe6SsJ5o8oxYDW_C7QrwtE0o7mQElMUC3lm6UrpsPr3ey21qvjUeOOPrNv6_QpsEVrOxyJ36niqc_cWN6UYoEk7POhf2DtewNDQyOQ~~" />
<div class="play flexcenter">
<img v-if="audioSrc == item.audio_url && isPlaying" class="suspend-btn" src="./assets/img/suspend.png" />
<img v-else class="play-btn" src="./assets/img/play-icon.png" />
</div>
</div>
<div class="content flexflex flex1">
<div class="name">{{ item.title }}</div>
<div class="desc">{{ item.tags || item?.metadata?.tags }}</div>
</div>
<div class="like-box flexcenter" @click.stop="handlelike(index)">
<img v-if="item.is_upvote == 1" class="like-icon" src="./assets/img/like-c-icon.png" />
<img v-else class="like-icon" src="./assets/img/like-icon.png" />
</div>
<el-popover popper-class="dot-popover" placement="bottom" :width="220" trigger="click" :teleported="false">
<div class="dot-list" @click.stop="">
<!-- <a class="dot-item" target="_parent" v-if="item.audio_url" :href="item.audio_url" :download="item.title + '.mp3'">下载MP3</a> -->
<a class="dot-item" target="_blank" v-if="item.audio_url" :href="item.audio_url">下载MP3</a>
<a class="dot-item" target="_blank" v-if="item.video_url" :href="item.video_url">下载视频</a>
<div class="dot-item" @click="generatingSimilar(index)">生成类似</div>
</div>
<template #reference>
<div class="dot flexcenter" @click.stop="">
<img class="dot-icon" src="./assets/img/dot.svg" />
</div>
</template>
</el-popover>
</div>
</div>
</template>
</div>
<!-- <div class="right-box" v-if="previewState"> -->
<div class="right-box flexflex">
<template v-if="JSON.stringify(songInfo) === '{}'">
<div class="nosong">选择要预览的歌曲</div>
</template>
<template v-else>
<div class="song-info-box">
<div class="close flexcenter" @click="closeInfo()">
<img class="close-icon" src="./assets/img/icon12.svg" />
</div>
<img class="song-picture" :src="songInfo.image_large_url" />
<div class="song-info">
<div class="name">{{ songInfo.title }}</div>
<div class="desc" v-html="songInfo?.metadata?.tags"></div>
<div class="like-box flexcenter" @click.stop="handlelike(listIndex)">
<img v-if="songInfo.is_upvote == 0" class="like-icon" src="./assets/img/like-icon.png" />
<img v-else class="like-icon" src="./assets/img/like-c-icon.png" />
</div>
<div class="desc" v-html="songInfo?.metadata?.prompt"></div>
</div>
</div>
</template>
<div class="register-box flexacenter" v-if="islogin">
<div class="register-text">Welcome</div>
<div class="register-btn" @click.stop="logOut()">[退出登录]</div>
</div>
</div>
</div>
<div class="base-bottom flexflex">
<div class="base-middle flexflex">
<div class="btn flexacenter">
<div class="btn-item btn-left flexcenter" @click="prevAudio()">
<img class="btn-icon" src="./assets/img/arrows-icon.svg" />
</div>
<div class="btn-item btn-centre flexcenter" @click="togglePlayPause()">
<img v-if="isPlaying" class="suspend" src="./assets/img/suspend-icon.svg" />
<img v-else class="suspend" src="./assets/img/play-icon.svg" />
</div>
<div class="btn-item btn-right flexcenter" @click="nextAudio()">
<img class="btn-icon" src="./assets/img/arrows-icon.svg" />
</div>
</div>
<div class="audio-box flexacenter">
<div class="time">{{ formatTime(currentTime) }}</div>
<el-slider class="progress-box" :show-tooltip="false" v-model="currentTime" :min="0" :max="duration" @change="seekAudio" />
<div class="time time-right">{{ formatTime(duration) }}</div>
</div>
<!-- <img class="cyclical-icon" src="./assets/img/loop.png" />
<img class="cyclical-icon" src="./assets/img/single.png" /> -->
</div>
</div>
<el-dialog class="wxlogin" v-model="wxloginVisible" width="480" :before-close="handleClose" align-center="true" :show-close="false">
<div class="wxlogin-box">
<div class="close-box">
<img class="close-icon" src="" />
</div>
<div class="wxlogin-title">微信登录</div>
<div class="qrcode-box flexcenter">
<img class="qrcode-img" :src="qrcode" />
<div class="lose-box flexcenter" v-if="wxloginQrcodeLose" @click="getLoginQrcode()">
二维码失效点击重新获取
</div>
</div>
<div class="hint">扫码关注SunoAI音乐创作工具即可登录</div>
</div>
</el-dialog>
<audio ref="audio" @timeupdate="updateCurrentTime" @loadedmetadata="updateDuration" @ended="nextAudio" @play="updatePlayStatus" @pause="updatePlayStatus"></audio>
</div>
</template>
<style scoped lang="less">
audio {
display: none;
}
body {
background-color: #000;
}
.left-box {
width: 320px;
// height: calc(100vh - 69px);
height: 100vh;
overflow: auto;
padding: 10px;
background-color: #000;
// background-image: url("./assets/img/left-bj.svg");
background: -webkit-linear-gradient(289.179008025811deg, rgba(107, 77, 229, 1) 0%, rgba(144, 91, 212, 1) 38%, rgba(83, 37, 142, 1) 70%, rgba(47, 51, 145, 1) 100%);
background: -moz-linear-gradient(160.820991974189deg, rgba(107, 77, 229, 1) 0%, rgba(144, 91, 212, 1) 38%, rgba(83, 37, 142, 1) 70%, rgba(47, 51, 145, 1) 100%);
background: linear-gradient(160.820991974189deg, rgba(107, 77, 229, 1) 0%, rgba(144, 91, 212, 1) 38%, rgba(83, 37, 142, 1) 70%, rgba(47, 51, 145, 1) 100%);
// border-right: 1px solid #252323;
border-right: 1px solid rgba(255, 255, 255, 0.2);
// height: calc(100vh - 69px);
position: relative;
z-index: 1;
// display: flex;
// flex-direction: column;
.left-bj {
&.left-bj1 {
top: 0;
height: 341px;
}
&.left-bj2 {
top: 341px;
height: 551px;
transform: rotate(180deg);
}
&.left-bj3 {
top: 892px;
}
position: fixed;
left: 0;
width: 319px;
// height: 440px;
z-index: -1;
}
.hemisphere-icon {
position: absolute;
top: 0;
left: 0;
width: 80px;
height: 83px;
}
.logo {
width: 131px;
height: 32px;
margin: 20px auto 44px;
display: block;
}
.star {
z-index: -1;
position: absolute;
bottom: 10px;
left: 0;
width: 154px;
height: 158px;
left: 34px;
bottom: 63px;
}
.user-box {
display: flex;
align-items: center;
// padding: 4px;
padding-left: 10px;
margin-top: 10px;
margin-bottom: 10px;
.user-img {
width: 32px;
height: 32px;
border-radius: 50%;
}
.user-text {
color: #fff;
font-size: 14px;
padding-left: 7px;
}
.user-dot {
padding: 0 12px;
min-width: 32px;
height: 32px;
cursor: pointer;
border-radius: 6px;
.user-dot-icon {
width: 16px;
height: 16px;
}
}
}
.tab-box {
display: flex;
align-items: center;
padding: 0 5px;
height: 46px;
border: 1px solid rgba(255, 255, 255, 0.196078);
background: rgba(255, 255, 255, 0.0980392);
border-radius: 10px;
color: #adadad;
margin-bottom: 24px;
.tab-item {
flex: 1;
cursor: pointer;
text-align: center;
font-size: 18px;
height: 36px;
line-height: 36px;
color: rgba(255, 255, 255, 0.8);
&:first-of-type {
margin-right: 5px;
}
&.sel {
color: #000;
background: -webkit-linear-gradient(344.577838681261deg, rgba(254, 233, 125, 1) 0%, rgba(231, 194, 91, 1) 49%, rgba(254, 140, 125, 1) 100%);
background: -moz-linear-gradient(105.422161318739deg, rgba(254, 233, 125, 1) 0%, rgba(231, 194, 91, 1) 49%, rgba(254, 140, 125, 1) 100%);
background: linear-gradient(105.422161318739deg, rgba(254, 233, 125, 1) 0%, rgba(231, 194, 91, 1) 49%, rgba(254, 140, 125, 1) 100%);
border: none;
border-radius: 10px;
}
}
}
.description {
margin-bottom: 24px;
.title {
margin-bottom: 10px;
}
.description-input {
/deep/ .el-textarea__inner {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.196078) inset !important;
background: rgba(255, 255, 255, 0.0980392);
border-radius: 10px;
color: #fff;
min-height: 180px !important;
// max-height: 300px;
padding: 16px 13px;
}
/deep/ .el-input__count {
font-size: 13px;
color: rgba(255, 255, 255, 0.498039215686275);
background: transparent;
}
// min-height: 31px;
min-height: 94px;
width: 100%;
color: #ffffffeb;
line-height: 1.5;
border-radius: 4px;
}
}
.toggle-switch {
color: #fff;
font-size: 14px;
.el-switch {
margin-right: 10px;
}
margin-bottom: 24px !important;
}
.title-box {
margin-bottom: 10px;
justify-content: space-between;
.title-left {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
.question-icon {
width: 16px;
height: 16px;
margin-left: 10px;
cursor: pointer;
}
/deep/ .el-popper {
font-size: 12px;
color: #fff;
}
}
.title-right {
.meta-tags {
width: 80px;
height: 28px;
color: rgb(204 204 204);
background-color: rgb(26 26 26);
font-size: 12px;
border-radius: 4px;
cursor: pointer;
}
}
}
.model {
margin-bottom: 24px;
.title {
margin-bottom: 10px;
}
/deep/ .el-select__wrapper {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.196078) inset !important;
background: rgba(255, 255, 255, 0.0980392);
height: 40px;
line-height: 40px;
border-radius: 10px;
.el-select__selected-item {
color: #fff;
}
}
// /deep/ .el-popper {
// .el-select-dropdown.model-select {
// .el-select-dropdown__item {
// color: #606266;
// &.is-selected {
// color: #fff;
// }
// &.is-hovering {
// background: #232426 !important;
// }
// }
// }
// }
}
.creation-btn {
padding: 0 16px;
cursor: pointer;
min-height: 44px;
height: 50px;
background: -webkit-linear-gradient(350.537677791974deg, rgba(254, 233, 125, 1) 0%, rgba(231, 194, 91, 1) 49%, rgba(254, 140, 125, 1) 100%);
background: -moz-linear-gradient(99.4623222080256deg, rgba(254, 233, 125, 1) 0%, rgba(231, 194, 91, 1) 49%, rgba(254, 140, 125, 1) 100%);
background: linear-gradient(99.4623222080256deg, rgba(254, 233, 125, 1) 0%, rgba(231, 194, 91, 1) 49%, rgba(254, 140, 125, 1) 100%);
border: none;
border-radius: 10px;
font-weight: 650;
font-size: 24px;
color: #000000;
}
.lyric {
.title-box {
.el-popper.type-popover {
.lyric-type {
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
padding: 10px;
.lyric-type-item {
width: 72px;
height: 24px;
color: rgb(170 170 170);
cursor: pointer;
border-radius: 9999px;
border: 1px solid #aaa;
&:hover {
border-color: #ddd;
}
}
}
}
}
.lyric-content {
/deep/ .lyric-input {
.el-textarea__inner {
background: transparent;
box-shadow: none;
color: #fff;
min-height: 107px !important;
max-height: 300px;
padding: 16px 13px;
}
.el-input__count {
background: transparent;
}
}
padding-bottom: 7px;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.196078) inset !important;
// border: 1px solid rgba(255, 255, 255, 0.196078);
background: rgba(255, 255, 255, 0.0980392);
border-radius: 10px;
color: #adadad;
margin-bottom: 24px;
position: relative;
.numberwords {
position: absolute;
bottom: 6px;
right: 13px;
font-size: 13px;
color: rgba(255, 255, 255, 0.498039215686275);
}
}
}
.style {
.style-content {
/deep/ .style-input {
.el-textarea__inner {
background: transparent;
box-shadow: none;
min-height: 100px !important;
color: #fff;
max-height: 300px;
padding: 16px 13px;
}
}
padding-bottom: 5px;
// box-shadow: 0 0 0 1px #252323 inset !important;
// margin-bottom: 10px;
border: 1px solid rgba(255, 255, 255, 0.196078);
background: rgba(255, 255, 255, 0.0980392);
margin-bottom: 24px;
border-radius: 10px;
.style-list {
width: calc(100% - 12px);
display: flex;
overflow: auto;
white-space: pre;
margin: 0 6px;
box-sizing: border-box;
.style-item {
padding: 0 8px;
font-size: 14px;
color: #000000;
cursor: pointer;
border-radius: 10px;
height: 26px;
line-height: 26px;
background-color: rgba(255, 255, 255, 0.6);
margin-bottom: 8px;
&:not(:last-of-type) {
margin-right: 6px;
}
}
&::-webkit-scrollbar {
width: 13px;
height: 13px;
background-color: rgba(255, 255, 255, 0.6);
border-radius: 10px;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: rgba(255, 255, 255, 1);
}
}
}
}
.random-btn {
// background: rgba(255, 255, 255, 0.16);
// width: 120px;
margin-left: 7px;
cursor: pointer;
// padding: 0 12px;
// min-width: 32px;
// height: 32px;
// border-radius: 6px;
// color: #fff;
// font-size: 12px;
width: 109px;
height: 36px;
background-color: rgba(255, 255, 255, 0.6);
// border: none;
border-radius: 10px;
font-size: 16px;
color: #000000;
.loading-icon {
width: 20px;
height: 20px;
animation: rotate 1s linear infinite;
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
}
.name {
margin-bottom: 24px;
.name-input {
/deep/ .el-input__wrapper {
// background: none;
// box-shadow: 0 0 0 1px #252323 inset !important;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.196078) inset !important;
background: rgba(255, 255, 255, 0.0980392);
height: 40px;
border-radius: 10px;
}
/deep/ .el-input__inner {
color: #fff;
}
}
}
}
/deep/ .el-popper {
background: rgb(25, 25, 26) !important;
border: none !important;
box-shadow: 0 0 0 1px #252323 inset !important;
&.is-light .el-popper__arrow:before {
border-color: #252323 !important;
background: rgb(25, 25, 26) !important;
}
}
.right-box {
width: 294px;
height: 100vh;
overflow: auto;
// height: calc(100vh - 69px);
overflow: auto;
position: relative;
flex-direction: column;
justify-content: space-between;
.song-info-box {
height: calc(100vh - 40px);
overflow: auto;
}
.nosong {
color: #a0aec0;
font-size: 14px;
text-align: center;
padding-top: 40px;
}
.close {
position: absolute;
top: 10px;
right: 10px;
background-color: #fff3;
width: 40px;
height: 40px;
border-radius: 5px;
cursor: pointer;
}
.song-picture {
width: 100%;
}
.song-info {
font-size: 14px;
padding: 10px;
color: #fff;
overflow: auto;
.name {
line-height: 24px;
font-size: 14px;
word-wrap: break-word;
}
.desc {
white-space: pre-line;
line-height: 24px;
// margin-bottom: 40px;
word-wrap: break-word;
}
.like-box {
padding: 0 10px;
width: fit-content;
height: 32px;
cursor: pointer;
margin-bottom: 20px;
&:hover {
border-radius: 5px;
background: rgba(255, 255, 255, 0.08);
}
.like-icon {
width: 17px;
}
}
}
.register-box {
justify-content: space-between;
align-items: center;
height: 40px;
padding: 0 15px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(51, 51, 51, 1);
.register-text {
font-weight: 650;
font-size: 20px;
color: #8080ff;
}
.register-btn {
font-size: 13px;
cursor: pointer;
color: rgba(255, 255, 255, 0.498039215686275);
}
}
}
.middle-box {
border-right: 1px solid rgba(255, 255, 255, 0.2);
// height: calc(100vh - 69px);
height: 100vh;
overflow: auto;
padding-top: 10px;
padding-bottom: 120px;
.nodata {
color: #a0aec0;
font-size: 14px;
text-align: center;
padding-top: 40px;
}
.list {
.item {
padding-left: 30px;
padding-right: 30px;
margin-bottom: 10px;
cursor: pointer;
align-items: center;
&.sel,
&:hover {
background: radial-gradient(3010.06% 152.11% at 27.33% 38.98%, rgba(61, 58, 58, 0.43) 0%, rgba(25, 24, 24, 0.57) 100%) !important;
}
&.playing {
.img-box {
.play {
display: flex;
}
}
.content {
.name {
color: #e7c25b;
}
}
}
.img-box {
position: relative;
width: 80px;
height: 80px;
margin-right: 20px;
border-radius: 6px;
.img {
width: 80px;
height: 80px;
border-radius: 6px;
}
&:hover {
.play {
display: flex;
}
}
.suspend-box,
.play {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
.play-btn {
width: 36px;
}
.suspend-btn {
width: 46px;
}
}
.play {
display: none;
}
}
position: relative;
.progress-bar {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
color: #fff;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(270deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0.8));
background-size: 300% 100%;
animation: gradientAnimation 5s linear infinite;
}
@keyframes gradientAnimation {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.content {
font-size: 14px;
line-height: 21px;
flex-direction: column;
justify-content: center;
margin-right: 10px;
.name {
font-weight: 700;
color: #fff;
word-break: break-word;
font-size: 18px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 6px;
}
.desc {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
word-break: break-all;
word-wrap: break-word;
white-space: normal;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
}
}
.like-box {
padding: 0 10px;
height: 32px;
cursor: pointer;
&:hover {
border-radius: 5px;
background: rgba(255, 255, 255, 0.08);
}
.like-icon {
width: 17px;
}
}
.dot {
padding: 0 10px;
min-width: 32px;
height: 32px;
cursor: pointer;
&:hover {
border-radius: 5px;
background: rgba(255, 255, 255, 0.08);
}
.dot-icon {
widows: 16px;
height: 16px;
}
}
/deep/ .dot-popover {
padding: 0;
.dot-list {
padding: 5px 0;
.dot-item {
padding-left: 10px;
padding-right: 10px;
line-height: 30px;
cursor: pointer;
height: 30px;
font-size: 14px;
color: #fff;
display: block;
text-decoration: none;
&:hover {
background: radial-gradient(3010.06% 152.11% at 27.33% 38.98%, rgba(61, 58, 58, 0.43) 0%, rgba(25, 24, 24, 0.57) 100%);
}
}
}
}
}
}
}
.base-bottom {
background-color: rgb(22 22 22);
border: 1px solid rgba(255, 255, 255, 0.19);
// border-top: 1px solid #222222;
padding-left: 24px;
padding-right: 24px;
// padding-top: 24px;
height: 68px;
display: flex;
justify-content: center;
// align-items: center;
position: fixed;
bottom: 10px;
width: 540px;
height: 96px;
border-radius: 10px;
z-index: 10;
left: 50%;
transform: translateX(-50%);
.base-middle {
flex-direction: column;
align-items: center;
padding-top: 18px;
margin-bottom: 6px;
.btn {
// margin-right: 24px;
.btn-item {
width: 36px;
height: 36px;
background: inherit;
background-color: rgba(255, 255, 255, 0.8);
border: none;
border-radius: 18px;
cursor: pointer;
transition: all 0.3s;
// &:hover {
// .el-icon {
// font-size: 30px;
// }
// }
.btn-icon {
width: 15px;
height: 16px;
}
&.btn-left {
.btn-icon {
transform: rotate(180deg);
}
}
&.btn-centre {
margin-left: 20px;
margin-right: 20px;
// background-color: #fbd38d;
background: transparent;
img {
width: 36px;
height: 36px;
// width: 20px;
}
}
}
}
.audio-box {
position: relative;
margin-left: 16px;
margin-right: 16px;
.time {
top: -17px;
left: 0;
position: absolute;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
&.time-right {
right: 0;
left: auto;
}
}
.progress-box {
width: 500px;
/deep/ .el-slider__runway {
height: 6px;
background-color: rgba(255, 255, 255, 0.803921568627451);
.el-slider__bar {
background: rgba(128, 128, 255, 1);
height: 6px;
}
}
/deep/ .el-slider__button {
border: none;
height: 16px;
width: 16px;
background: rgba(128, 128, 255, 1);
}
}
}
.cyclical-icon {
height: 14px;
margin-right: 5px;
margin: 0 12px;
cursor: pointer;
}
}
}
/deep/ .el-dialog {
border-radius: 10px;
color: #fff !important;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
background: rgb(25, 25, 26) !important;
padding: 16px;
.el-dialog__header {
display: none;
}
&.wxlogin {
.el-dialog__body {
.wxlogin-box {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.close-box {
width: 0;
height: 0;
position: absolute;
top: 15px;
right: 15px;
}
.wxlogin-title {
color: #969595;
padding: 0 20px;
height: 40px;
margin-bottom: 10px;
line-height: 40px;
font-size: 18px;
}
.qrcode-box {
width: 300px;
height: 300px;
position: relative;
.qrcode-img {
width: 100%;
height: 100%;
}
.lose-box {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
}
}
.hint {
margin-top: 20px;
margin-bottom: 20px;
color: rgb(204 204 204);
text-align: center;
}
.remark {
text-align: center;
font-size: 12px;
color: rgb(180 185 191);
a {
color: rgb(88 101 242);
text-decoration: underline;
}
}
}
}
}
}
</style>
<style lang="less">
.el-popper {
background: rgb(25, 25, 26) !important;
border: none !important;
box-shadow: 0 0 0 1px #252323 inset !important;
font-size: 12px !important;
color: #fff !important;
&.is-light .el-popper__arrow:before {
border-color: #252323 !important;
background: rgb(25, 25, 26) !important;
border-left-color: transparent !important;
border-top-color: transparent !important;
}
}
.el-popper.model-select {
.el-select-dropdown__item {
color: #606266;
&.is-selected {
color: #fff;
}
&.is-hovering {
background: #232426 !important;
}
}
}
.user-popover {
.user-btn {
width: 100%;
height: 40px;
border-radius: 8px;
cursor: pointer;
margin-top: 12px;
margin-bottom: 8px;
border: 1px solid rgba(255, 255, 255, 0.16);
color: #fff;
}
}
.el-switch {
.el-switch__core {
height: 24px;
min-width: 48px;
border-radius: 48px;
.el-switch__action {
width: 18px;
height: 18px;
}
}
&.is-checked .el-switch__core {
background: -webkit-linear-gradient(350.53767779deg, #fee97d 0%, #e7c25b 49%, #fe8c7d 100%);
background: -moz-linear-gradient(99.46232221deg, #fee97d 0%, #e7c25b 49%, #fe8c7d 100%);
background: linear-gradient(99.46232221deg, #fee97d 0%, #e7c25b 49%, #fe8c7d 100%);
border: none;
.el-switch__action {
left: calc(100% - 21px);
}
}
}
</style>