Compare commits

..

No commits in common. 'main' and 'pyw' have entirely different histories.
main ... pyw

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,23 @@
{
"name": "music_administrate",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "1.2.1",
"echarts": "^5.5.1",
"element-plus": "^2.8.6",
"form-data": "^4.0.1",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.10"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,10 @@
<template>
<router-view></router-view>
</template>
<script>
</script>
<style scoped>
</style>

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732697791833" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7020" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M192 789.333333a21.24 21.24 0 0 1-12.8-4.28 344.513333 344.513333 0 0 1-99.333333-118A341.246667 341.246667 0 0 1 384 170.666667h256q6.36 0 12.733333 0.233333l-49.153333-49.146667a21.333333 21.333333 0 0 1 30.173333-30.173333l85.333334 85.333333a21.333333 21.333333 0 0 1 0 30.173334l-85.333334 85.333333a21.333333 21.333333 0 0 1-30.173333-30.173333l48.666667-48.666667Q646.126667 213.333333 640 213.333333H384c-164.666667 0-298.666667 134-298.666667 298.666667 0 94.833333 43.546667 181.933333 119.48 238.966667A21.333333 21.333333 0 0 1 192 789.333333z m228.433333 143.06a21.333333 21.333333 0 0 0 0-30.173333l-49.153333-49.146667q6.366667 0.233333 12.733333 0.233334H640a341.46 341.46 0 0 0 304.146667-496.42 344.513333 344.513333 0 0 0-99.333334-118 21.333333 21.333333 0 1 0-25.626666 34.113333C895.12 330.066667 938.666667 417.166667 938.666667 512c0 164.666667-134 298.666667-298.666667 298.666667H384q-6.12 0-12.246667-0.246667l48.666667-48.666667a21.333333 21.333333 0 0 0-30.173333-30.173333l-85.333334 85.333333a21.333333 21.333333 0 0 0 0 30.173334l85.333334 85.333333a21.333333 21.333333 0 0 0 30.173333 0z" fill="#5C5C66" p-id="7021"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732698243829" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18121" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M507.136 514.944c3.072-4.362667 7.210667-7.925333 12.032-10.314667a37.365333 37.365333 0 0 1 46.634667 6.144l97.418666 102.794667a32 32 0 0 1-46.442666 44.021333L565.333333 603.317333V853.333333a32 32 0 1 1-64 0V608.757333l-52.618666 50.016a32 32 0 0 1-44.096-46.4l102.517333-97.429333zM512 138.666667c123.018667 0 228.213333 85.696 259.424 204.469333C864.298667 344.736 938.666667 422.752 938.666667 518.218667 938.666667 614.688 862.752 693.333333 768.533333 693.333333a32 32 0 0 1 0-64C826.890667 629.333333 874.666667 579.84 874.666667 518.218667c0-61.610667-47.776-111.104-106.133334-111.104-5.856 0-11.626667 0.490667-17.301333 1.461333a32 32 0 0 1-37.024-26.666667C698.346667 279.04 612.714667 202.666667 512 202.666667c-73.834667 0-140.928 41.066667-177.376 106.613333a32 32 0 0 1-30.122667 16.373333c-3.168-0.213333-6.357333-0.32-9.568-0.32C214.784 325.333333 149.333333 393.141333 149.333333 477.333333S214.784 629.333333 294.933333 629.333333a32 32 0 1 1 0 64C178.912 693.333333 85.333333 596.373333 85.333333 477.333333c0-116.938667 90.293333-212.554667 203.456-215.904C338.090667 185.696 421.013333 138.666667 512 138.666667z" fill="#000000" p-id="18122"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732698154940" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17096" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M32 407.584a279.584 279.584 0 0 1 480-194.944 279.584 279.584 0 0 1 480 194.944 278.144 278.144 0 0 1-113.024 224.512L562.592 892.8a96 96 0 0 1-124.416-1.952l-308.16-270.688A278.976 278.976 0 0 1 32 407.584z" fill="#000000" p-id="17097"></path></svg>

After

Width:  |  Height:  |  Size: 584 B

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732697928205" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12247" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M198.4 473.6l332.8-332.8c6.4-6.4 25.6 0 25.6 12.8l0 256 262.4-262.4c6.4-6.4 25.6 0 25.6 12.8l0 729.6c0 12.8-12.8 19.2-25.6 12.8L556.8 627.2l0 256c0 12.8-12.8 19.2-25.6 12.8L198.4 550.4C172.8 531.2 172.8 492.8 198.4 473.6z" p-id="12248"></path></svg>

After

Width:  |  Height:  |  Size: 583 B

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732698273579" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19133" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M187.392 70.656q28.672 0 48.64 19.456t19.968 48.128l0 52.224q0 28.672-19.968 48.64t-48.64 19.968l-54.272 0q-27.648 0-47.616-19.968t-19.968-48.64l0-52.224q0-28.672 19.968-48.128t47.616-19.456l54.272 0zM889.856 70.656q27.648 0 47.616 19.456t19.968 48.128l0 52.224q0 28.672-19.968 48.64t-47.616 19.968l-437.248 0q-28.672 0-48.64-19.968t-19.968-48.64l0-52.224q0-28.672 19.968-48.128t48.64-19.456l437.248 0zM187.392 389.12q28.672 0 48.64 19.968t19.968 48.64l0 52.224q0 27.648-19.968 47.616t-48.64 19.968l-54.272 0q-27.648 0-47.616-19.968t-19.968-47.616l0-52.224q0-28.672 19.968-48.64t47.616-19.968l54.272 0zM889.856 389.12q27.648 0 47.616 19.968t19.968 48.64l0 52.224q0 27.648-19.968 47.616t-47.616 19.968l-437.248 0q-28.672 0-48.64-19.968t-19.968-47.616l0-52.224q0-28.672 19.968-48.64t48.64-19.968l437.248 0zM187.392 708.608q28.672 0 48.64 19.968t19.968 47.616l0 52.224q0 28.672-19.968 48.64t-48.64 19.968l-54.272 0q-27.648 0-47.616-19.968t-19.968-48.64l0-52.224q0-27.648 19.968-47.616t47.616-19.968l54.272 0zM889.856 708.608q27.648 0 47.616 19.968t19.968 47.616l0 52.224q0 28.672-19.968 48.64t-47.616 19.968l-437.248 0q-28.672 0-48.64-19.968t-19.968-48.64l0-52.224q0-27.648 19.968-47.616t48.64-19.968l437.248 0z" p-id="19134"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732699888921" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5063" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M838.4 473.6 505.6 134.4C499.2 128 480 134.4 480 147.2l0 256L217.6 134.4C211.2 128 192 134.4 192 147.2l0 729.6c0 12.8 12.8 19.2 25.6 12.8l262.4-262.4 0 256c0 12.8 12.8 19.2 25.6 12.8l339.2-339.2C864 531.2 864 492.8 838.4 473.6z" p-id="5064"></path></svg>

After

Width:  |  Height:  |  Size: 587 B

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732698078664" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16099" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M227.556 312.889v-28.445h-85.334c-47.457 0-85.333 38.326-85.333 85.603v283.906c0 47.583 38.205 85.603 85.333 85.603h85.334V312.889z m56.888 415.289L512 910.222V113.778L284.444 295.822v432.356z m-56.888-45.511v56.889l254.317 211.93c47.86 39.885 87.016 21.699 87.016-40.984V113.498c0-62.718-38.958-81.033-87.016-40.985L227.556 284.444v56.89h-56.89c-31.637 0-56.888 25.234-56.888 56.364v228.604c0 30.71 25.47 56.365 56.889 56.365h56.889z m455.113-384.419l241.359-241.36 40.226 40.227-241.359 241.36-40.226-40.227z m0 424.645l241.359 241.36 40.226-40.227-241.359-241.36-40.226 40.227z m-0.002-267.782H967.11V512H682.667v-56.889z" fill="#333333" p-id="16100"></path></svg>

After

Width:  |  Height:  |  Size: 1001 B

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1732697988479" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13232" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M309.3 130.7h-70.9c-24.3 0-44 19.7-44 44v674.5c0 24.3 19.7 44 44 44h70.9c24.3 0 44-19.7 44-44V174.7c0-24.3-19.7-44-44-44z m476.3 0h-70.9c-24.3 0-44 19.7-44 44v674.5c0 24.3 19.7 44 44 44h70.9c24.3 0 44-19.7 44-44V174.7c0-24.3-19.7-44-44-44z" p-id="13233"></path></svg>

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

@ -0,0 +1,126 @@
<template>
<div class="carousel">
<div
class="carousel-container"
:style="{ transform: `translateX(-${currentSlide * 100}%)` }"
>
<div class="carousel-slide" v-for="(image, index) in images" :key="index">
<img :src="image" :alt="'Slide ' + (index + 1)" />
</div>
</div>
<!-- Controls -->
<div class="carousel-controls">
<button @click="prevSlide"></button>
<button @click="nextSlide"></button>
</div>
<!-- Dots -->
<div class="carousel-dots">
<span
v-for="(image, index) in images"
:key="index"
:class="{ active: index === currentSlide }"
@click="goToSlide(index)"
></span>
</div>
</div>
</template>
<script>
export default {
name: "CarouselSection",
data() {
return {
currentSlide: 0,
images: [
new URL("../../assets/img/slide1.jpg", import.meta.url).href,
new URL("../../assets/img/slide2.jpg", import.meta.url).href,
new URL("../../assets/img/slide3.jpg", import.meta.url).href,
],
};
},
methods: {
nextSlide() {
this.currentSlide =
(this.currentSlide + 1) % this.images.length; //
},
prevSlide() {
this.currentSlide =
(this.currentSlide - 1 + this.images.length) % this.images.length; //
},
goToSlide(index) {
this.currentSlide = index;
},
},
};
</script>
<style scoped>
.carousel {
position: relative;
width: 100%;
max-width: 800px;
margin: 0 auto;
overflow: hidden;
border-radius: 10px;
}
.carousel-container {
display: flex;
transition: transform 0.5s ease-in-out;
}
.carousel-slide {
min-width: 100%;
box-sizing: border-box;
}
.carousel-slide img {
width: 100%;
height: auto;
display: block;
}
.carousel-controls {
position: absolute;
top: 50%;
width: 100%;
display: flex;
justify-content: space-between;
transform: translateY(-50%);
}
.carousel-controls button {
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
border: none;
padding: 10px;
cursor: pointer;
border-radius: 50%;
}
.carousel-controls button:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.carousel-dots {
position: absolute;
bottom: 15px;
width: 100%;
text-align: center;
}
.carousel-dots span {
display: inline-block;
width: 10px;
height: 10px;
margin: 0 5px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
cursor: pointer;
}
.carousel-dots span.active {
background-color: #fff;
}
</style>

@ -0,0 +1,438 @@
<template>
<!-- 最外层的大盒子 -->
<div class="bigBox">
<div class="box" ref="box">
<!-- 滑动盒子 -->
<div class="pre-box">
<h1>WELCOME</h1>
<p>JOIN US!</p>
<div class="img-box">
<img src="../assets/img/waoku.jpg" alt="" id="avatar" />
</div>
</div>
<!-- 注册盒子 -->
<div class="register-form">
<!-- 标题盒子 -->
<div class="title-box">
<h1>注册</h1>
</div>
<!-- 输入框盒子 -->
<el-form
ref="registerFormRef"
:model="registerForm"
:rules="rules"
label-with="5px"
>
<el-form-item prop="username" label=" ">
<el-input
type="text"
placeholder="用户名"
:suffix-icon="User"
v-model="registerForm.username"
/>
</el-form-item>
<el-form-item prop="password" label=" ">
<el-input
type="password"
placeholder="密码"
:suffix-icon="Lock"
v-model="registerForm.password"
/>
</el-form-item>
<el-form-item prop="confirmPassword" label=" ">
<el-input
type="password"
placeholder="确认密码"
:suffix-icon="Lock"
v-model="registerForm.confirmPassword"
/>
</el-form-item>
</el-form>
<!-- 按钮盒子 -->
<div class="btn-box">
<button @click="register"></button>
<!-- 绑定点击事件 -->
<p @click="mySwitch">?</p>
</div>
</div>
<!-- 登录盒子 -->
<div class="login-form">
<!-- 标题盒子 -->
<div class="title-box">
<h1>登录</h1>
</div>
<!-- 输入框盒子 -->
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="rules"
label-with="5px"
>
<el-form-item prop="username" label=" ">
<el-input
type="text"
placeholder="用户名"
:suffix-icon="User"
v-model="loginForm.username"
/>
</el-form-item>
<el-form-item prop="password" label=" ">
<el-input
type="password"
placeholder="密码"
:suffix-icon="Lock"
v-model="loginForm.password"
/>
</el-form-item>
</el-form>
<!-- 按钮盒子 -->
<div class="btn-box">
<button @click="login"></button>
<!-- 绑定点击事件 -->
<p @click="mySwitch">?</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { Lock, User } from '@element-plus/icons-vue'
import mySwitch from '../utils/mySwitch'
import { reactive, ref } from 'vue'
import axios from 'axios';
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { onMounted } from '@vue/runtime-core'
const router = useRouter();
//
const loginForm = reactive({
username: '',
password: '',
});
//
const registerForm = reactive({
username: '',
password: '',
confirmPassword: '',
});
//
const loginRules = reactive({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 16, message: '用户名长度应在3~16个字符之间', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' },
],
});
const registerRules = reactive({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 16, message: '用户名长度应在3~16个字符之间', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' },
],
confirmPassword: [
{
required: true,
message: '请确认密码',
trigger: 'blur'
},
{
validator: (rule, value) => {
if (value !== registerForm.password) {
return new Error('两次密码输入不一致');
}
return true;
},
trigger: 'blur',
},
],
});
//
const loginFormRef = ref(null);
const registerFormRef = ref(null);
//
const toggleForm = () => {
const box = document.querySelector('.box');
box.classList.toggle('active');
};
//
const login = () => {
loginFormRef.value.validate(valid => {
if (valid) {
axios.post('http://localhost:8081/api/users/login', {
username: loginForm.username,
password: loginForm.password,
})
.then(response => {
ElMessage.success('登录成功');
console.log(response.data); //
//
const userRole = response.data.role;
// userRole
console.log('User role:', userRole);
localStorage.setItem('isLoggedIn', JSON.stringify(true));
console.log(localStorage.getItem('isLoggedIn'));
localStorage.setItem('user',JSON.stringify(response.data)); //
//
if (userRole === 'admin') {
router.push({ name: 'adminindex' }); //
// this.$router.push({
// path:'/admin',
// query:{}
// })
} else {
router.push({ name: 'webindex' }); //
}
})
.catch(error => {
ElMessage.error('用户名或密码错误');
});
}
});
};
//
const register = () => {
registerFormRef.value.validate((valid) => {
if (valid) {
// POST
axios.post('http://localhost:8081/api/users/register', {
username: registerForm.username,
password: registerForm.password,
confirmPassword: registerForm.confirmPassword, //
}, {
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
ElMessage.success('注册成功,请登录');
toggleForm();
})
.catch(error => {
console.log(error.response.data); //
ElMessage.error('注册失败,请重试');
});
} else {
ElMessage.error('注册失败,请检查输入');
}
});
};
</script>
<style >
/* 去除input的轮廓 */
input {
outline: none;
}
.bigBox {
width: 100%; /* 占满整个宽度 */
height: 100vh;
overflow-x: hidden;
display: flex;
justify-content: center; /* 内容居中 */
align-items: center; /* 垂直居中 */
background: linear-gradient(to right, rgb(247, 209, 215), rgb(191, 227, 241));
}
/* 最外层的大盒子 */
.box {
max-width: 1200px; /* 根据需要调整宽度 */
width: 90%; /* 动态适应屏幕宽度 */
height: 600px;
display: flex;
position: relative;
margin: auto;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow: 2px 1px 19px rgba(0, 0, 0, 0.1);
}
/* 滑动的盒子 */
.pre-box {
/* 宽度为大盒子的一半 */
width: 50%;
height: 100%;
/* 绝对定位 */
position: absolute;
/* 距离大盒子左侧为0 */
left: 0;
/* 距离大盒子顶部为0 */
top: 0;
z-index: 99;
border-radius: 4px;
background-color: #edd4dc;
box-shadow: 2px 1px 19px rgba(0, 0, 0, 0.1);
/* 动画过渡,先加速再减速 */
transition: 0.5s ease-in-out;
}
/* 滑动盒子的标题 */
.pre-box h1 {
margin-top: 150px;
text-align: center;
/* 文字间距 */
letter-spacing: 5px;
color: white;
/* 禁止选中 */
user-select: none;
/* 文字阴影 */
text-shadow: 4px 4px 3px rgba(0, 0, 0, 0.1);
}
/* 滑动盒子的文字 */
.pre-box p {
height: 30px;
line-height: 30px;
text-align: center;
margin: 20px 0;
/* 禁止选中 */
user-select: none;
font-weight: bold;
color: white;
text-shadow: 4px 4px 3px rgba(0, 0, 0, 0.1);
}
/* 图片盒子 */
.img-box {
width: 200px;
height: 200px;
margin: 20px auto;
/* 设置为圆形 */
border-radius: 50%;
/* 设置用户禁止选中 */
user-select: none;
overflow: hidden;
box-shadow: 4px 4px 3px rgba(0, 0, 0, 0.1);
}
/* 图片 */
.img-box img {
width: 100%;
transition: 0.5s;
}
/* 登录和注册盒子 */
.login-form,
.register-form {
flex: 1;
height: 100%;
}
/* 标题盒子 */
.title-box {
height: 300px;
line-height: 500px;
}
/* 标题 */
.title-box h1 {
text-align: center;
color: white;
/* 禁止选中 */
user-select: none;
letter-spacing: 5px;
text-shadow: 4px 4px 3px rgba(0, 0, 0, 0.1);
}
/* 输入框盒子 */
.el-form {
display: flex;
/* 纵向布局 */
flex-direction: column;
/* 水平居中 */
align-items: center;
}
.el-form-item {
width: 65%;
}
/* 输入框 */
input {
/* width: 60%; */
height: 40px;
margin-bottom: 20px;
text-indent: 10px;
border: 1px solid #fff;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 120px;
/* 增加磨砂质感 */
backdrop-filter: blur(10px);
}
input:focus {
/* 光标颜色 */
color: #b0cfe9;
}
/* 聚焦时隐藏文字 */
input:focus::placeholder {
opacity: 0;
}
/* 按钮盒子 */
.btn-box {
display: flex;
justify-content: center;
}
/* 按钮 */
button {
width: 100px;
height: 30px;
margin: 0 7px;
line-height: 30px;
border: none;
border-radius: 4px;
background-color: #69b3f0;
color: white;
}
/* 按钮悬停时 */
button:hover {
/* 鼠标小手 */
cursor: pointer;
/* 透明度 */
opacity: 0.8;
}
/* 按钮文字 */
.btn-box p {
height: 30px;
line-height: 30px;
/* 禁止选中 */
user-select: none;
font-size: 14px;
color: white;
}
.btn-box p:hover {
cursor: pointer;
border-bottom: 1px solid white;
}
</style>

@ -0,0 +1,9 @@
<template>
<h1>PlaylistProfile</h1>
</template>
<script>
</script>
<style>
</style>

@ -0,0 +1,147 @@
<template>
<section class="playlist-page">
<!-- 分类部分 -->
<div class="playlist-categories">
<span
v-for="category in categories"
:key="category"
@click="filterByCategory(category)"
:class="{ active: selectedCategory === category }"
>
{{ category }}
</span>
</div>
<!-- 歌单展示 -->
<div class="playlist">
<div
class="playlist-item"
v-for="item in filteredPlaylist"
:key="item.id"
@click="openPlaylistDetail(item)"
>
<img :src="item.cover" :alt="item.title" />
<p>{{ item.title }}</p>
</div>
</div>
</section>
</template>
<script>
// import PlaylistDetailPage from "../view/web/PlaylistDetailPage.vue"
import PlayListProfile from './PlayListProfile.vue';
export default {
name: "PlaylistPage",
data() {
return {
//
playlist: [
{ id: 1, cover: "https://via.placeholder.com/150", title: "歌单标题1", category: "全部歌单" },
{ id: 2, cover: "https://via.placeholder.com/150", title: "歌单标题2", category: "华语" },
{ id: 3, cover: "https://via.placeholder.com/150", title: "歌单标题3", category: "欧美" },
{ id: 4, cover: "https://via.placeholder.com/150", title: "歌单标题4", category: "日韩" },
{ id: 5, cover: "https://via.placeholder.com/150", title: "歌单标题5", category: "轻音乐" },
{ id: 6, cover: "https://via.placeholder.com/150", title: "歌单标题6", category: "BGM" },
{ id: 7, cover: "https://via.placeholder.com/150", title: "歌单标题7", category: "乐器" },
{ id: 8, cover: "https://via.placeholder.com/150", title: "歌单标题8", category: "粤语" },
],
//
categories: ["全部歌单", "华语", "粤语", "欧美", "日韩", "轻音乐", "BGM", "乐器"],
//
selectedCategory: "全部歌单",
};
},
computed: {
//
filteredPlaylist() {
if (this.selectedCategory === "全部歌单") {
return this.playlist;
}
return this.playlist.filter((item) => item.category === this.selectedCategory);
},
},
methods: {
// filterByCategory
filterByCategory(category) {
this.selectedCategory = category;
},
openPlaylistDetail(playlist) {
this.$emit("switchPage", "PlayListProfile", playlist); //
},
}
};
</script>
<style scoped>
/* 页面样式 */
.playlist-page {
padding: 20px;
background-color: #f8f9fa;
}
/* 分类样式 */
.playlist-categories {
margin-bottom: 20px;
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.playlist-categories span {
cursor: pointer;
font-size: 16px;
padding: 5px 10px;
transition: color 0.3s ease, font-weight 0.3s ease;
}
.playlist-categories span.active {
font-weight: bold;
color: #007bff; /* 选中时加深颜色 */
}
/* 歌单容器样式 */
.playlist {
display: flex; /* 使用 Flex 布局 */
flex-wrap: wrap; /* 子项换行 */
gap: 16px; /* 设置间距 */
justify-content: space-between; /* 子项均匀分布 */
padding: 10px; /* 添加内边距 */
box-sizing: border-box; /* 包含内外边距 */
}
/* 单个歌单项样式 */
.playlist-item {
flex: 1 1 calc(25% - 16px); /* 每项占25%的宽度,减去间距 */
max-width: calc(25% - 50px); /* 限制最大宽度 */
box-sizing: border-box; /* 确保内外边距计算正确 */
background-color: #fff; /* 设置背景色 */
border-radius: 8px; /* 圆角效果 */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 添加阴影 */
text-align: center; /* 居中对齐 */
overflow: hidden; /* 防止内容溢出 */
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease;
}
/* 悬停效果 */
.playlist-item:hover {
transform: scale(1.05); /* 放大效果 */
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); /* 增强阴影效果 */
background-color: #f0f8ff; /* 更改背景颜色 */
}
/* 图片样式 */
.playlist-item img {
width: 150px; /* 图片宽度 */
height: auto; /* 保持图片比例 */
border-radius: 8px; /* 圆角效果 */
margin-bottom: 8px; /* 图片和文字之间的间距 */
transition: transform 0.3s ease; /* 添加平滑缩放效果 */
}
/* 图片悬停缩放 */
.playlist-item:hover img {
transform: scale(1.1); /* 图片放大 */
}
</style>

@ -0,0 +1,112 @@
<template>
<section class="singer-page">
<!-- 分类部分 -->
<div class="singer-categories">
<span
v-for="category in categories"
:key="category"
@click="filterByCategory(category)"
:class="{ active: selectedCategory === category }"
>
{{ category }}
</span>
</div>
<!-- 歌手展示 -->
<div class="singer">
<div class="singer-item" v-for="singer in filteredSingers" :key="singer.id">
<img :src="singer.avatar" :alt="singer.name" />
<p>{{ singer.name }}</p>
</div>
</div>
</section>
</template>
<script>
export default {
name: "SingerPage",
data() {
return {
//
singers: [
{ id: 1, avatar: "https://via.placeholder.com/100", name: "歌手1", gender: "男歌手" },
{ id: 2, avatar: "https://via.placeholder.com/100", name: "歌手2", gender: "女歌手" },
{ id: 3, avatar: "https://via.placeholder.com/100", name: "歌手3", gender: "男歌手" },
{ id: 4, avatar: "https://via.placeholder.com/100", name: "歌手4", gender: "组合歌手" },
],
//
categories: ["全部歌手", "男歌手", "女歌手", "组合歌手"],
//
selectedCategory: "全部歌手",
};
},
computed: {
//
filteredSingers() {
if (this.selectedCategory === "全部歌手") {
return this.singers;
}
return this.singers.filter((singer) => singer.gender === this.selectedCategory);
},
},
methods: {
//
filterByCategory(category) {
this.selectedCategory = category;
this.$emit('changePage', 'SingerProfile'); // currentPage
},
},
};
</script>
<style scoped>
/* 页面样式 */
.singer-page {
padding: 20px;
background-color: #f8f9fa;
}
/* 分类样式 */
.singer-categories {
margin-bottom: 20px;
display: flex;
gap: 15px;
}
.singer-categories span {
cursor: pointer;
font-size: 16px;
padding: 5px 10px;
transition: all 0.3s ease;
}
.singer-categories span.active {
font-weight: bold;
color: #007bff; /* 选中时加深颜色 */
}
/* 歌手展示样式 */
.singer {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.singer-item {
flex: 1 1 calc(25% - 20px);
max-width: calc(25% - 20px);
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 10px;
text-align: center;
}
.singer-item img {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
margin-bottom: 10px;
}
</style>

@ -0,0 +1,9 @@
<template>
<h1>Singerprofile</h1>
</template>
<script>
</script>
<style>
</style>

@ -0,0 +1,217 @@
<template>
<div class="system-setting">
<h2>系统设置</h2>
<!-- 个人资料 -->
<section class="setting-section">
<h3>个人资料</h3>
<form class="profile-form">
<div class="form-item">
<label for="username">用户名</label>
<input type="text" id="username" v-model="profile.username" placeholder="请输入用户名" />
</div>
<div class="form-item">
<label for="email">邮箱</label>
<input type="email" id="email" v-model="profile.email" placeholder="请输入邮箱" />
</div>
<div class="form-item">
<label for="avatar">头像</label>
<input type="file" id="avatar" @change="handleAvatarChange" />
</div>
<button type="button" class="btn-save" @click="saveProfile"></button>
</form>
</section>
<!-- 更改密码 -->
<section class="setting-section">
<h3>更改密码</h3>
<form class="password-form">
<div class="form-item">
<label for="current-password">当前密码</label>
<input type="password" id="current-password" v-model="password.current" placeholder="请输入当前密码" />
</div>
<div class="form-item">
<label for="new-password">新密码</label>
<input type="password" id="new-password" v-model="password.new" placeholder="请输入新密码" />
</div>
<div class="form-item">
<label for="confirm-password">确认密码</label>
<input type="password" id="confirm-password" v-model="password.confirm" placeholder="再次输入新密码" />
</div>
<button type="button" class="btn-save" @click="changePassword"></button>
</form>
</section>
<!-- 账号与安全 -->
<section class="setting-section">
<h3>账号与安全</h3>
<div class="security-item">
<p>绑定手机</p>
<span>{{ security.phone }}</span>
<button @click="editPhone"></button>
</div>
<div class="security-item">
<p>两步验证</p>
<span>{{ security.twoFactor ? '已启用' : '未启用' }}</span>
<button @click="toggleTwoFactor">{{ security.twoFactor ? '' : '' }}</button>
</div>
</section>
</div>
</template>
<script>
export default {
name: "SystemSetting",
data() {
return {
//
profile: {
username: "音乐达人",
email: "example@mail.com",
avatar: null,
},
//
password: {
current: "",
new: "",
confirm: "",
},
//
security: {
phone: "未绑定",
twoFactor: false,
},
};
},
methods: {
saveProfile() {
//
alert("个人资料已保存!");
},
handleAvatarChange(event) {
const file = event.target.files[0];
if (file) {
this.profile.avatar = URL.createObjectURL(file);
alert("头像已更换!");
}
},
changePassword() {
//
if (this.password.new !== this.password.confirm) {
alert("两次密码输入不一致!");
return;
}
alert("密码已修改!");
},
editPhone() {
alert("跳转到手机绑定页面!");
},
toggleTwoFactor() {
this.security.twoFactor = !this.security.twoFactor;
alert(this.security.twoFactor ? "两步验证已启用!" : "两步验证已关闭!");
},
},
};
</script>
<style scoped>
.system-setting {
padding: 20px;
background-color: #fff;
font-family: Arial, sans-serif;
}
/* 标题样式 */
h2 {
margin-bottom: 20px;
font-size: 24px;
color: #333;
}
.setting-section {
margin-bottom: 30px;
}
h3 {
margin-bottom: 15px;
font-size: 20px;
color: #1db954;
}
/* 表单样式 */
form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-item {
display: flex;
flex-direction: column;
gap: 5px;
}
label {
font-size: 14px;
color: #555;
}
input {
padding: 8px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn-save {
padding: 10px;
font-size: 16px;
color: #fff;
background-color: #1db954;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-save:hover {
background-color: #149740;
}
/* 账号与安全样式 */
.security-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
}
.security-item p {
margin: 0;
font-size: 14px;
color: #333;
}
.security-item span {
font-size: 14px;
color: #555;
}
.security-item button {
padding: 5px 10px;
font-size: 14px;
color: #fff;
background-color: #1db954;
border: none;
border-radius: 4px;
cursor: pointer;
}
.security-item button:hover {
background-color: #149740;
}
</style>

@ -0,0 +1,190 @@
<template>
<div class="user-profile">
<!-- 用户信息 -->
<div class="profile-header">
<img class="avatar" src="../../assets/img/waoku.jpg" alt="用户头像" />
<div class="user-info">
<h2 class="username">音乐达人</h2>
<p class="bio">热爱音乐分享快乐 🎵</p>
</div>
</div>
<!-- 用户统计信息 -->
<div class="user-stats">
<div class="stat">
<span class="stat-number">120</span>
<span class="stat-label">喜欢的歌曲</span>
</div>
<div class="stat">
<span class="stat-number">15</span>
<span class="stat-label">创建的歌单</span>
</div>
<div class="stat">
<span class="stat-number">300</span>
<span class="stat-label">播放次数</span>
</div>
</div>
<!-- 用户的歌单 -->
<div class="user-playlists">
<h3>我的歌单</h3>
<ul class="playlist-list">
<li class="playlist-item" v-for="(playlist, index) in playlists" :key="index">
<img class="playlist-cover" :src="playlist.cover" :alt="playlist.name" />
<div class="playlist-info">
<p class="playlist-name">{{ playlist.name }}</p>
<p class="playlist-songs">{{ playlist.songs }} 首歌曲</p>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "UserProfile",
data() {
return {
playlists: [
{
name: "轻音乐之旅",
songs: 25,
cover: "../../assets/img/playlist1.jpg",
},
{
name: "经典摇滚",
songs: 40,
cover: "../../assets/img/playlist2.jpg",
},
{
name: "爵士情怀",
songs: 30,
cover: "../../assets/img/playlist3.jpg",
},
],
};
},
};
</script>
<style scoped>
.user-profile {
padding: 20px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
}
/* 个人信息 */
.profile-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #1db954;
}
.user-info {
margin-left: 15px;
}
.username {
font-size: 24px;
font-weight: bold;
color: #333;
}
.bio {
font-size: 14px;
color: #777;
margin-top: 5px;
}
/* 用户统计信息 */
.user-stats {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding: 10px;
background: linear-gradient(135deg, #c9e0ed, #edd4dc);
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.stat {
text-align: center;
}
.stat-number {
font-size: 20px;
font-weight: bold;
color: #1db954;
}
.stat-label {
font-size: 12px;
color: #555;
}
/* 用户歌单 */
.user-playlists h3 {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.playlist-list {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.playlist-item {
display: flex;
align-items: center;
width: calc(33.33% - 10px);
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.playlist-item:hover {
transform: translateY(-5px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.playlist-cover {
width: 60px;
height: 60px;
border-radius: 8px;
object-fit: cover;
margin-right: 10px;
}
.playlist-info {
flex: 1;
}
.playlist-name {
font-size: 14px;
font-weight: bold;
color: #333;
}
.playlist-songs {
font-size: 12px;
color: #777;
}
</style>

@ -0,0 +1,10 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
const app = createApp(App);
app.use(router);
app.use(ElementPlus);
app.mount("#app");

@ -0,0 +1,79 @@
import { createRouter, createWebHistory } from 'vue-router';
import LoginPage from '@/components/LoginPage.vue';
import Web from '@/view/web/Home.vue';
import Admin from '@/view/admin/index.vue';
import SystemIndex from '@/view/admin/systemIndex.vue';
import UserManager from '@/view/admin/userManager.vue';
import ArtistManager from '@/view/admin/artistManager.vue';
import PlaylistManager from '@/view/admin/playlistManager.vue';
import SonglistManager from '@/view/admin/songlistManager.vue';
const routes = [
{
path: '/',
name: 'web',
component: Web,
},
{
path: '/login',
name: 'LoginPage',
component: LoginPage,
},
{
path: '/web',
name: 'webindex',
component: Web,
},
{
path: '/admin',
name: 'adminindex',
component: Admin,
beforeEnter: (to, from, next) => {
const isAdmin = localStorage.getItem('isLoggedIn');
if (isAdmin === 'true') {
next();
} else {
next({ name: 'LoginPage' });
}
},
children: [
{
path: '', // 默认子路由
name: 'SystemIndex',
component: SystemIndex,
},
{
path: 'systemIndex',
name: 'systemIndex',
component: SystemIndex,
},
{
path: 'userManager',
name: 'userManager',
component: UserManager,
},
{
path: 'artistManager',
name: 'artistManager',
component: ArtistManager,
},
{
path: 'playlistManager',
name: 'playlistManager',
component: PlaylistManager,
},
{
path: 'songlistManager',
name: 'songlistManager',
component: SonglistManager,
},
],
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;

@ -0,0 +1,80 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
/* 移除 body 的默认边距和填充 */
body {
margin: 0; /* 移除顶部和底部边距 */
padding: 0; /* 移除左右边距 */
width: 100vw; /* 设置宽度为视口宽度 */
height: 100vh; /* 设置高度为视口高度 */
}
/* #app 设置为全屏 */
#app {
display: flex; /* 使用 flexbox 进行布局 */
flex-direction: column; /* 纵向布局 */
width: 100%; /* 使宽度填满 */
height: 100%; /* 使高度填满 */
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

@ -0,0 +1,29 @@
import { ref } from 'vue';
import wuwuImg from '../assets/img/wuwu.jpeg';
import waokuImg from '../assets/img/waoku.jpg';
let flag = ref(true);
const mySwitch = () => {
const pre_box = document.querySelector('.pre-box');
const img = document.querySelector('#avatar');
if (!pre_box || !img) {
console.error('DOM 元素未找到');
return;
}
if (flag.value) {
pre_box.style.transform = 'translateX(100%)';
pre_box.style.backgroundColor = '#c9e0ed';
img.src = wuwuImg;
} else {
pre_box.style.transform = 'translateX(0%)';
pre_box.style.backgroundColor = '#edd4dc';
img.src = waokuImg;
}
flag.value = !flag.value;
};
export default mySwitch;

@ -0,0 +1,8 @@
import axios from 'axios';
const instance = axios.create({
baseURL: 'http://localhost:8080/api/users',
timeout: 5000,
});
export default instance;

@ -0,0 +1,343 @@
<template>
<div class="artist-management">
<!-- 顶部操作栏 -->
<div class="top-bar">
<div class="search-section">
<el-input
v-model="searchQuery"
placeholder="搜索歌手"
style="width: 300px;"
@input="filterArtists"
clearable
/>
</div>
<div class="button-section">
<el-button type="primary" @click="openAddArtistDialog"></el-button>
<el-button
type="danger"
@click="batchDelete"
:disabled="selection.length === 0"
>
批量删除
</el-button>
</div>
</div>
<!-- 歌手列表表格 -->
<el-table
:data="filteredArtistsData"
border
style="width: 100%"
v-model:selection="selection"
>
<el-table-column type="selection" width="55" />
<el-table-column label="歌手图片" width="120">
<template #default="{ row }">
<img
:src="row.imageUrl || 'https://via.placeholder.com/60'"
alt="artist image"
class="artist-image"
/>
</template>
</el-table-column>
<el-table-column label="歌手名称" prop="name" />
<el-table-column label="性别" prop="gender" />
<el-table-column label="生日" prop="birthday" />
<el-table-column label="地区" prop="region" />
<el-table-column label="简介" prop="bio" />
<el-table-column label="歌曲管理">
<template #default="{ row }">
<el-button type="text" @click="manageSongs(row)"></el-button>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editArtist(row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="deleteArtist(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加歌手对话框 -->
<el-dialog title="添加歌手" v-model="isAddDialogVisible" width="500px">
<el-form :model="newArtist" label-width="100px">
<el-form-item label="歌手名称">
<el-input v-model="newArtist.name" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="newArtist.gender" placeholder="请选择">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="生日">
<el-date-picker
v-model="newArtist.birthday"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="地区">
<el-input v-model="newArtist.region" />
</el-form-item>
<el-form-item label="简介">
<el-input type="textarea" v-model="newArtist.bio" rows="3" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="isAddDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveNewArtist"></el-button>
</div>
</el-dialog>
<!-- 编辑歌手对话框 -->
<el-dialog title="编辑歌手信息" v-model="isEditDialogVisible" width="500px">
<el-form :model="currentArtist" label-width="100px">
<el-form-item label="歌手名称">
<el-input v-model="currentArtist.name" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="currentArtist.gender" placeholder="请选择">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="生日">
<el-date-picker
v-model="currentArtist.birthday"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="地区">
<el-input v-model="currentArtist.region" />
</el-form-item>
<el-form-item label="简介">
<el-input type="textarea" v-model="currentArtist.bio" rows="3" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="isEditDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveArtist"></el-button>
</div>
</el-dialog>
</div>
</template>
<script >
import axios from "axios";
export default {
data() {
return {
searchQuery: "",
selection: [],
artistsData: [],
filteredArtistsData: [],
isAddDialogVisible: false,
isEditDialogVisible: false,
currentArtist: {},
newArtist: {
imageUrl: "",
name: "",
gender: "",
birthday: "",
region: "",
bio: "",
},
};
},
created() {
this.fetchArtists();
},
methods: {
async fetchArtists() {
try {
const response = await axios.get("http://localhost:8081/singers");
this.artistsData = response.data;
this.filteredArtistsData = [...this.artistsData];
} catch (error) {
this.$message.error("加载歌手数据失败");
}
},
openAddArtistDialog() {
this.isAddDialogVisible = true;
this.resetNewArtist();
},
async saveNewArtist() {
try {
const response = await axios.post("http://localhost:8081/singers", this.newArtist);
this.artistsData.push(response.data);
this.filteredArtistsData = [...this.artistsData];
this.isAddDialogVisible = false;
this.$message.success("添加成功");
} catch {
this.$message.error("添加失败");
}
},
resetNewArtist() {
this.newArtist = {
imageUrl: "",
name: "",
gender: "",
birthday: "",
region: "",
bio: "",
};
},
editArtist(artist) {
this.currentArtist = JSON.parse(JSON.stringify(artist));
this.isEditDialogVisible = true;
},
async saveArtist() {
try {
const response = await axios.put(
`http://localhost:8081/singers/${this.currentArtist.id}`,
this.currentArtist
);
const index = this.artistsData.findIndex(
(item) => item.id === this.currentArtist.id
);
if (index !== -1) {
this.artistsData[index] = response.data;
this.filteredArtistsData = [...this.artistsData];
this.isEditDialogVisible = false;
this.$message.success("编辑成功");
}
} catch {
this.$message.error("编辑失败");
}
},
async deleteArtist(artist) {
try {
await axios.delete(`http://localhost:8081/singers/${artist.id}`);
this.artistsData = this.artistsData.filter((item) => item.id !== artist.id);
this.filteredArtistsData = [...this.artistsData];
this.$message.success("删除成功");
} catch {
this.$message.error("删除失败");
}
},
async batchDelete() {
try {
const ids = this.selection.map((item) => item.id);
const promises = ids.map((id) =>
axios.delete(`http://localhost:8081/singers/${id}`).catch(() => id)
);
const failedIds = (await Promise.all(promises)).filter((id) => id);
this.artistsData = this.artistsData.filter(
(item) => !ids.includes(item.id)
);
this.filteredArtistsData = [...this.artistsData];
if (failedIds.length) {
this.$message.warning(`部分删除失败: ${failedIds.join(", ")}`);
} else {
this.$message.success("批量删除成功");
}
} catch {
this.$message.error("批量删除失败");
}
},
filterArtists() {
const query = this.searchQuery.toLowerCase();
this.filteredArtistsData = this.artistsData.filter((item) =>
item.name.toLowerCase().includes(query)
);
},
},
};
</script>
<style scoped>
.artist-management {
padding: 20px;
}
.top-bar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.artist-image {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
}
.dialog-footer {
text-align: right;
}
/* 优化表格 */
.el-table {
margin-top: 20px;
width: 100%;
}
/* 调整操作列按钮的排列 */
.el-table-column .cell {
display: flex;
justify-content: flex-start;
align-items: center;
}
.el-table-column .cell .el-button {
margin-right: 10px; /* 按钮之间的间距 */
font-size: 12px; /* 更小的字体 */
padding: 5px 10px; /* 更小的内边距 */
width: auto; /* 按钮宽度自适应 */
}
/* 搜索框和按钮区域优化 */
.search-section {
display: flex;
align-items: center;
}
.button-section {
display: flex;
gap: 10px; /* 增加按钮之间的间距 */
}
/* 优化按钮尺寸 */
.el-button {
font-size: 12px; /* 更小的字体 */
padding: 5px 10px; /* 调整按钮内边距 */
min-width: 80px; /* 设置最小宽度 */
white-space: nowrap; /* 防止文本换行 */
max-width: 100px; /* 限制按钮最大宽度 */
}
/* 优化操作按钮排列,确保它们不换行 */
.el-table-column .cell {
display: flex;
justify-content: flex-start;
align-items: center;
}
.el-table-column .cell .el-button {
margin-right: 10px;
font-size: 12px;
padding: 5px 10px; /* 更小的内边距 */
min-width: 80px; /* 设置最小宽度 */
max-width: 120px; /* 限制按钮最大宽度 */
}
/* 确保按钮按一行排列 */
.button-section {
display: flex;
flex-wrap: nowrap; /* 防止换行 */
gap: 10px; /* 按钮间隔 */
}
</style>

@ -0,0 +1,255 @@
<template>
<div class="song-manager">
<el-row class="top-bar" type="flex" justify="space-between" align="middle">
<!-- 搜索框 -->
<el-col :span="8">
<el-input
v-model="searchQuery"
placeholder="搜索歌单"
clearable
suffix-icon="el-icon-search"
></el-input>
</el-col>
<!-- 批量删除和添加歌曲按钮 -->
<el-col :span="6" class="button-group" style="text-align: right;">
<el-button
type="danger"
:disabled="selectedSongs.length === 0"
@click="batchDeleteSongs"
>
批量删除
</el-button>
<el-button type="primary" @click="openUploadDialog">
添加歌单
</el-button>
</el-col>
</el-row>
<!-- 歌曲列表 -->
<el-table
:data="filteredSongs"
style="width: 100%"
@selection-change="handleSelectionChange"
v-loading="loading"
>
<!-- 复选框 -->
<el-table-column type="selection" width="55"></el-table-column>
<!-- 歌单图片 -->
<el-table-column label="歌单图片" width="150">
<template #default="{ row }">
<div>
<img :src="row.image" alt="歌单图片" class="song-image" />
<el-button size="mini" type="primary" @click="updateImage(row)">
更新图片
</el-button>
</div>
</template>
</el-table-column>
<!-- 标题 -->
<el-table-column label="标题" prop="name"></el-table-column>
<!-- 简介 -->
<el-table-column label="简介" prop="description"></el-table-column>
<!-- 发行时间 -->
<el-table-column label="发行时间" prop="releaseDate"></el-table-column>
<!-- 更新时间 -->
<el-table-column label="更新时间" prop="updateDate"></el-table-column>
<!-- 歌曲管理按钮 -->
<el-table-column label="歌曲管理" width="160">
<template #default="{ row }">
<el-button size="mini" type="primary" @click="manageSong(row)">
歌曲管理
</el-button>
</template>
</el-table-column>
<!-- 操作按钮 -->
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button size="mini" type="primary" @click="editSong(row)"></el-button>
<el-button size="mini" type="danger" @click="deleteSong(row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加歌曲对话框 -->
<el-dialog title="上传歌曲" v-model="uploadDialogVisible">
<el-form ref="uploadForm" v-model="songData" label-width="80px">
<el-form-item label="歌曲名">
<el-input v-model="songData.name"></el-input>
</el-form-item>
<el-form-item label="歌手">
<el-input v-model="songData.artist"></el-input>
</el-form-item>
<el-form-item label="专辑">
<el-input v-model="songData.album"></el-input>
</el-form-item>
<el-form-item label="歌词">
<el-input v-model="songData.lyrics"></el-input>
</el-form-item>
<el-form-item label="文件上传">
<el-upload
action="/api/upload/song"
:on-success="handleUploadSuccess"
:before-upload="beforeUpload"
:show-file-list="false"
>
<el-button slot="trigger" type="primary">选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpload"></el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: "", //
selectedSongs: [], //
filteredSongs: [], //
songs: [], //
loading: false, //
uploadDialogVisible: false, //
songData: {
name: "",
artist: "",
album: "",
lyrics: "",
}, //
};
},
computed: {
//
filteredSongs() {
if (!this.searchQuery.trim()) {
return this.songs;
}
return this.songs.filter((song) =>
song.name.toLowerCase().includes(this.searchQuery.toLowerCase())
);
},
},
methods: {
//
fetchSongs() {
this.loading = true;
//
setTimeout(() => {
this.songs = [
{
id: 1,
name: "歌单1",
artist: "歌手A",
album: "专辑A",
style: "流行",
description: "流行风格歌单",
image: "https://via.placeholder.com/150",
},
{
id: 2,
name: "歌单2",
artist: "歌手B",
album: "专辑B",
style: "摇滚",
description: "摇滚风格歌单",
image: "https://via.placeholder.com/150",
},
];
this.loading = false;
}, 1000);
},
//
batchDeleteSongs() {
this.songs = this.songs.filter(
(song) => !this.selectedSongs.includes(song)
);
this.selectedSongs = [];
},
//
handleSelectionChange(selected) {
this.selectedSongs = selected;
},
//
openUploadDialog() {
this.uploadDialogVisible = true;
},
//
submitUpload() {
if (!this.songData.name || !this.songData.artist) {
this.$message.error("请完整填写歌曲信息");
return;
}
//
this.songs.push({
id: Date.now(),
...this.songData,
image: "https://via.placeholder.com/150",
description: "用户上传的歌单",
style: "未知",
});
this.uploadDialogVisible = false;
this.songData = {}; //
this.$message.success("歌单上传成功!");
},
//
manageSong(row) {
this.$message.info(`管理歌单: ${row.name}`);
},
//
manageComments(row) {
this.$message.info(`管理评论: ${row.name}`);
},
//
updateImage(row) {
this.$message.info(`更新图片: ${row.name}`);
},
//
editSong(row) {
this.$message.info(`编辑歌单: ${row.name}`);
},
//
deleteSong(id) {
this.songs = this.songs.filter((song) => song.id !== id);
this.$message.success("歌单删除成功!");
},
//
handleUploadSuccess(response) {
this.$message.success("文件上传成功!");
},
//
beforeUpload(file) {
const isImage = file.type.startsWith("image/");
if (!isImage) {
this.$message.error("只能上传图片文件!");
}
return isImage;
},
},
mounted() {
this.fetchSongs();
},
};
</script>
<style scoped>
.song-manager {
padding-top: 20px; /* 根据需要调整距离 */
}
.top-bar {
margin-bottom: 20px; /* 调整与表单的间距 */
}
</style>

@ -0,0 +1,321 @@
<template>
<div class="song-management">
<!-- 顶部操作栏 -->
<div class="top-bar">
<!-- 搜索框 -->
<el-input
v-model="searchQuery"
placeholder="搜索歌曲"
style="width: 300px;"
@input="handleSearch"
clearable
/>
<!-- 添加按钮 -->
<el-button type="primary" @click="openAddSongDialog"></el-button>
</div>
<!-- 表格 -->
<el-table
:data="filteredSongsData"
border
style="width: 100%"
>
<!-- 第一列复选框 -->
<el-table-column type="selection" width="55"></el-table-column>
<!-- 第二列歌曲图片 -->
<el-table-column label="歌曲图片" width="120">
<template #default="{ row }">
<img :src="row.imageUrl || 'https://via.placeholder.com/60'" alt="song image" class="song-image" />
</template>
</el-table-column>
<!-- 第三列歌手 -->
<el-table-column label="歌手" prop="artist"></el-table-column>
<!-- 第四列歌名 -->
<el-table-column label="歌名" prop="name"></el-table-column>
<!-- 第五列专辑 -->
<el-table-column label="专辑" prop="album"></el-table-column>
<!-- <el-table-column> 标签中插入以下内容 -->
<el-table-column label="时长 (秒)" prop="duration"></el-table-column>
<!-- 第六列歌词 -->
<el-table-column label="歌词" prop="lyrics"></el-table-column>
<!-- 第七列资源更新 -->
<el-table-column label="资源更新">
<template #default="{ row }">
<el-button type="text" @click="updateResource(row)"></el-button>
</template>
</el-table-column>
<!-- 第八列操作 -->
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editSong(row)"></el-button>
<el-button type="danger" size="small" @click="deleteSong(row)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加歌曲对话框 -->
<el-dialog title="添加歌曲" v-model="isAddDialogVisible" width="500px">
<el-form :model="newSong" label-width="100px">
<el-form-item label="歌曲图片">
<el-input v-model="newSong.imageUrl" placeholder="请输入图片链接"></el-input>
</el-form-item>
<el-form-item label="歌手">
<el-input v-model="newSong.artist"></el-input>
</el-form-item>
<el-form-item label="歌名">
<el-input v-model="newSong.name"></el-input>
</el-form-item>
<el-form-item label="专辑">
<el-input v-model="newSong.album"></el-input>
</el-form-item>
<el-form-item label="时长 (秒)">
<el-input v-model="newSong.duration" type="number" placeholder="请输入歌曲时长"></el-input>
</el-form-item>
<el-form-item label="歌词">
<el-input type="textarea" v-model="newSong.lyrics" rows="3"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="isAddDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveNewSong"></el-button>
</div>
</el-dialog>
<!-- 编辑歌曲对话框 -->
<el-dialog title="编辑歌曲" v-model="isEditDialogVisible" width="500px">
<el-form :model="currentSong" label-width="100px">
<el-form-item label="歌曲图片">
<el-input v-model="currentSong.imageUrl"></el-input>
</el-form-item>
<el-form-item label="歌手">
<el-input v-model="currentSong.artist"></el-input>
</el-form-item>
<el-form-item label="歌名">
<el-input v-model="currentSong.name"></el-input>
</el-form-item>
<el-form-item label="专辑">
<el-input v-model="currentSong.album"></el-input>
</el-form-item>
<el-form-item label="时长 (秒)">
<el-input v-model="currentSong.duration" type="number"></el-input>
</el-form-item>
<el-form-item label="歌词">
<el-input type="textarea" v-model="currentSong.lyrics" rows="3"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="isEditDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveSong"></el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
searchQuery: '', //
songsData: [], //
filteredSongsData: [], //
isAddDialogVisible: false, //
isEditDialogVisible: false, //
currentSong: {}, //
newSong: {
imageUrl: '',
artist: '',
name: '',
album: '',
lyrics: '',
duration: '', //
},
};
},
created() {
this.fetchSongs(); //
console.log("11111")
},
methods: {
//
async fetchSongs() {
try {
const response = await axios.get('http://localhost:8081/songs'); // API
this.songsData = response.data; //
this.filteredSongsData = [...this.songsData]; //
} catch (error) {
this.$message.error('加载歌曲数据失败');
console.error(error);
}
},
//
handleSearch() {
const query = this.searchQuery.trim().toLowerCase();
this.filteredSongsData = this.songsData.filter((song) =>
Object.values(song).some((value) =>
value?.toString().toLowerCase().includes(query)
)
);
},
//
openAddSongDialog() {
this.isAddDialogVisible = true;
},
//
async saveNewSong() {
if (!this.newSong.name || !this.newSong.artist || !this.newSong.duration) {
this.$message.warning('请填写完整信息');
return;
}
try {
const response = await axios.post('http://localhost:8081/songs', this.newSong);
this.songsData.push(response.data);
this.filteredSongsData = [...this.songsData];
this.isAddDialogVisible = false;
this.newSong = { imageUrl: '', artist: '', name: '', album: '', lyrics: '', duration: '' };
this.$message.success('添加成功');
} catch (error) {
this.$message.error('保存歌曲失败');
console.error(error);
}
},
//
editSong(song) {
this.currentSong = { ...song };
this.isEditDialogVisible = true;
},
//
async saveSong() {
try {
const response = await axios.put(`http://localhost:8081/songs/${this.currentSong.id}`, this.currentSong);
const index = this.songsData.findIndex((s) => s.id === this.currentSong.id);
if (index !== -1) {
this.songsData.splice(index, 1, response.data);
this.filteredSongsData = [...this.songsData];
this.isEditDialogVisible = false;
this.$message.success('编辑成功');
}
} catch (error) {
this.$message.error('更新歌曲失败');
console.error(error);
}
},
//
async deleteSong(song) {
this.$confirm(`确认删除歌曲 "${song.name}" 吗?`, '提示', { type: 'warning' })
.then(async () => {
try {
await axios.delete(`http://localhost:8081/songs/${song.id}`); // DELETE
this.songsData = this.songsData.filter((s) => s.id !== song.id);
this.filteredSongsData = [...this.songsData];
this.$message.success('删除成功');
} catch (error) {
this.$message.error('删除歌曲失败');
console.error(error);
}
})
.catch(() => {});
},
//
updateResource(song) {
this.$message.info(`更新资源:${song.name}`);
},
},
};
</script>
<style scoped>
/* 页面整体样式 */
.song-management {
padding: 20px;
background-color: #f9f9f9;
font-family: Arial, sans-serif;
}
/* 顶部操作栏样式 */
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
gap: 10px;
}
.top-bar .search-section {
flex: 0 0 auto;
}
.top-bar .el-input {
width: 300px; /* 固定宽度 */
}
/* 表格中的图片样式 */
.song-image {
width: 60px;
height: 60px;
border-radius: 6px;
object-fit: cover;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* 操作按钮样式 */
.el-table .el-button {
margin: 0 5px;
}
/* 表格列的最小宽度调整 */
.el-table-column {
word-wrap: break-word;
word-break: break-word;
}
/* 对话框中的表单样式 */
.el-dialog .el-form-item {
margin-bottom: 15px;
}
/* 表单中输入框的统一宽度 */
.el-dialog .el-input {
width: 100%;
}
/* 弹窗底部按钮样式 */
.dialog-footer {
text-align: right;
}
.dialog-footer .el-button {
margin-left: 10px;
}
</style>

@ -0,0 +1,243 @@
<template>
<div class="dashboard">
<!-- 第一行用户总数歌曲总数歌手数量歌单数量 -->
<el-row :gutter="20">
<el-col :span="6" v-for="(item, index) in summaryData" :key="index">
<el-card class="box-card">
<div slot="header" class="card-header">{{ item.title }}</div>
<div class="card-content">{{ item.value }}</div>
</el-card>
</el-col>
</el-row>
<!-- 第二行用户性别比例和歌曲类型分布 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<div class="chart-card">
<h3>用户性别比例</h3>
<div ref="genderPieChart" class="chart-container"></div>
</div>
</el-col>
<el-col :span="12">
<div class="chart-card">
<h3>歌曲类型分布</h3>
<div ref="songTypeBarChart" class="chart-container"></div>
</div>
</el-col>
</el-row>
<!-- 第三行歌手性别比例和歌手国籍分布 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<div class="chart-card">
<h3>歌手性别比例</h3>
<div ref="artistGenderPieChart" class="chart-container"></div>
</div>
</el-col>
<el-col :span="12">
<div class="chart-card">
<h3>歌手国籍分布</h3>
<div ref="artistNationalityBarChart" class="chart-container"></div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
//
const summaryData = ref([
{ title: '用户总数', value: 1200 },
{ title: '歌曲总数', value: 2000 },
{ title: '歌手数量', value: 450 },
{ title: '歌单数量', value: 300 }
]);
//
const genderData = ref([
{ value: 60, name: '男' },
{ value: 40, name: '女' }
]);
//
const songTypeData = ref([
{ name: 'Pop', value: 500 },
{ name: 'Rock', value: 300 },
{ name: 'Jazz', value: 200 },
{ name: 'Classical', value: 400 },
]);
//
const artistGenderData = ref([
{ value: 70, name: '男' },
{ value: 30, name: '女' }
]);
//
const artistNationalityData = ref([
{ name: '美国', value: 800 },
{ name: '中国', value: 600 },
{ name: '英国', value: 400 },
{ name: '韩国', value: 200 },
]);
// ref
const genderPieChart = ref(null);
const songTypeBarChart = ref(null);
const artistGenderPieChart = ref(null);
const artistNationalityBarChart = ref(null);
onMounted(() => {
//
const genderPieChartInstance = echarts.init(genderPieChart.value, 'light');
genderPieChartInstance.setOption({
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '用户性别',
type: 'pie',
radius: '50%',
data: genderData.value,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
});
//
const songTypeBarChartInstance = echarts.init(songTypeBarChart.value, 'light');
songTypeBarChartInstance.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
},
xAxis: {
type: 'category',
data: songTypeData.value.map(item => item.name),
},
yAxis: { type: 'value' },
series: [
{
name: '歌曲类型',
type: 'bar',
data: songTypeData.value.map(item => item.value),
},
],
});
//
const artistGenderPieChartInstance = echarts.init(artistGenderPieChart.value, 'light');
artistGenderPieChartInstance.setOption({
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '歌手性别',
type: 'pie',
radius: '50%',
data: artistGenderData.value,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
});
//
const artistNationalityBarChartInstance = echarts.init(artistNationalityBarChart.value, 'light');
artistNationalityBarChartInstance.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
},
xAxis: {
type: 'category',
data: artistNationalityData.value.map(item => item.name),
},
yAxis: { type: 'value' },
series: [
{
name: '歌手国籍',
type: 'bar',
data: artistNationalityData.value.map(item => item.value),
},
],
});
});
</script>
<style scoped>
.dashboard {
padding: 20px;
}
.card-header {
font-size: 18px;
font-weight: bold;
color: #409EFF;
}
.card-content {
font-size: 24px;
margin-top: 10px;
}
/* 最上面的四个卡片的高度调整 */
.card-card {
background: #fff;
padding: 15px 20px; /* 减小内边距,降低高度 */
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease; /* 平滑过渡 */
height: 120px; /* 可以设置一个固定的高度,依据需求调整 */
}
/* 卡片悬停效果 */
.card-card:hover {
transform: scale(1.05); /* 放大效果 */
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); /* 增加阴影效果 */
}
.chart-card {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease; /* 平滑过渡 */
}
/* 图表卡片悬停效果 */
.chart-card:hover {
transform: scale(1.05); /* 放大效果 */
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); /* 增加阴影效果 */
}
.chart-container {
height: 300px;
}
</style>

@ -0,0 +1,286 @@
<template>
<div class="user-manager">
<!-- 顶部工具栏 -->
<el-row class="top-bar" type="flex" justify="space-between" align="middle">
<el-col :span="8">
<el-input
v-model="searchQuery"
placeholder="搜索用户"
clearable
suffix-icon="el-icon-search"
/>
</el-col>
<el-col :span="6" class="button-group">
<el-button
type="danger"
:disabled="selectedUsers.length === 0"
@click="batchDeleteUsers"
>
批量删除
</el-button>
<el-button type="primary" @click="openAddUserDialog">
添加用户
</el-button>
</el-col>
</el-row>
<!-- 用户列表 -->
<el-table
:data="filteredUsers"
border
v-loading="loading"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="username" label="用户名" width="150" />
<el-table-column label="用户图片" width="100">
<template #default="{ row }">
<img :src="row.profileImage" alt="用户图片" class="user-image" />
</template>
</el-table-column>
<el-table-column prop="gender" label="性别" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="email" label="电子邮箱" />
<el-table-column prop="birthday" label="生日" />
<el-table-column prop="signature" label="签名" />
<el-table-column prop="location" label="地区" />
<el-table-column label="会员">
<template #default="{ row }">
<el-switch v-model="row.isMember" active-text="" inactive-text="" />
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button size="mini" type="primary" @click="openEditUserDialog(row)"></el-button>
<el-button size="mini" type="danger" @click="deleteUser(row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 用户编辑/添加对话框 -->
<el-dialog :title="dialogTitle" v-model="userDialogVisible" width="50%">
<el-form ref="userForm" :model="currentUserData" label-width="80px">
<el-form-item label="用户名">
<el-input v-model="currentUserData.username" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="currentUserData.gender" placeholder="请选择性别">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="currentUserData.phone" />
</el-form-item>
<el-form-item label="电子邮箱">
<el-input v-model="currentUserData.email" />
</el-form-item>
<el-form-item label="生日">
<el-date-picker v-model="currentUserData.birthday" type="date" placeholder="选择生日" />
</el-form-item>
<el-form-item label="签名">
<el-input v-model="currentUserData.signature" />
</el-form-item>
<el-form-item label="地区">
<el-input v-model="currentUserData.location" />
</el-form-item>
<el-form-item label="密码" v-if="isAddUser">
<el-input type="password" v-model="currentUserData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="用户图片">
<el-upload
action="/api/upload/profile-image"
:on-success="handleImageUploadSuccess"
:before-upload="beforeUpload"
:show-file-list="false"
>
<el-button type="primary">选择图片</el-button>
</el-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUserForm"></el-button>
</span>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import axios from 'axios';
import { ElMessage } from 'element-plus';
const users = ref([]); //
const searchQuery = ref(''); //
const selectedUsers = ref([]); //
const userDialogVisible = ref(false); //
const dialogTitle = ref("添加用户"); //
const loading = ref(false); //
const currentUserData = ref({
id: '',
username: '',
gender: '',
phone: '',
email: '',
birthday: '',
signature: '',
location: '',
profileImage: '',
password: '',
});
const isAddUser = computed(() => dialogTitle.value === "添加用户");
//
const fetchUsers = async () => {
loading.value = true;
try {
const response = await axios.get('http://localhost:8081/api/users');
users.value = Array.isArray(response.data) ? response.data : [];
} catch (error) {
console.error('获取用户数据失败:', error);
ElMessage.error('用户数据加载失败,请稍后重试');
} finally {
loading.value = false;
}
};
//
const filteredUsers = computed(() => {
const query = searchQuery.value.trim().toLowerCase();
return users.value.filter(user =>
user.username.toLowerCase().includes(query) ||
user.phone.includes(query) ||
user.email.toLowerCase().includes(query)
);
});
//
const handleSelectionChange = (selection) => {
selectedUsers.value = selection;
};
//
const openAddUserDialog = () => {
currentUserData.value = {
id: '',
username: '',
gender: '',
phone: '',
email: '',
birthday: '',
signature: '',
location: '',
profileImage: '',
password: '',
};
dialogTitle.value = "添加用户";
userDialogVisible.value = true;
};
//
const openEditUserDialog = (user) => {
currentUserData.value = { ...user };
dialogTitle.value = "编辑用户";
userDialogVisible.value = true;
};
//
const submitUserForm = async () => {
loading.value = true;
try {
if (isAddUser.value) {
//
await axios.post('http://localhost:8081/api/users/register', currentUserData.value, {
headers: { 'Content-Type': 'application/json' },
});
ElMessage.success('用户添加成功');
} else {
console.log("1111111111",currentUserData.value);
// //
await axios.post('http://localhost:8081/api/users/update', currentUserData.value, {
headers: { 'Content-Type': 'application/json' },
});
ElMessage.success('用户更新成功');
}
fetchUsers(); //
userDialogVisible.value = false;
} catch (error) {
console.error('操作失败:', error.response ? error.response.data : error);
ElMessage.error('用户操作失败,请检查输入内容');
} finally {
loading.value = false;
}
};
//
const deleteUser = async (userId) => {
loading.value = true;
try {
await axios.delete(`http://localhost:8081/api/users/${userId}`);
ElMessage.success('用户删除成功');
fetchUsers(); //
} catch (error) {
console.error('删除用户失败:', error);
ElMessage.error('删除失败,请稍后重试');
} finally {
loading.value = false;
}
};
//
const batchDeleteUsers = async () => {
if (selectedUsers.value.length === 0) return;
const ids = selectedUsers.value.map(user => user.id);
loading.value = true;
try {
await axios.delete('http://localhost:8081/api/users/batch-delete', { data: ids });
ElMessage.success('批量删除成功');
fetchUsers(); //
} catch (error) {
console.error('批量删除失败:', error);
ElMessage.error('批量删除失败,请稍后重试');
} finally {
loading.value = false;
}
};
//
const handleImageUploadSuccess = (response) => {
currentUserData.value.profileImage = response.url; // URL
ElMessage.success('图片上传成功');
};
//
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
ElMessage.error('只能上传图片文件');
}
return isImage;
};
//
fetchUsers();
</script>
<style scoped>
.user-manager {
padding: 20px;
background: #fff;
border: 1px solid #e4e4e4;
border-radius: 5px;
}
.top-bar {
margin-bottom: 20px;
}
.el-table .user-image {
width: 50px;
height: 50px;
border-radius: 50%;
}
</style>

@ -0,0 +1,389 @@
<template>
<div class="common-layout">
<el-container style="height: 100%">
<el-header class="music-header">
<el-row class="header-content" type="flex" justify="space-between">
<!-- 只保留 Music管理系统紧靠左侧 -->
<el-col :span="24" class="logo-container">
<span class="logo-text">Music管理系统</span>
</el-col>
<!-- 右侧管理员头像 -->
<el-col class="user-avatar">
<el-avatar
size="40"
shape="circle"
src="https://via.placeholder.com/40"
@click="handleAvatarClick"
style="cursor: pointer;"
/>
</el-col>
</el-row>
</el-header>
<!-- Drawer 弹窗 -->
<!-- Drawer 弹窗 -->
<el-drawer title="Edit User" v-model="drawerVisible" size="30%">
<el-form :model="formData" :rules="rules" ref="formRef">
<el-form-item label="Username" prop="username">
<el-input v-model="formData.username" placeholder="Enter username"></el-input>
</el-form-item>
<el-form-item label="Profile Image" prop="profileImage">
<el-input v-model="formData.profileImage" placeholder="Enter image URL"></el-input>
</el-form-item>
<el-form-item label="Gender" prop="gender">
<el-select v-model="formData.gender" placeholder="Select gender">
<el-option label="Male" value="Male"></el-option>
<el-option label="Female" value="Female"></el-option>
</el-select>
</el-form-item>
<el-form-item label="Phone" prop="phone">
<el-input v-model="formData.phone" placeholder="Enter phone number"></el-input>
</el-form-item>
<el-form-item label="Email" prop="email">
<el-input v-model="formData.email" placeholder="Enter email"></el-input>
</el-form-item>
<el-form-item label="Birthday" prop="birthday">
<el-date-picker v-model="formData.birthday" type="date" placeholder="Select birthday"></el-date-picker>
</el-form-item>
<el-form-item label="Signature" prop="signature">
<el-input v-model="formData.signature" placeholder="Enter signature"></el-input>
</el-form-item>
<el-form-item label="Location" prop="location">
<el-input v-model="formData.location" placeholder="Enter location"></el-input>
</el-form-item>
<el-form-item label="Password" prop="password">
<el-input v-model="formData.password" type="password" placeholder="Enter password"></el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="drawerVisible = false">Cancel</el-button>
<el-button type="primary" @click="submitForm(formData)">Save</el-button>
</div>
</el-drawer>
<el-container>
<el-aside width="200px">
<el-row class="tac" >
<el-col>
<el-menu default-active="1" class="el-menu-vertical-demo">
<!-- 一级菜单项系统首页 -->
<el-menu-item index="1">
<router-link to="/admin/systemIndex">
<el-icon><location /></el-icon>
<span>系统首页</span>
</router-link>
</el-menu-item>
<!-- 一级菜单项用户管理 -->
<el-menu-item index="2">
<router-link to="/admin/userManager">
<el-icon><icon-menu /></el-icon>
<span>用户管理</span>
</router-link>
</el-menu-item>
<!-- 一级菜单项歌手管理 -->
<el-menu-item index="3">
<router-link to="/admin/artistManager">
<el-icon><icon-menu /></el-icon>
<span>歌手管理</span>
</router-link>
</el-menu-item>
<!-- 一级菜单项歌单管理 -->
<el-menu-item index="4">
<router-link to="/admin/playlistManager">
<el-icon><icon-menu /></el-icon>
<span>歌单管理</span>
</router-link>
</el-menu-item>
<!-- 一级菜单项歌单管理 -->
<el-menu-item index="5">
<router-link to="/admin/songlistManager">
<el-icon><icon-menu /></el-icon>
<span>歌曲管理</span>
</router-link>
</el-menu-item>
</el-menu>
</el-col>
</el-row>
</el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script lang="ts" setup>
import {
Location,
Menu as IconMenu,
} from '@element-plus/icons-vue'
import { ref } from 'vue'
import axios from 'axios'
const drawerVisible = ref(false)
const formData = ref({
id: null,
username: '',
profileImage: '',
gender: '',
phone: '',
email: '1',
birthday: '',
signature: '',
location: '',
password: '',
})
const rules = {
username: [
{ required: true, message: 'Username is required', trigger: 'blur' },
],
email: [
{ type: 'email', message: 'Invalid email', trigger: 'blur' },
],
phone: [
{ pattern: /^[0-9]{10}$/, message: 'Invalid phone number', trigger: 'blur' },
],
}
const handleAvatarClick = async () => {
drawerVisible.value = true;
}
const updateData = async() => {
try{
const user = localStorage.getItem('user');
console.log("user", user); // user
const userObj = JSON.parse(user);
console.log("userObj", userObj.username); // username
const response = await axios.put('http://localhost:8081/api/users/admin', { username: userObj.username });
formData.value = { ...response.data };
}catch(error){
console.log("数据错误:",error);
alert("请重试");
}
}
const submitForm = async (formData) => {
try {
// await axios.put('http://localhost:8081/api/users/admin', formData.value)
// alert('')
// drawerVisible.value = false
// const user = localStorage.getItem('user');
// console.log("user", user); // user
// const userObj = JSON.parse(user);
// console.log("userObj", userObj.username); // username
console.log(formData);
const response = await axios.put('http://localhost:8081/api/users/admin', {
userUpdateDto:formData});
formData.value = { ...response.data };
} catch (error) {
console.error('保存管理员信息失败:', error)
alert('保存失败,请重试')
}
}
</script>
<style scoped>
.music-header {
background-color: #3498db;
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.logo-text {
font-size: 22px;
font-weight: bold;
margin-left: 15px;
text-transform: uppercase;
}
.user-avatar {
position: absolute;
right: 20px;
top: 10px;
}
.drawer-content {
margin-top: 20px;
}
.admin-drawer .el-button {
margin-top: 20px;
width: 100%;
}
/* 顶部导航栏样式 */
.music-header {
background-color: #3498db; /* 变更为中性的蓝色 */
color: white; /* 文字颜色 */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* 更柔和的阴影效果 */
}
.music-header .logo-text {
font-size: 22px; /* 增加字体大小 */
font-weight: bold; /* 加粗文字 */
margin-left: 15px;
text-transform: uppercase; /* 全部大写 */
}
/* 侧边栏样式 */
.el-aside {
background-color: #34495e; /* 更深的蓝灰色背景 */
color: white;
box-shadow: 4px 0 10px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease-in-out; /* 添加滑动动画 */
}
.el-aside:hover {
background-color: #2c3e50; /* 悬停时稍亮 */
}
/* 菜单项样式 */
.el-menu-vertical-demo {
padding-top: 20px;
}
.el-menu-vertical-demo .el-menu-item {
font-size: 18px; /* 字体更大 */
color: white;
border-radius: 6px; /* 圆角 */
margin: 5px 10px; /* 增加间距 */
transition: all 0.2s ease-in-out; /* 鼠标移入效果 */
}
.el-menu-vertical-demo .el-menu-item:hover {
background-color: #1abc9c; /* 更温和的悬停颜色 */
transform: scale(1.05); /* 放大 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* 添加阴影 */
}
.el-menu-vertical-demo .el-menu-item.is-active {
background-color: #1abc9c; /* 活动项背景 */
font-weight: bold;
}
/* 图标颜色 */
.el-menu-vertical-demo .el-menu-item .el-icon {
color: white;
margin-right: 10px; /* 图标与文字的间距 */
}
/* 主内容区域样式 */
.el-main {
margin-left: 200px; /* 留出侧边栏空间 */
margin-top: 60px; /* 留出顶部导航栏空间 */
background-color: #f5f7fa; /* 浅灰背景 */
padding: 30px; /* 内边距 */
border-radius: 10px; /* 圆角 */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); /* 添加阴影 */
animation: fadeIn 0.5s ease; /* 动画效果 */
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 顶部导航栏固定样式 */
.el-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1000;
background-color: #3498db;
text-align: left;
padding-left: 20px;
line-height: 60px;
transition: background-color 0.3s ease-in-out; /* 颜色过渡 */
}
.el-header:hover {
background-color: #2980b9; /* 悬停颜色变化 */
}
/* 侧边栏菜单固定样式 */
.el-aside {
position: fixed;
top: 60px;
left: 0;
width: 200px;
height: calc(100vh - 60px);
overflow-y: auto; /* 滚动条 */
scrollbar-width: thin; /* 自定义滚动条 */
}
/* 滚动条样式 */
.el-aside::-webkit-scrollbar {
width: 6px;
}
.el-aside::-webkit-scrollbar-thumb {
background-color: #1abc9c;
border-radius: 3px;
}
/* 主内容区域调整 */
.el-main {
margin-left: 210px; /* 为滚动条留出空间 */
}
/* 页脚样式 */
.el-footer {
background-color: #ecf5ff;
color: #333;
text-align: center;
line-height: 60px;
border-top: 1px solid #dcdfe6; /* 分割线 */
}
/* 布局调整 */
.common-layout {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
}
.container {
height: 100%;
}
.logo {
height: 55px;
}
.el-menu-horizontal > .el-menu-item:nth-child(3) {
margin-left: auto;
}
</style>

@ -0,0 +1,388 @@
<template>
<div class="music-app">
<!-- Header -->
<header class="header">
<div class="logo">Listen-Music</div>
<nav class="nav">
<a href="#" class="nav-link" :class="{ active: currentPage === 'CarouselSection' }" @click.prevent="switchPage('CarouselSection')">首页</a>
<a href="#" class="nav-link" :class="{ active: currentPage === 'PlaylistPage' }" @click.prevent="switchPage('PlaylistPage')">歌单</a>
<a href="#" class="nav-link" :class="{ active: currentPage === 'SingerPage' }" @click.prevent="switchPage('SingerPage')">歌手</a>
</nav>
<input type="text" class="search-box" placeholder="搜索音乐、歌手" />
<!-- 用户头像及菜单 -->
<!-- 修改文件用户端主界面 -->
<div class="user-menu">
<button class="user-icon" @click="handleUserClick">
<img src="../../assets/img/waoku.jpg" alt="用户头像" />
</button>
<ul class="dropdown-menu" v-if="showMenu">
<li @click="switchPage('UserProfile'); closeMenu()">个人主页</li>
<li @click="switchPage('SystemSetting'); closeMenu()">设置</li>
<li @click="logout(); closeMenu()">退出</li>
</ul>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<component :is="currentPage" :data="pageData" @switchPage="switchPage" />
</main>
<!-- Footer -->
<footer class="footer">
<div class="player">
<!-- 左侧歌曲图片和信息 -->
<div class="song-info">
<img src="../../assets/img/song-cover.jpg" alt="歌曲封面" class="song-cover" />
<div class="song-details">
<div class="song-title">歌曲名称</div>
<div class="song-artist">歌手名称</div>
</div>
</div>
<!-- 中间控制按钮 -->
<div class="controls">
<img src="../../assets/icon/circle.svg" alt="循环播放" class="icon" />
<img src="../../assets/icon/left.svg" alt="上一首" class="icon" />
<img src="../../assets/icon/stop.svg" alt="播放/暂停" class="icon" />
<img src="../../assets/icon/right.svg" alt="下一首" class="icon" />
<img src="../../assets/icon/speaker.svg" alt="音量" class="icon" />
</div>
<!-- 右侧其他功能 -->
<div class="actions">
<img src="../../assets/icon/favour.svg" alt="收藏" class="icon" />
<img src="../../assets/icon/download.svg" alt="下载" class="icon" />
<img src="../../assets/icon/list.svg" alt="列表" class="icon" />
</div>
</div>
</footer>
</div>
</template>
<script>
import CarouselSection from "../../components/CarouselSection.vue";
import PlaylistPage from "../../components/PlaylistPage.vue";
import SingerPage from "../../components/SingerPage.vue";
import UserProfile from "../../components/UserProfile.vue";
import SystemSetting from "../../components/SystemSetting.vue";
import SingerProfile from '../../components/SingerProfile.vue'; //
export default {
name: "Index",
components: {
CarouselSection,
PlaylistPage,
SingerPage,
UserProfile,
SystemSetting,
SingerProfile,
},
data() {
return {
currentPage: "CarouselSection", //
showMenu: false, //
};
},
methods: {
// currentPage
changePage(pageName) {
this.currentPage = pageName; //
},
switchPage(page, data = null) { //
this.currentPage = page;
if (data) {
this.pageData = data; //
}
this.showMenu = false; //
},
logout() {
localStorage.removeItem('isLoggedIn');
alert('您已退出!');
this.$router.push({ name: 'LoginPage' });
},
closeMenu() {
this.showMenu = false; //
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
handleUserClick() {
const isLoggedIn = localStorage.getItem('isLoggedIn');
if (isLoggedIn) {
this.toggleMenu();
} else {
this.$router.push({ name: 'LoginPage' });
}
},
},
mounted() {
document.addEventListener("click", this.handleOutsideClick);
},
beforeDestroy() {
document.removeEventListener("click", this.handleOutsideClick);
},
};
</script>
<style >
/* 保留原来的 CSS */
/* 确保html和body覆盖整个视口并使用正确的盒模型 */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
box-sizing: border-box; /* 设置盒模型 */
}
*,
*::before,
*::after {
box-sizing: inherit; /* 继承盒模型 */
}
.music-app {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/* 头部 */
.header {
width: 100%;
height: 70px;
background: linear-gradient(135deg, #c9e0ed, #edd4dc);
display: flex;
align-items: center;
padding: 0 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
position: relative;
box-sizing: border-box;
}
.logo {
font-size: 26px;
font-weight: bold;
color: black;
margin-right: 20px;
}
.nav {
display: flex;
gap: 15px;
align-items: center;
}
.nav-link {
color: black;
text-decoration: none;
font-size: 16px;
font-weight: bold;
position: relative;
}
.nav-link:hover {
color: #1db954;
}
.nav-link:hover::after {
content: "";
position: absolute;
bottom: -5px;
left: 0;
width: 100%;
height: 2px;
background-color: black;
}
.active {
border-bottom: 2px solid black;
}
.search-box {
margin: 0 auto;
width: 400px;
padding: 8px 15px;
border: 1px solid #ccc;
border-radius: 20px;
font-size: 14px;
}
.user-menu {
margin-left: auto;
position: relative;
display: flex;
align-items: center;
}
.user-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center; /* 确保图片居中 */
overflow: hidden; /* 超出部分隐藏 */
border-radius: 50%; /* 设置圆形 */
border: 2px solid #ddd; /* 添加边框美观效果 */
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); /* 添加阴影 */
}
.user-icon img {
width: 100%; /* 图片宽度匹配容器 */
height: auto; /* 自动调整高度保持比例 */
object-fit: cover; /* 确保图片完整填充容器 */
border-radius: 50%; /* 确保图片圆形 */
}
.dropdown-menu {
position: absolute;
top: 40px;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
list-style: none;
padding: 10px 0;
width: 150px;
z-index: 100;
}
.dropdown-menu li {
padding: 12px 15px;
cursor: pointer;
font-size: 14px;
text-align: left;
color: #333;
transition: background-color 0.3s ease;
}
.dropdown-menu li:hover {
background-color: #f9f9f9;
color: #1db954;
}
/* 内容区 */
.main-content {
flex: 1; /* 占据剩余空间 */
padding: 20px;
background-color: #f9f9f9;
overflow-y: auto; /* 滚动条支持 */
box-sizing: border-box;
}
.carousel {
width: 100%;
height: 250px;
background-color: #ccc;
margin-bottom: 20px;
display: flex;
gap: 10px;
overflow: hidden;
border-radius: 8px;
}
.carousel-item {
flex: 1;
background-color: #888;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border-radius: 8px;
}
/* 底部 */
.footer {
width: 100%;
height: 80px;
background: linear-gradient(135deg, #c9e0ed, #edd4dc);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
position: fixed; /* 固定定位 */
bottom: 0; /* 固定到页面底部 */
left: 0; /* 左边对齐 */
z-index: 10;
box-sizing: border-box;
}
.player {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
}
/* 左侧:歌曲信息 */
.song-info {
display: flex;
align-items: center;
}
.song-cover {
width: 50px;
height: 50px;
border-radius: 5px;
margin-right: 10px;
object-fit: cover;
}
.song-details {
display: flex;
flex-direction: column;
}
.song-title {
font-size: 14px;
font-weight: bold;
color: #333;
}
.song-artist {
font-size: 12px;
color: #666;
}
/* 中间:控制按钮 */
.controls {
display: flex;
gap: 20px;
align-items: center;
}
.icon {
width: 25px;
height: 25px;
cursor: pointer;
margin-left: 15px;
transition: transform 0.2s ease;
}
.icon:hover {
transform: scale(1.2);
}
/* 右侧:其他功能 */
.actions {
display: flex;
gap: 15px;
align-items: center;
}
</style>

@ -0,0 +1,47 @@
<template>
<section class="playlist-detail">
<!-- 如果数据存在正常渲染 -->
<div v-if="pageData">
<h2>{{ pageData.title }}</h2>
<img :src="pageData.cover" alt="歌单封面" />
<p>分类{{ pageData.category }}</p>
</div>
<!-- 如果数据为空显示提示信息 -->
<div v-else>
<p>暂无数据请稍后再试</p>
</div>
<button @click="$emit('switchPage', 'PlaylistPage')">返回</button>
</section>
</template>
<script>
export default {
name: "PlaylistDetailPage",
props: {
//
pageData: {
type: Object,
default: () => ({
title: "默认歌单标题",
cover: "https://via.placeholder.com/150", //
category: "默认分类",
}),
},
},
};
</script>
<style scoped>
.playlist-detail {
text-align: center;
padding: 20px;
}
img {
max-width: 100%;
height: auto;
border-radius: 10px;
}
</style>

@ -0,0 +1,12 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});

@ -0,0 +1,486 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/parser@^7.25.3":
version "7.26.1"
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.26.1.tgz"
integrity sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==
dependencies:
"@babel/types" "^7.26.0"
"@babel/types@^7.26.0":
version "7.26.0"
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.26.0.tgz"
integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@ctrl/tinycolor@^3.4.1":
version "3.6.1"
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz"
integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==
"@element-plus/icons-vue@^2.3.1":
version "2.3.1"
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz"
integrity sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==
"@esbuild/win32-x64@0.21.5":
version "0.21.5"
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz"
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
"@floating-ui/core@^1.6.0":
version "1.6.8"
resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.6.8.tgz"
integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==
dependencies:
"@floating-ui/utils" "^0.2.8"
"@floating-ui/dom@^1.0.1":
version "1.6.11"
resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.6.11.tgz"
integrity sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==
dependencies:
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.8"
"@floating-ui/utils@^0.2.8":
version "0.2.8"
resolved "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.8.tgz"
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
"@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
version "2.11.7"
resolved "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz"
integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
"@rollup/rollup-win32-x64-msvc@4.24.3":
version "4.24.3"
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz"
integrity sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==
"@types/estree@1.0.6":
version "1.0.6"
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/lodash-es@*", "@types/lodash-es@^4.17.6":
version "4.17.12"
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz"
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*", "@types/lodash@^4.14.182":
version "4.17.13"
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.13.tgz"
integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==
"@types/web-bluetooth@^0.0.16":
version "0.0.16"
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz"
integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==
"@vitejs/plugin-vue@^5.1.4":
version "5.1.4"
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz"
integrity sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==
"@vue/compiler-core@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.12.tgz"
integrity sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/shared" "3.5.12"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz"
integrity sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==
dependencies:
"@vue/compiler-core" "3.5.12"
"@vue/shared" "3.5.12"
"@vue/compiler-sfc@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz"
integrity sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/compiler-core" "3.5.12"
"@vue/compiler-dom" "3.5.12"
"@vue/compiler-ssr" "3.5.12"
"@vue/shared" "3.5.12"
estree-walker "^2.0.2"
magic-string "^0.30.11"
postcss "^8.4.47"
source-map-js "^1.2.0"
"@vue/compiler-ssr@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz"
integrity sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==
dependencies:
"@vue/compiler-dom" "3.5.12"
"@vue/shared" "3.5.12"
"@vue/devtools-api@^6.6.4":
version "6.6.4"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
"@vue/reactivity@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.12.tgz"
integrity sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==
dependencies:
"@vue/shared" "3.5.12"
"@vue/runtime-core@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.12.tgz"
integrity sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==
dependencies:
"@vue/reactivity" "3.5.12"
"@vue/shared" "3.5.12"
"@vue/runtime-dom@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz"
integrity sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==
dependencies:
"@vue/reactivity" "3.5.12"
"@vue/runtime-core" "3.5.12"
"@vue/shared" "3.5.12"
csstype "^3.1.3"
"@vue/server-renderer@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.12.tgz"
integrity sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==
dependencies:
"@vue/compiler-ssr" "3.5.12"
"@vue/shared" "3.5.12"
"@vue/shared@3.5.12":
version "3.5.12"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz"
integrity sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==
"@vueuse/core@^9.1.0":
version "9.13.0"
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz"
integrity sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==
dependencies:
"@types/web-bluetooth" "^0.0.16"
"@vueuse/metadata" "9.13.0"
"@vueuse/shared" "9.13.0"
vue-demi "*"
"@vueuse/metadata@9.13.0":
version "9.13.0"
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz"
integrity sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==
"@vueuse/shared@9.13.0":
version "9.13.0"
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz"
integrity sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==
dependencies:
vue-demi "*"
async-validator@^4.2.5:
version "4.2.5"
resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz"
integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/axios/-/axios-1.2.1.tgz"
integrity sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
dayjs@^1.11.3:
version "1.11.13"
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
echarts@^5.5.1:
version "5.5.1"
resolved "https://registry.npmmirror.com/echarts/-/echarts-5.5.1.tgz"
integrity sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==
dependencies:
tslib "2.3.0"
zrender "5.6.0"
element-plus@^2.8.6:
version "2.8.6"
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.8.6.tgz"
integrity sha512-fk5jB8V3efM02/4roZ5SWOLArgaYXbxEydZLlXSr+KPAwjNyHBlk2+HO5em8YKo5+RLBoHnn6BaThj6IE4nXoQ==
dependencies:
"@ctrl/tinycolor" "^3.4.1"
"@element-plus/icons-vue" "^2.3.1"
"@floating-ui/dom" "^1.0.1"
"@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
"@types/lodash" "^4.14.182"
"@types/lodash-es" "^4.17.6"
"@vueuse/core" "^9.1.0"
async-validator "^4.2.5"
dayjs "^1.11.3"
escape-html "^1.0.3"
lodash "^4.17.21"
lodash-es "^4.17.21"
lodash-unified "^1.0.2"
memoize-one "^6.0.0"
normalize-wheel-es "^1.2.0"
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
esbuild@^0.21.3:
version "0.21.5"
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz"
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
optionalDependencies:
"@esbuild/aix-ppc64" "0.21.5"
"@esbuild/android-arm" "0.21.5"
"@esbuild/android-arm64" "0.21.5"
"@esbuild/android-x64" "0.21.5"
"@esbuild/darwin-arm64" "0.21.5"
"@esbuild/darwin-x64" "0.21.5"
"@esbuild/freebsd-arm64" "0.21.5"
"@esbuild/freebsd-x64" "0.21.5"
"@esbuild/linux-arm" "0.21.5"
"@esbuild/linux-arm64" "0.21.5"
"@esbuild/linux-ia32" "0.21.5"
"@esbuild/linux-loong64" "0.21.5"
"@esbuild/linux-mips64el" "0.21.5"
"@esbuild/linux-ppc64" "0.21.5"
"@esbuild/linux-riscv64" "0.21.5"
"@esbuild/linux-s390x" "0.21.5"
"@esbuild/linux-x64" "0.21.5"
"@esbuild/netbsd-x64" "0.21.5"
"@esbuild/openbsd-x64" "0.21.5"
"@esbuild/sunos-x64" "0.21.5"
"@esbuild/win32-arm64" "0.21.5"
"@esbuild/win32-ia32" "0.21.5"
"@esbuild/win32-x64" "0.21.5"
escape-html@^1.0.3:
version "1.0.3"
resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
follow-redirects@^1.15.0:
version "1.15.9"
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
form-data@^4.0.0, form-data@^4.0.1:
version "4.0.1"
resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz"
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
lodash-es@*, lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash-unified@^1.0.2:
version "1.0.3"
resolved "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz"
integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==
lodash@*, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
magic-string@^0.30.11:
version "0.30.12"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.12.tgz"
integrity sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
memoize-one@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz"
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
normalize-wheel-es@^1.2.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz"
integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==
picocolors@^1.1.0:
version "1.1.1"
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
postcss@^8.4.43, postcss@^8.4.47:
version "8.4.47"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.47.tgz"
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
dependencies:
nanoid "^3.3.7"
picocolors "^1.1.0"
source-map-js "^1.2.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
rollup@^4.20.0:
version "4.24.3"
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.24.3.tgz"
integrity sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==
dependencies:
"@types/estree" "1.0.6"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.24.3"
"@rollup/rollup-android-arm64" "4.24.3"
"@rollup/rollup-darwin-arm64" "4.24.3"
"@rollup/rollup-darwin-x64" "4.24.3"
"@rollup/rollup-freebsd-arm64" "4.24.3"
"@rollup/rollup-freebsd-x64" "4.24.3"
"@rollup/rollup-linux-arm-gnueabihf" "4.24.3"
"@rollup/rollup-linux-arm-musleabihf" "4.24.3"
"@rollup/rollup-linux-arm64-gnu" "4.24.3"
"@rollup/rollup-linux-arm64-musl" "4.24.3"
"@rollup/rollup-linux-powerpc64le-gnu" "4.24.3"
"@rollup/rollup-linux-riscv64-gnu" "4.24.3"
"@rollup/rollup-linux-s390x-gnu" "4.24.3"
"@rollup/rollup-linux-x64-gnu" "4.24.3"
"@rollup/rollup-linux-x64-musl" "4.24.3"
"@rollup/rollup-win32-arm64-msvc" "4.24.3"
"@rollup/rollup-win32-ia32-msvc" "4.24.3"
"@rollup/rollup-win32-x64-msvc" "4.24.3"
fsevents "~2.3.2"
source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
vite@^5.0.0, vite@^5.4.10:
version "5.4.10"
resolved "https://registry.npmmirror.com/vite/-/vite-5.4.10.tgz"
integrity sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"
rollup "^4.20.0"
optionalDependencies:
fsevents "~2.3.3"
vue-demi@*:
version "0.14.10"
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
vue-router@^4.4.5:
version "4.4.5"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.4.5.tgz"
integrity sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==
dependencies:
"@vue/devtools-api" "^6.6.4"
"vue@^3.0.0-0 || ^2.6.0", vue@^3.2.0, vue@^3.2.25, vue@^3.5.12, vue@3.5.12:
version "3.5.12"
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.12.tgz"
integrity sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==
dependencies:
"@vue/compiler-dom" "3.5.12"
"@vue/compiler-sfc" "3.5.12"
"@vue/runtime-dom" "3.5.12"
"@vue/server-renderer" "3.5.12"
"@vue/shared" "3.5.12"
zrender@5.6.0:
version "5.6.0"
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.6.0.tgz"
integrity sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==
dependencies:
tslib "2.3.0"

@ -1,2 +0,0 @@
# yinyuexitong

@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

@ -0,0 +1,19 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

259
springboot/demo/mvnw vendored

@ -0,0 +1,259 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

@ -0,0 +1,149 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>demo</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 如果不使用 JPA删除该依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok (可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test (用于单元测试) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 如果不使用 Spring Security可以删除该依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL Connector (如果使用 MySQL 数据库) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot Starter for JPA (如果需要 JPA 功能) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot Starter Test (用于单元测试) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,17 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableJpaRepositories("com.example.demo.repository") // 启用 JPA 仓库扫描
@EntityScan("com.example.demo.entity") // 显式指定扫描实体类路径
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

@ -0,0 +1,49 @@
package com.example.demo.controller;
import com.example.demo.entity.Playlist;
import com.example.demo.service.PlaylistService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/playlists")
@CrossOrigin(origins = "http://localhost:5173") // 允许来自 5173 端口的请求
public class PlaylistController {
@Autowired
private PlaylistService playlistService;
// 获取歌单列表
@GetMapping
public List<Playlist> getPlaylists() {
return playlistService.getAllPlaylists();
}
// 添加歌单
@PostMapping
public Playlist addPlaylist(@RequestBody Playlist playlist) {
return playlistService.addPlaylist(playlist);
}
// 删除单个歌单
@DeleteMapping("/{id}")
public void deletePlaylist(@PathVariable Long id) {
playlistService.deletePlaylist(id);
}
// 批量删除歌单
@PostMapping("/batch-delete")
public void batchDeletePlaylists(@RequestBody List<Long> ids) {
playlistService.batchDeletePlaylists(ids);
}
// 更新歌单
@PutMapping("/{id}")
public Playlist updatePlaylist(@PathVariable Long id, @RequestBody Playlist playlist) {
// 设置歌单 ID 确保服务层能正确识别要更新的歌单
playlist.setId(id);
return playlistService.updatePlaylist(playlist);
}
}

@ -0,0 +1,56 @@
package com.example.demo.controller;
import com.example.demo.entity.Singer;
import com.example.demo.repository.SingerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/singers")
@CrossOrigin(origins = "http://localhost:5173") // 允许来自 5173 端口的请求
public class SingerController {
@Autowired
private SingerRepository singerRepository;
// 获取所有歌手
@GetMapping
public List<Singer> getAllSingers() {
return singerRepository.findAll();
}
// 根据 ID 获取歌手
@GetMapping("/{id}")
public Optional<Singer> getSingerById(@PathVariable Long id) {
return singerRepository.findById(id);
}
// 根据歌手名称搜索歌手
@GetMapping("/search")
public List<Singer> searchSingers(@RequestParam String name) {
return singerRepository.findByNameContainingIgnoreCase(name);
}
// 添加新歌手
@PostMapping
public Singer addSinger(@RequestBody Singer singer) {
return singerRepository.save(singer);
}
// 更新歌手信息
@PutMapping("/{id}")
public Singer updateSinger(@PathVariable Long id, @RequestBody Singer singer) {
// 可以根据需要在这里添加更多的更新逻辑
singer.setId(id); // 设置歌手 ID 以保证更新的是正确的歌手
return singerRepository.save(singer);
}
// 删除歌手
@DeleteMapping("/{id}")
public void deleteSinger(@PathVariable Long id) {
singerRepository.deleteById(id);
}
}

@ -0,0 +1,61 @@
package com.example.demo.controller;
import com.example.demo.entity.Song;
import com.example.demo.repository.SongRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/songs")
@CrossOrigin(origins = "http://localhost:5173") // 允许来自 5173 端口的请求
public class SongController {
@Autowired
private SongRepository songRepository;
// 获取所有歌曲
@GetMapping
public List<Song> getAllSongs() {
return songRepository.findAll();
}
// 根据ID获取歌曲
@GetMapping("/{id}")
public ResponseEntity<Song> getSongById(@PathVariable Long id) {
return songRepository.findById(id)
.map(song -> ResponseEntity.ok(song))
.orElseGet(() -> ResponseEntity.notFound().build());
}
// 添加歌曲
@PostMapping
public ResponseEntity<Song> addSong(@RequestBody Song song) {
Song savedSong = songRepository.save(song);
return ResponseEntity.status(HttpStatus.CREATED).body(savedSong);
}
// 编辑歌曲
@PutMapping("/{id}")
public ResponseEntity<Song> updateSong(@PathVariable Long id, @RequestBody Song song) {
if (!songRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
song.setId(id);
Song updatedSong = songRepository.save(song);
return ResponseEntity.ok(updatedSong);
}
// 删除歌曲
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteSong(@PathVariable Long id) {
if (!songRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
songRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
}

@ -0,0 +1,136 @@
package com.example.demo.controller;
import com.example.demo.dto.UserLoginResponse;
import com.example.demo.dto.UserUpdateDto;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import jakarta.transaction.Transactional;
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:5173") // 允许来自 5173 端口的请求
public class UserController {
@Autowired
private UserRepository userRepository;
// 加密工具
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 注册用户
@PostMapping("/register")
@Transactional
public ResponseEntity<?> register(@RequestBody User user) {
// 检查用户名是否已存在
if (userRepository.existsByUsername(user.getUsername())) {
return ResponseEntity.badRequest().body("用户名已存在");
}
// 密码加密
String encryptedPassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encryptedPassword);
// 默认角色为 'user'
if (user.getRole() == null || user.getRole().isEmpty()) {
user.setRole("user");
}
// 保存用户信息
userRepository.save(user);
return ResponseEntity.ok("注册成功");
}
// 用户登录
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody User user) {
System.out.println("==============="+user);
User foundUser = userRepository.findByUsername(user.getUsername());
// 验证用户是否存在及密码是否正确
if (foundUser == null || !passwordEncoder.matches(user.getPassword(), foundUser.getPassword())) {
return ResponseEntity.badRequest().body("用户名或密码错误");
}
// 登录成功后返回用户的基本信息(包括角色)
return ResponseEntity.ok(new UserLoginResponse(foundUser.getUsername(), foundUser.getRole()));
}
// 获取所有用户
@GetMapping
public ResponseEntity<?> getAllUsers() {
return ResponseEntity.ok(userRepository.findAll());
}
// 更新用户信息
@PostMapping("update")
public ResponseEntity<?> updateUser(@RequestBody User user) {
System.out.println("=============="+user);
Long id = user.getId();
if (!userRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
// 如果传来的是密码,则进行加密
if (user.getPassword() != null) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
}
user.setId(id);
userRepository.save(user);
return ResponseEntity.ok("用户更新成功");
}
// 删除用户
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteUser(@PathVariable long id) {
if (!userRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
userRepository.deleteById(id);
return ResponseEntity.ok("用户删除成功");
}
// 新增接口:更新管理员信息
@PutMapping("/admin")
public ResponseEntity<?> updateAdmin(@RequestBody UserUpdateDto userUpdateDto) {
System.out.println("============" + userUpdateDto);
// 查找管理员,假设最多一个管理员
User admin = userRepository.findByRole("admin");
if (admin == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("管理员不存在");
}
// 更新允许修改的字段
if (userUpdateDto.getUsername() != null) admin.setUsername(userUpdateDto.getUsername());
if (userUpdateDto.getProfileImage() != null) admin.setProfileImage(userUpdateDto.getProfileImage());
if (userUpdateDto.getGender() != null) admin.setGender(userUpdateDto.getGender());
if (userUpdateDto.getPhone() != null) admin.setPhone(userUpdateDto.getPhone());
if (userUpdateDto.getEmail() != null) admin.setEmail(userUpdateDto.getEmail());
if (userUpdateDto.getBirthday() != null) admin.setBirthday(userUpdateDto.getBirthday());
if (userUpdateDto.getSignature() != null) admin.setSignature(userUpdateDto.getSignature());
if (userUpdateDto.getLocation() != null) admin.setLocation(userUpdateDto.getLocation());
// 如果需要更新密码
if (userUpdateDto.getPassword() != null && !userUpdateDto.getPassword().isEmpty()) {
String encryptedPassword = passwordEncoder.encode(userUpdateDto.getPassword());
admin.setPassword(encryptedPassword);
}
// 保存更新后的管理员信息
userRepository.save(admin);
// 返回更新后的信息
return ResponseEntity.ok(admin); // 返回更新后的管理员信息
}
}

@ -0,0 +1,29 @@
package com.example.demo.dto;
public class UserLoginResponse {
private String username;
private String role;
// 构造函数
public UserLoginResponse(String username, String role) {
this.username = username;
this.role = role;
}
// getters 和 setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}

@ -0,0 +1,17 @@
package com.example.demo.dto;
import lombok.Data;
import java.sql.Date;
@Data
public class UserUpdateDto {
private String username;
private String profileImage;
private String gender;
private String phone;
private String email;
private Date birthday;
private String signature;
private String location;
private String password; // 可选,若需要修改密码
}

@ -0,0 +1,21 @@
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.util.List;
@Data
@Entity
public class Playlist {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 歌单ID
private String name; // 歌单名称
private String description; // 歌单简介
private java.sql.Timestamp createTime; // 创建时间
private java.sql.Timestamp updateTime; // 更新时间
}

@ -0,0 +1,22 @@
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.util.List;
@Data
@Entity
public class Singer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 歌手ID
private String name; // 歌手名称
private String gender; // 性别
private java.sql.Date birthday; // 生日
private String location; // 地区
private String bio; // 歌手简介
private String profileImage; // 歌手图片
}

@ -0,0 +1,23 @@
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.util.List;
@Data
@Entity
public class Song {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 歌曲ID
private String artist; // 歌手名称
private String name; // 歌曲名称
private String title; // 歌曲title
private String genre; // 歌曲类型
private String album; // 专辑名称
private String duration; // 歌曲时长
private String lyrics; // 歌词
}

@ -0,0 +1,89 @@
package com.example.demo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import lombok.Getter;
@Getter
@Data
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String role;
private String profileImage;
private String gender;
private String phone;
private String email;
private java.sql.Date birthday;
private String signature;
private String location;
private String isMember;
private String password;
public void setRole(String role) {
this.role = role;
}
public void setId(long id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setProfileImage(String profileImage) {
this.profileImage = profileImage;
}
public void setGender(String gender) {
this.gender = gender;
}
public void setPhone(String phone) {
this.phone = phone;
}
public void setEmail(String email) {
this.email = email;
}
public void setBirthday(java.sql.Date birthday) {
this.birthday = birthday;
}
public void setSignature(String signature) {
this.signature = signature;
}
public void setLocation(String location) {
this.location = location;
}
public void setIsMember(String isMember) {
this.isMember = isMember;
}
public void setPassword(String password) {
this.password = password;
}
}

@ -0,0 +1,23 @@
package com.example.demo.middle_class;
import com.example.demo.entity.Playlist;
import jakarta.persistence.*;
import lombok.Data;
import java.util.List;
@Data
public class Singervo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 歌手ID
private String name; // 歌手名称
private String gender; // 性别
private java.sql.Date birthday; // 生日
private String location; // 地区
private String bio; // 歌手简介
private String profileImage; // 歌手图片
private List<Playlist> playlists;
}

@ -0,0 +1,16 @@
package com.example.demo.repository;
import com.example.demo.entity.Playlist;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PlaylistRepository extends JpaRepository<Playlist, Long> {
// 根据名称查询歌单
List<Playlist> findByNameContaining(String name);
// 根据更新时间排序(示例:按更新时间降序)
List<Playlist> findAllByOrderByUpdateTimeDesc();
}

@ -0,0 +1,11 @@
package com.example.demo.repository;
import com.example.demo.entity.Singer;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface SingerRepository extends JpaRepository<Singer, Long> {
// 可以定义一些自定义查询方法
List<Singer> findByNameContainingIgnoreCase(String name);
}

@ -0,0 +1,8 @@
package com.example.demo.repository;
import com.example.demo.entity.Song;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SongRepository extends JpaRepository<Song, Long> {
// JpaRepository 提供了基本的增删改查方法
}

@ -0,0 +1,12 @@
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByUsername(String username);
User findByUsername(String username);
User findByRole(String role); // 添加此方法
}

@ -0,0 +1,41 @@
package com.example.demo.service;
import com.example.demo.entity.Playlist;
import com.example.demo.repository.PlaylistRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.sql.Timestamp;
import java.util.List;
@Service
public class PlaylistService {
@Autowired
private PlaylistRepository playlistRepository;
public List<Playlist> getAllPlaylists() {
return playlistRepository.findAll();
}
public Playlist addPlaylist(Playlist playlist) {
playlist.setCreateTime(new Timestamp(System.currentTimeMillis()));
playlist.setUpdateTime(new Timestamp(System.currentTimeMillis()));
return playlistRepository.save(playlist);
}
public Playlist updatePlaylist(Playlist playlist) {
// 确保歌单 ID 已经存在,进行更新
if (playlistRepository.existsById(playlist.getId())) {
return playlistRepository.save(playlist);
} else {
throw new RuntimeException("歌单不存在");
}
}
public void deletePlaylist(Long id) {
playlistRepository.deleteById(id);
}
public void batchDeletePlaylists(List<Long> ids) {
playlistRepository.deleteAllById(ids);
}
}

@ -0,0 +1,14 @@
spring.datasource.url=jdbc:mysql://localhost:3306/music_admin?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# MyBatis ??
mybatis.mapper-locations=classpath:mapper/**/*.xml
mybatis.type-aliases-package=com.example.demo.entity
server.port=8081
debug=true

@ -0,0 +1,13 @@
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
}
Loading…
Cancel
Save