commit fcdd9fd33c51fc66223a4fc5dfd2bef04a6da752 Author: A1300399510 <A1300399510> Date: Wed Jul 17 16:32:57 2024 +0800 no message diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ee54e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5cbacd --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# suno + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Compile and Minify for Production + +```sh +npm run build +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..99f583a --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <link rel="icon" href="/favicon.ico"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Vite App</title> + </head> + <body> + <div id="app"></div> + <script type="module" src="/src/main.js"></script> + </body> +</html> diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..5a1f2d2 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + }, + "exclude": ["node_modules", "dist"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4585735 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "suno", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.7.2", + "element-plus": "^2.7.7", + "qs": "^6.12.3", + "vue": "^3.4.29", + "vue-router": "^4.3.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.5", + "less": "^4.2.0", + "vite": "^5.3.1" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..881fe3c --- /dev/null +++ b/src/App.vue @@ -0,0 +1,1165 @@ +<script setup> +import { RouterLink, RouterView } from "vue-router" +import { onMounted, ref } from "vue" + +import { getQrcode, getList, monitorState, Generate, monitorMusic, getDetails } from "./utils/api" +import { ElMessage } from "element-plus" + +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 || [] + + data.forEach(element => { + console.log("element", element.metadataid) + }) + + 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 => { + console.log(res) + // 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 = () => { + console.log("挂壁了") + wxloginVisible.value = false + wxloginQrcodeLose.value = false + clearInterval(monitorTimer) +} + +let textarea = ref("") +let value2 = ref(false) +let modelValue = ref("chirp-v3-5") +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 => { + type.value = key +} + +let obj = ref({ + "gpt_description_prompt": "", // 描述 + "mv": "chirp-v3-5", + "make_instrumental": false, + "tags": null, // 标签 + "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, + "tags": "", +}) + +// 点击创建音乐 +const creativeMusic = () => { + console.log("提交了") + + // return + + Generate({ ...obj.value }).then(res => { + if (res.code != 200) { + ElMessage({ + showClose: true, + message: res.message, + type: "error", + }) + return + } + const data = res.data || {} + + monitorMusicState(data.metadataid) + ElMessage({ + showClose: true, + message: res.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 + if (target.status !== "complete") { + monitorMusicState(target.metadataid) + } else { + getMusicDetails(target.id) + } +} + +let monitorMusicTimer = null +// 监听音乐的生成状态 +const monitorMusicState = metadataid => { + clearTimeout(monitorMusicTimer) + monitorMusicTimer = setTimeout(() => { + monitorMusic({ metadataid }).then(res => { + if (res.code != 200) return + const data = res.data || {} + const dataList = data.list || [] + + let obj = {} + let isnoneed = 0 // isnoneed > 0 需要继续监听的意思 submitted streaming 代表生产中 + dataList.forEach((element, index) => { + if (element.status != "complete") isnoneed++ + obj[element.sid] = index + }) + + console.log("isnoneed", isnoneed) + + 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 + }) + }, 1000) +} + +let songInfo = ref({}) // 歌详情 + +const getMusicDetails = id => { + getDetails({ id }).then(res => { + if (res.code != 200) return + + songInfo.value = res.data + + previewState.value = true + }) +} + +const audio = ref(null) +const isPlaying = ref(false) + +let previewState = ref(false) +const closeInfo = () => { + previewState.value = false +} + +const playAudio = index => { + if (index == undefined) { + index = listIndex.value || 0 + } + let targetlist = list.value || [] + let target = targetlist[index] || {} + audio.value.src = target.audio_url || "" + audio.value.play() + listIndex.value = index +} + +const togglePlayPause = () => { + if (isPlaying.value) { + pauseAudio() + } else { + playAudio() + } +} + +const updatePlayStatus = () => { + isPlaying.value = !audio.value.paused +} + +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 + console.log(audio.value) +} + +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 = () => { + currentTime.value = audio.value.currentTime +} + +const updateDuration = () => { + duration.value = audio.value.duration +} + +const seekAudio = value => { + audio.value.currentTime = value +} + +const formatTime = time => { + const minutes = Math.floor(time / 60) + const seconds = Math.floor(time % 60) + return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}` +} +</script> + +<template> + <div> + <div class="flexflex" style="height: calc(100vh - 69px); overflow: auto;"> + <div class="left-box"> + <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='描述你想要的音乐风格和主题(例如"关于假日")。使用流派和氛围,而不是特定的艺术家和歌曲。' :teleported="false"> + <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%;" :rows="4" type="textarea" placeholder="请输入您的歌曲描述,例如:一首关于青春的冷酷的电子歌曲" /> + </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="没有歌词的音乐。" :teleported="false"> + <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音乐模型" :teleported="false"> + <template #reference> + <img class="question-icon" src="./assets/img/question-icon.svg" /> + </template> + </el-popover> + </div> + </div> + + <el-select v-model="modelValue" placeholder="选择模型" size="large" style="width: 100%;" popper-class="model-select" :teleported="false"> + <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" /> + 纯音乐 + <el-popover placement="right" :width="150" trigger="click" content="没有歌词的音乐。" :teleported="false"> + <template #reference> + <img class="question-icon" src="./assets/img/question-icon.svg" /> + </template> + </el-popover> + </div> + </div> + + <div class="lyric"> + <div class="title-box flexacenter"> + <div class="title-left flexacenter"> + 歌词 + <el-popover placement="right" :width="300" trigger="click" content="随机生成歌词,自己写,或者从Al那里得到一些帮助。使用两首诗(8行)获得最佳效果。" :teleported="false"> + <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.gpt_description_prompt" class="lyric-input" maxlength="2000" style="width: 100%;" :rows="4" type="textarea" placeholder="请输入你的歌词" /> + <!-- <div class="random-btn flexcenter">随机生成歌词</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的模型无法识别具体的艺人名称,但能理解音乐类型和氛围。" :teleported="false"> + <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 /> + <!-- <div class="random-btn flexcenter">选择音乐风格</div> --> + <div class="style-list"> + <div class="style-item" v-for="item in stylelist" :key="item" @click="addStyle(item)">{{ item }}</div> + </div> + </div> + </div> + <div class="name"> + <div class="title-box flexacenter"> + <div class="title-left flexacenter"> + 歌曲名称 + <el-popover placement="right" :width="300" trigger="click" content="为你的歌曲命名" :teleported="false"> + <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="40" 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音乐模型" :teleported="false"> + <template #reference> + <img class="question-icon" src="./assets/img/question-icon.svg" /> + </template> + </el-popover> + </div> + </div> + + <el-select v-model="modelValue" placeholder="选择模型" size="large" style="width: 100%;" popper-class="model-select" :teleported="false"> + <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> + <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 }" v-for="(item, index) in list" :key="index" @click="handleMusicList(index)"> + <!-- <div class="progress-bar" :style="{ 'width': item.progress + '%' }"></div> --> + <div class="progress-bar" v-if="item.status != 'complete'">生成中...</div> + <div class="img-box" @click="playAudio(index)"> + <img class="img" :src="item.image_url" /> + <div class="play flexcenter"> + <img 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> + + <el-popover popper-class="dot-popover" placement="bottom" :width="220" trigger="click" :teleported="false"> + <div class="dot-list"> + <a class="dot-item" v-if="item.audio_url" :href="item.audio_url" :download="item.title + '.mp3'">下载MP3</a> + <!-- <div class="dot-item" v-if="item.audio_url">下载MP3</div> --> + <!-- <div class="dot-item">下载wav</div> --> + <a class="dot-item" v-if="item.video_url" :href="item.video_url" :download="item.title + '.mp4'">下载视频</a> + <!-- <div class="dot-item" v-if="item.video_url">下载视频</div> --> + </div> + <template #reference> + <div class="dot flexcenter"> + <img class="dot-icon" src="./assets/img/dot.svg" /> + </div> + </template> + </el-popover> + </div> + </div> + </template> + </div> + <div class="right-box" v-if="previewState"> + <template v-if="JSON.stringify(songInfo) === '{}'"> + <div class="nosong">选择要预览的歌曲。</div> + </template> + <template v-else> + <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">{{ songInfo?.metadata?.tags }}</div> + <div class="desc" v-html="songInfo?.metadata?.prompt"></div> + </div> + </template> + </div> + </div> + + <div class="base-bottom flexacenter"> + <div class="base-side"></div> + <div class="base-middle flexacenter"> + <div class="btn flexacenter"> + <div class="btn-item btn-left flexcenter" @click="prevAudio()"> + <el-icon color="#fff" :size="18"><DArrowLeft /></el-icon> + </div> + <div class="btn-item btn-centre flexcenter" @click="togglePlayPause()"> + <img v-if="isPlaying" class="suspend" src="./assets/img/suspend.png" /> + <img v-else class="suspend" src="./assets/img/play-icon.png" /> + </div> + <div class="btn-item btn-right flexcenter" @click="nextAudio()"> + <el-icon color="#fff" :size="18"><DArrowRight /></el-icon> + </div> + </div> + <div class="audio-box flexacenter"> + <div class="time">{{ formatTime(currentTime) }}</div> + <el-slider class="progress-box" :show-tooltip="false" v-model="currentTime" :max="duration" @change="seekAudio" /> + <div class="time">{{ formatTime(duration) }}</div> + </div> + <!-- <img class="cyclical-icon" src="./assets/img/loop.png" /> + <img class="cyclical-icon" src="./assets/img/single.png" /> --> + </div> + <div class="base-side"></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 class="remark flexacenter"> + 注册/登录表示您已同意 + <a>《用户协议》</a> + 及 + <a>《隐私政策》</a> + </div> --> + </div> + </el-dialog> + <!-- 风格 --> + <!-- <el-dialog class="style-dialog" v-model="styleVisible" width="480" :before-close="handleClose" align-center="true" :show-close="false"> + <div class="style-box"> + <div class="title">音乐流派</div> + <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick"> + <el-tab-pane label="User" name="first">User</el-tab-pane> + <el-tab-pane label="Config" name="second">Config</el-tab-pane> + <el-tab-pane label="Role" name="third">Role</el-tab-pane> + <el-tab-pane label="Task" name="fourth">Task</el-tab-pane> + </el-tabs> + </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); + // overflow: auto; + padding: 10px; + background-color: #000; + border-right: 1px solid #252323; + height: calc(100vh - 69px); + .tab-box { + display: flex; + align-items: center; + padding: 4px; + border: 1px solid #adadad; + border-radius: 23px; + color: #adadad; + margin-bottom: 18px; + + .tab-item { + flex: 1; + height: 34px; + cursor: pointer; + text-align: center; + line-height: 34px; + &.sel { + color: #000; + border-radius: 20px; + background-color: #fbd38d; + } + } + } + + .description { + margin-bottom: 10px; + .title { + margin-bottom: 10px; + } + + .description-input { + /deep/ .el-textarea__inner { + box-shadow: 0 0 0 1px #252323 inset !important; + background: transparent; + } + min-height: 31px; + 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: 10px; + } + + .title-box { + margin-bottom: 10px; + justify-content: space-between; + + .title-left { + font-size: 14px; + color: #fff; + .question-icon { + width: 14px; + height: 14px; + margin-left: 3px; + 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 { + .title { + margin-bottom: 10px; + } + /deep/ .el-select__wrapper { + box-shadow: 0 0 0 1px #252323 inset !important; + background: transparent; + + .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 { + color: #1a202c; + font-size: 16px; + font-weight: 700; + padding: 0 16px; + background-color: #fbd38d; + height: 44px; + border-radius: 4px; + margin-top: 24px; + cursor: pointer; + } + + .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; + } + } + padding-bottom: 5px; + box-shadow: 0 0 0 1px #252323 inset !important; + margin-bottom: 10px; + } + } + + .style { + .style-content { + /deep/ .style-input { + .el-textarea__inner { + background: transparent; + box-shadow: none; + min-height: 100px !important; + } + } + padding-bottom: 5px; + box-shadow: 0 0 0 1px #252323 inset !important; + margin-bottom: 10px; + + .style-list { + width: 100%; + display: flex; + overflow: auto; + white-space: pre; + + .style-item { + color: rgb(250 247 245); + font-size: 13px; + padding: 8px 12px; + background: #363030bf; + margin-left: 4px; + border-radius: 9999px; + cursor: pointer; + } + } + } + } + + .random-btn { + background: rgba(255, 255, 255, 0.16); + width: 120px; + margin-left: 5px; + cursor: pointer; + padding: 0 12px; + min-width: 32px; + height: 32px; + border-radius: 6px; + color: #fff; + font-size: 12px; + } + + .name { + margin-bottom: 10px; + + .name-input { + /deep/ .el-input__wrapper { + background: none; + box-shadow: 0 0 0 1px #252323 inset !important; + } + } + } +} + +/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: calc(100vh - 69px); + overflow: auto; + position: relative; + .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; + + .name { + line-height: 24px; + font-size: 14px; + } + .desc { + white-space: pre-line; + line-height: 24px; + margin-bottom: 40px; + } + } +} + +.middle-box { + border-right: 1px solid #252323; + height: calc(100vh - 69px); + overflow: auto; + .nodata { + color: #a0aec0; + font-size: 14px; + text-align: center; + padding-top: 40px; + } + + .list { + .item { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 20px; + padding-right: 20px; + 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; + } + + .img-box { + position: relative; + width: 60px; + height: 60px; + margin-right: 10px; + border-radius: 6px; + + .img { + width: 60px; + height: 60px; + border-radius: 6px; + } + &:hover { + .play { + display: flex; + } + } + .play { + display: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + .play-btn { + width: 22px; + } + } + } + + 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; + .name { + font-weight: 700; + color: #fff; + } + .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: rgb(203 213 224); + } + } + + .dot { + padding: 0 12px; + min-width: 32px; + height: 32px; + cursor: pointer; + border-radius: 6px; + + .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-top: 1px solid #222222; + padding-left: 24px; + padding-right: 24px; + height: 68px; + .base-side { + width: 450px; + } + + .base-middle { + .btn { + margin-right: 24px; + .btn-item { + width: 32px; + height: 32px; + border-radius: 50% !important; + background: rgba(255, 255, 255, 0.08); + cursor: pointer; + transition: all 0.3s; + &:hover { + .el-icon { + font-size: 30px; + } + } + + &.btn-centre { + margin-left: 6px; + margin-right: 6px; + background-color: #fbd38d; + img { + width: 20px; + } + } + } + } + + .audio-box { + .time { + color: rgb(160 174 192); + font-size: 12px; + } + + .progress-box { + width: 390px; + margin-left: 16px; + margin-right: 16px; + /deep/ .el-slider__runway { + height: 4px; + .el-slider__bar { + background: #fbd38d; + height: 4px; + } + } + /deep/ .el-slider__button { + border: none; + height: 14px; + width: 14px; + } + } + } + .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-dialog { + .el-dialog__body { + .style-box { + .title { + font-size: 18px; + color: #fff; + } + } + } + } +} +</style> diff --git a/src/assets/base.css b/src/assets/base.css new file mode 100644 index 0000000..e69de29 diff --git a/src/assets/img/dot.svg b/src/assets/img/dot.svg new file mode 100644 index 0000000..936dc81 --- /dev/null +++ b/src/assets/img/dot.svg @@ -0,0 +1 @@ +<svg data-v-98294f16="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="#fff" d="M176 416a112 112 0 1 1 0 224 112 112 0 0 1 0-224m336 0a112 112 0 1 1 0 224 112 112 0 0 1 0-224m336 0a112 112 0 1 1 0 224 112 112 0 0 1 0-224"></path></svg> \ No newline at end of file diff --git a/src/assets/img/icon12.svg b/src/assets/img/icon12.svg new file mode 100644 index 0000000..79d6b89 --- /dev/null +++ b/src/assets/img/icon12.svg @@ -0,0 +1 @@ +<svg stroke="#fff" fill="#fff" stroke-width="0" viewBox="0 0 352 512" data-theme="dark" focusable="false" class="chakra-icon css-13otjrl" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> \ No newline at end of file diff --git a/src/assets/img/loop-c.png b/src/assets/img/loop-c.png new file mode 100644 index 0000000..1da24f2 Binary files /dev/null and b/src/assets/img/loop-c.png differ diff --git a/src/assets/img/loop.png b/src/assets/img/loop.png new file mode 100644 index 0000000..64a347d Binary files /dev/null and b/src/assets/img/loop.png differ diff --git a/src/assets/img/play-icon.png b/src/assets/img/play-icon.png new file mode 100644 index 0000000..af87ea7 Binary files /dev/null and b/src/assets/img/play-icon.png differ diff --git a/src/assets/img/question-icon.svg b/src/assets/img/question-icon.svg new file mode 100644 index 0000000..ef010d4 --- /dev/null +++ b/src/assets/img/question-icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="#fff" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m23.744 191.488c-52.096 0-92.928 14.784-123.2 44.352-30.976 29.568-45.76 70.4-45.76 122.496h80.256c0-29.568 5.632-52.8 17.6-68.992 13.376-19.712 35.2-28.864 66.176-28.864 23.936 0 42.944 6.336 56.32 19.712 12.672 13.376 19.712 31.68 19.712 54.912 0 17.6-6.336 34.496-19.008 49.984l-8.448 9.856c-45.76 40.832-73.216 70.4-82.368 89.408-9.856 19.008-14.08 42.24-14.08 68.992v9.856h80.96v-9.856c0-16.896 3.52-31.68 10.56-45.76 6.336-12.672 15.488-24.64 28.16-35.2 33.792-29.568 54.208-48.576 60.544-55.616 16.896-22.528 26.048-51.392 26.048-86.592 0-42.944-14.08-76.736-42.24-101.376-28.16-25.344-65.472-37.312-111.232-37.312zm-12.672 406.208a54.272 54.272 0 0 0-38.72 14.784 49.408 49.408 0 0 0-15.488 38.016c0 15.488 4.928 28.16 15.488 38.016A54.848 54.848 0 0 0 523.072 768c15.488 0 28.16-4.928 38.72-14.784a51.52 51.52 0 0 0 16.192-38.72 51.968 51.968 0 0 0-15.488-38.016 55.936 55.936 0 0 0-39.424-14.784z"></path></svg> \ No newline at end of file diff --git a/src/assets/img/single-c.png b/src/assets/img/single-c.png new file mode 100644 index 0000000..6fa0fec Binary files /dev/null and b/src/assets/img/single-c.png differ diff --git a/src/assets/img/single.png b/src/assets/img/single.png new file mode 100644 index 0000000..1c8207e Binary files /dev/null and b/src/assets/img/single.png differ diff --git a/src/assets/img/suspend.png b/src/assets/img/suspend.png new file mode 100644 index 0000000..f339f4f Binary files /dev/null and b/src/assets/img/suspend.png differ diff --git a/src/assets/main.css b/src/assets/main.css new file mode 100644 index 0000000..eead539 --- /dev/null +++ b/src/assets/main.css @@ -0,0 +1,70 @@ +/* @import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} */ + + +* { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +.flexflex { + display: flex; +} + +.flexcenter { + display: flex; + justify-content: center; + align-items: center; +} + +.flexjcenter { + display: flex; + justify-content: center; +} + +.flexacenter { + display: flex; + align-items: center; +} + +.flex1 { + flex: 1; +} + +body { + background-color: #000; +} \ No newline at end of file diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue new file mode 100644 index 0000000..5fb372c --- /dev/null +++ b/src/components/HelloWorld.vue @@ -0,0 +1,44 @@ +<script setup> +defineProps({ + msg: { + type: String, + required: true + } +}) +</script> + +<template> + <div class="greetings"> + <h1 class="green">{{ msg }}</h1> + <h3> + You’ve successfully created a project with + <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> + + <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. + </h3> + </div> +</template> + +<style scoped> +h1 { + font-weight: 500; + font-size: 2.6rem; + position: relative; + top: -10px; +} + +h3 { + font-size: 1.2rem; +} + +.greetings h1, +.greetings h3 { + text-align: center; +} + +@media (min-width: 1024px) { + .greetings h1, + .greetings h3 { + text-align: left; + } +} +</style> diff --git a/src/components/TheWelcome.vue b/src/components/TheWelcome.vue new file mode 100644 index 0000000..dab9536 --- /dev/null +++ b/src/components/TheWelcome.vue @@ -0,0 +1,88 @@ +<script setup> +import WelcomeItem from './WelcomeItem.vue' +import DocumentationIcon from './icons/IconDocumentation.vue' +import ToolingIcon from './icons/IconTooling.vue' +import EcosystemIcon from './icons/IconEcosystem.vue' +import CommunityIcon from './icons/IconCommunity.vue' +import SupportIcon from './icons/IconSupport.vue' +</script> + +<template> + <WelcomeItem> + <template #icon> + <DocumentationIcon /> + </template> + <template #heading>Documentation</template> + + Vue’s + <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a> + provides you with all information you need to get started. + </WelcomeItem> + + <WelcomeItem> + <template #icon> + <ToolingIcon /> + </template> + <template #heading>Tooling</template> + + This project is served and bundled with + <a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The + recommended IDE setup is + <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> + + <a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If + you need to test your components and web pages, check out + <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and + <a href="https://on.cypress.io/component" target="_blank" rel="noopener" + >Cypress Component Testing</a + >. + + <br /> + + More instructions are available in <code>README.md</code>. + </WelcomeItem> + + <WelcomeItem> + <template #icon> + <EcosystemIcon /> + </template> + <template #heading>Ecosystem</template> + + Get official tools and libraries for your project: + <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>, + <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>, + <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and + <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If + you need more resources, we suggest paying + <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a> + a visit. + </WelcomeItem> + + <WelcomeItem> + <template #icon> + <CommunityIcon /> + </template> + <template #heading>Community</template> + + Got stuck? Ask your question on + <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official + Discord server, or + <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener" + >StackOverflow</a + >. You should also subscribe to + <a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow + the official + <a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a> + twitter account for latest news in the Vue world. + </WelcomeItem> + + <WelcomeItem> + <template #icon> + <SupportIcon /> + </template> + <template #heading>Support Vue</template> + + As an independent project, Vue relies on community backing for its sustainability. You can help + us by + <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>. + </WelcomeItem> +</template> diff --git a/src/components/WelcomeItem.vue b/src/components/WelcomeItem.vue new file mode 100644 index 0000000..ac366d0 --- /dev/null +++ b/src/components/WelcomeItem.vue @@ -0,0 +1,86 @@ +<template> + <div class="item"> + <i> + <slot name="icon"></slot> + </i> + <div class="details"> + <h3> + <slot name="heading"></slot> + </h3> + <slot></slot> + </div> + </div> +</template> + +<style scoped> +.item { + margin-top: 2rem; + display: flex; + position: relative; +} + +.details { + flex: 1; + margin-left: 1rem; +} + +i { + display: flex; + place-items: center; + place-content: center; + width: 32px; + height: 32px; + color: var(--color-text); +} + +h3 { + font-size: 1.2rem; + font-weight: 500; + margin-bottom: 0.4rem; + color: var(--color-heading); +} + +@media (min-width: 1024px) { + .item { + margin-top: 0; + padding: 0.4rem 0 1rem calc(var(--section-gap) / 2); + } + + i { + top: calc(50% - 25px); + left: -26px; + position: absolute; + border: 1px solid var(--color-border); + background: var(--color-background); + border-radius: 8px; + width: 50px; + height: 50px; + } + + .item:before { + content: ' '; + border-left: 1px solid var(--color-border); + position: absolute; + left: 0; + bottom: calc(50% + 25px); + height: calc(50% - 25px); + } + + .item:after { + content: ' '; + border-left: 1px solid var(--color-border); + position: absolute; + left: 0; + top: calc(50% + 25px); + height: calc(50% - 25px); + } + + .item:first-of-type:before { + display: none; + } + + .item:last-of-type:after { + display: none; + } +} +</style> diff --git a/src/components/icons/IconCommunity.vue b/src/components/icons/IconCommunity.vue new file mode 100644 index 0000000..2dc8b05 --- /dev/null +++ b/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ +<template> + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor"> + <path + d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z" + /> + </svg> +</template> diff --git a/src/components/icons/IconDocumentation.vue b/src/components/icons/IconDocumentation.vue new file mode 100644 index 0000000..6d4791c --- /dev/null +++ b/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ +<template> + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor"> + <path + d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z" + /> + </svg> +</template> diff --git a/src/components/icons/IconEcosystem.vue b/src/components/icons/IconEcosystem.vue new file mode 100644 index 0000000..c3a4f07 --- /dev/null +++ b/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ +<template> + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor"> + <path + d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z" + /> + </svg> +</template> diff --git a/src/components/icons/IconSupport.vue b/src/components/icons/IconSupport.vue new file mode 100644 index 0000000..7452834 --- /dev/null +++ b/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ +<template> + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor"> + <path + d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z" + /> + </svg> +</template> diff --git a/src/components/icons/IconTooling.vue b/src/components/icons/IconTooling.vue new file mode 100644 index 0000000..660598d --- /dev/null +++ b/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ +<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license--> +<template> + <svg + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + aria-hidden="true" + role="img" + class="iconify iconify--mdi" + width="24" + height="24" + preserveAspectRatio="xMidYMid meet" + viewBox="0 0 24 24" + > + <path + d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z" + fill="currentColor" + ></path> + </svg> +</template> diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..65521f6 --- /dev/null +++ b/src/main.js @@ -0,0 +1,20 @@ +import "./assets/main.css" + +import { createApp } from "vue" +import ElementPlus from "element-plus" +import "element-plus/dist/index.css" +// 如果您正在使用CDN引入,请删除下面一行。 +import * as ElementPlusIconsVue from "@element-plus/icons-vue" + +import App from "./App.vue" +import router from "./router" + +const app = createApp(App) +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} +app.use(ElementPlus) + +app.use(router) + +app.mount("#app") diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..a49ae50 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '../views/HomeView.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + component: HomeView + }, + { + path: '/about', + name: 'about', + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import('../views/AboutView.vue') + } + ] +}) + +export default router diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 0000000..750b3ac --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,32 @@ +import Http from "@/utils/http" + +// 获取二维码 +export const getQrcode = params => { + return Http.post("/api/login/qrcode", params) +} + +// 监听扫码登录 状态 +export const monitorState = params => { + return Http.post("/api/login/monitor", params) +} + +// 获取列表 +export const getList = params => { + return Http.post("/api/lists", params) +} + +// 点击创建音乐 +export const Generate = params => { + return Http.post("/api/music/generate", params) +} + +// 监听音乐生成状态 +export const monitorMusic = params => { + return Http.post("/api/music/monitor", params) +} +// 获取音乐详情 +export const getDetails = params => { + return Http.post("/api/details", params) +} + + diff --git a/src/utils/http.js b/src/utils/http.js new file mode 100644 index 0000000..ef8d946 --- /dev/null +++ b/src/utils/http.js @@ -0,0 +1,78 @@ +import axios from 'axios'; +import QS from 'qs'; +import { ElMessage } from "element-plus"; + + +axios.defaults.baseURL = 'https://suno.ansnid.com' +axios.defaults.emulateJSON = true +axios.defaults.withCredentials = true + +axios.interceptors.request.use( //响应拦截 + async config => { + // 开发时登录用的,可以直接替换小程序的 authorization + const token = localStorage.getItem('token') || '' + config['headers']['token'] = process.env.NODE_ENV !== "production" && token + config['headers']['Content-Type'] = "application/json;charset=utf-8" + return config; + }, + error => { + return Promise.error(error); + }) +// 响应拦截器 +axios.interceptors.response.use(response => { + if (response.status === 200) return Promise.resolve(response); //进行中 + else return Promise.reject(response); //失败 +}, error => { // 服务器状态码不是200的情况 + if (error.response.status) { + switch (error.response.status) { + // 401: 未登录 + case 401: + // goTologin() // 跳转登录页面 + break + default: + } + return Promise.reject(error.response); + } +}); + +/** + * get方法,对应get请求 + * @param {String} url [请求的url地址] + * @param {Object} params [请求时携带的参数] + */ +const get = (url, params) => { + return new Promise((resolve, reject) => { + axios.get(url, { params }).then(res => resolve(res.data)).catch(err => reject(err.data)) + }); +} +/** + * post方法,对应post请求 + * @param {String} url [请求的url地址] + * @param {Object} params [请求时携带的参数] + */ +const post = (url, params) => { + return new Promise((resolve, reject) => { + //是将对象 序列化成URL的形式,以&进行拼接 + // axios.post(url, QS.stringify(params)).then(res => { + axios.post(url, params).then(res => { + let data = res.data + if (data.code == 401 && !process.server) goLogin() + resolve(data) + }).catch(err => { + if (err.data.code == 401) { + goLogin() + resolve(err.data); + } else reject(err.data) + }) + }); +} + +// 打开登录 +const goLogin = () => { + if (typeof ajax_login === "function") ajax_login() +} + +export default { + get, + post, +} \ No newline at end of file diff --git a/src/views/AboutView.vue b/src/views/AboutView.vue new file mode 100644 index 0000000..756ad2a --- /dev/null +++ b/src/views/AboutView.vue @@ -0,0 +1,15 @@ +<template> + <div class="about"> + <h1>This is an about page</h1> + </div> +</template> + +<style> +@media (min-width: 1024px) { + .about { + min-height: 100vh; + display: flex; + align-items: center; + } +} +</style> diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue new file mode 100644 index 0000000..6bb706f --- /dev/null +++ b/src/views/HomeView.vue @@ -0,0 +1,9 @@ +<script setup> +import TheWelcome from '../components/TheWelcome.vue' +</script> + +<template> + <main> + <TheWelcome /> + </main> +</template> diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..5c45e1d --- /dev/null +++ b/vite.config.js @@ -0,0 +1,16 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +})