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))
+    }
+  }
+})