音乐网站实战
文章目录
1、vue工程搭建
电脑已配置过vue-cli,cnpm,vue2.x
在需要创建vue工程的目录执行命令行vue init webpack
然后依次填写项目名称及一系列配置,不要用npm下载node_modules,一会用自己的cnpm下载快一点。
cnpm install,cnpm run dev启动项目。
2、引入模块
2.1、Element-UI
cnpm install element-ui --save
在main.js中添加下面的代码
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI);
这样每个vue页面都能使用了。
2.2、stylus
cnpm install stylus stylus-loader --save-dev
使用:
<style scoped lang="stylus">
</style>
报错的话,把"stylus-loader": "^3.0.2"降级。
2.3、清除默认样式
在main.js中导入
//清除默认样式
import './assets/css/base.css'
/**
* Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
* http://cssreset.com
*/
html,body{
width: 100%;
height: 100%;
}
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video,
input {
margin: 0;
padding: 0;
border: 0;
/* font-size: 100%; */
font-weight: normal;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
font-family: "Microsoft YaHei";
font-size: 16px;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* custom */
a {
color: #7e8c8d;
text-decoration: none;
-webkit-backface-visibility: hidden;
}
li {
list-style: none;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track-piece {
background-color: rgba(0, 0, 0, 0.2);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical {
height: 5px;
background-color: rgba(125, 125, 125, 0.7);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal {
width: 5px;
background-color: rgba(125, 125, 125, 0.7);
-webkit-border-radius: 6px;
}
html,
body {
width: 100%;
}
body {
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
3、登录
环境:jdk1.8、springboot2.4.4和vue2.x和MySQL8
3.1、后端
设计数据库,先设计好admin表,id,username,password即可。
导入所需依赖
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!--devtools-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
首先需要热加载,导入 spring-boot-devtools
,然后在IDEA中配置相关属性。
参考CSDN:https://blog.csdn.net/qq_16148137/article/details/99694566
编写applciation.yml
server:
port: 8088
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/music?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
type-aliases-package: com.ljh.pojo
mapper-locations: classpath:mapper/*.xml
端口号和vue冲突,这里先改为8088
spring原生jdbc配置,也可以用druid
新建resources->mapper
配置type-aliases-package
和mapper-locations
,这一步千万别忘!!!
接下来就是pojo、mapper、service、controller、config的依次编写
pojo实体类
/**
* 管理员表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Admin {
private Integer id;
private String username;
private String password;
}
mapper接口
@Repository
@Mapper
public interface AdminMapper {
/**
* 通过name和password查询返回多少个数值
* @return
*/
int getAdminByName(String username,String password);
}
resources->mapper的mapper实现类
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace绑定一个Mapper-->
<mapper namespace="com.ljh.mapper.AdminMapper">
<select id="getAdminByName" resultType="int">
select count(*) from admin where username = #{username} and password = #{password}
</select>
</mapper>
AdminService
public interface AdminService {
/**
* 根据name查询数据库看是否存在
* @param username
* @return
*/
boolean getAdminByName(String username,String password);
}
AdminServiceImpl
@Service
public class AdminServiceImpl implements AdminService {
@Autowired
AdminMapper adminMapper;
@Override
public boolean getAdminByName(String username,String password) {
//返回结果大于0,则代表name真实存在于数据库
return adminMapper.getAdminByName(username,password)>0;
}
}
先配置常量类utils->Constants
/**
* 常量
*/
public class Constants {
//返回结果code
public static final String CODE = "code";
//返回信息
public static final String MSG = "msg";
}
LoginController
@RestController
public class LoginController {
@Autowired
private AdminService adminService;
@PostMapping("/admin/login")
public Object login(HttpServletRequest request, HttpSession session) {
JSONObject jsonObject = new JSONObject();
String username = request.getParameter("username");
String password = request.getParameter("password");
boolean flag = adminService.getAdminByName(username, password);
if (flag) {
jsonObject.put(Constants.CODE, 1);
jsonObject.put(Constants.MSG,"登录成功");
session.setAttribute("name",username);
return jsonObject;
}
jsonObject.put(Constants.CODE, 0);
jsonObject.put(Constants.MSG,"用户名或密码不正确!");
return jsonObject;
}
}
前端通过传递过去的code和msg判断是否登录成功。
因为是前后端分离,所以需要解决跨域问题
@Configuration
public class CrossOriginConfig implements WebMvcConfigurer{
@Override
public void addCorsMappings(CorsRegistry registry) {
/**
* 允许跨域,参数
* addMapping:配置可以被跨域的路径,可以任意配置,可以具体到直接请求路径。
* allowedOrigins:允许所有的请求域名访问我们的跨域资源,可以固定单条或者多条内容,如:"http://www.baidu.com",只有百度可以访问我们的跨域资源。
* allowCredentials: 响应头表示是否可以将对请求的响应暴露给页面。返回true则可以,其他值均不可以
* allowedMethods:允许输入参数的请求方法访问该跨域资源服务器,如:POST、GET、PUT、OPTIONS、DELETE等。
* allowedHeaders:允许所有的请求header访问,可以自定义设置任意请求头信息,如:"X-YAUTH-TOKEN"
* maxAge:配置客户端缓存预检请求的响应的时间(以秒为单位)。默认设置为1800秒(30分钟)。
*
*/
// 2.4.*版本解决跨域问题
// 当allowCredentials是true时,allowedOrigins不能为" * ",切记切记!
registry.addMapping("/**")
.allowedOrigins("http://localhost:8081")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.allowedHeaders("*")
.maxAge(3600);
}
}
springboot版本不同,解决跨域问题代码也不一样。2.4.x版本之后当allowCredentials是true时,allowedOrigins不能为" * ",直接写vue的地址。
3.2、前端
前端用的是vue脚手架和element-ui
后台登录页面
<template>
<div class="login-wrap">
<div class="ms-login">
<div class="ms-title">后台管理系统</div>
<el-form status-icon :model="param" :rules="rules" ref="login" label-width="0px" class="ms-content">
<el-form-item prop="username">
<el-input name="username"
v-model="param.username"
placeholder="username"
@keyup.enter.native="keyupClick()">
<!-- <template slot="prepend"></template> -->
<!-- 下面的代码#prepend就是slot="prepend"的简写 -->
<!-- 通过 slot 来指定在 input 中前置或者后置内容。-->
<!-- prepend是前置,append是后置 -->
<template #prepend>
<el-button icon="el-icon-user"></el-button>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
name="password"
placeholder="password"
v-model="param.password"
ref="password"
@keyup.enter.native="submitForm()"
>
<template #prepend>
<el-button icon="el-icon-lock"></el-button>
</template>
</el-input>
</el-form-item>
<div class="login-btn">
<el-button type="primary" @click="submitForm()">登录</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script>
import {getLoginStatus} from "../api/index";
export default {
data() {
return {
param: {
username: "",
password: ""
},
rules: {
username: [
{required: true, message: "请输入用户名", trigger: "blur"}
],
password: [
{required: true, message: "请输入密码", trigger: "blur"}
]
}
};
},
methods: {
keyupClick() {
this.$refs.password.focus();
},
submitForm() {
this.$refs.login.validate(validate=>{
if(validate) {
let params = new URLSearchParams();
params.append("username", this.param.username);
params.append("password", this.param.password);
getLoginStatus(params).then(res => {
if (res.code === 1) {
this.$router.push("/home");
this.$message({
message:res.msg,
type:'success'
})
}else{
this.$message({
message:res.msg,
type:'error'
})
}
})
}
});
}
}
};
</script>
<style scoped>
.login-wrap {
position: relative;
width: 100%;
height: 100%;
background-image: url(../assets/image/login-bg.jpg);
background-size: 100%;
}
.ms-title {
width: 100%;
line-height: 50px;
text-align: center;
font-size: 20px;
color: #aaa;
border-bottom: 1px solid #ddd;
}
.ms-login {
position: absolute;
left: 50%;
top: 50%;
width: 350px;
margin: -190px 0 0 -175px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.7);
overflow: hidden;
}
.ms-content {
padding: 30px 30px;
}
.login-btn {
text-align: center;
}
.login-btn button {
width: 100%;
height: 36px;
margin-bottom: 10px;
}
</style>
要保证width和height占满整个屏幕,必须设置html和body的width和height都为100%
总体使用element-ui的布局和方法,比较方便。
说明: status-icon属性为输入框添加了表示校验结果的反馈图标
:model="param"绑定data数据
:rules="rules"制定规则,在data中自定义规则即可
Form-Item中的prop属性设置需要校验的字段名
@keyup.enter.native="keyupClick()"就是在输完username后,按下回车键执行方法,这个方法就是聚焦到密码框上(因为密码框上设置了ref=“password”)
密码上的 @keyup.enter.native 就是回车提交表单
this.$refs.login.validate(validate=>{}) 返回的validate就是看用户名和密码是否为空,任意一个为空就返回false,全有值才为true。
this.$message({message:res.msg, type:‘error’}) 就是element-ui的弹窗提示,按类型弹窗。
封装axios
http.js
import axios from 'axios';
axios.defaults.timeout = 5000;//超时时间5s
axios.defaults.withCredentials = true;//允许跨域
//Content-Type 响应头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
//基础url
axios.defaults.baseURL = "http://localhost:8088";
//响应拦截器
axios.interceptors.response.use(response=>{
if(response.status === 200) {
return Promise.resolve(response);
}else{
return Promise.reject(response);
}
},error=>{
if(error.response.status){
switch(error.response.status){
case 401: //未登录
router.replace('/');
break;
case 404: //没找到
break;
}
return Promise.reject(error.response);
}
});
/**
* 封装get方法
*/
export function get(url,params={}){
return new Promise((resolve,reject) => {
axios.get(url,{params:params})
.then(response =>{
resolve(response.data);
})
.catch(err =>{
reject(err);
})
});
}
/**
* 封装post方法
*/
export function post(url,data={}){
return new Promise((resolve,reject) => {
axios.post(url,data)
.then(response =>{
resolve(response.data);
})
.catch(err =>{
reject(err);
})
});
}
编写index.js与后台交互
import {get,post} from './http';
// 判断管理员是否登录成功
export const getLoginStatus = (params)=>post(`admin/login`,params);
编写路由
import Vue from 'vue'
import Router from 'vue-router'
//一级路由
import Login from "./../pages/Login"
import Home from "../components/Home"
// 二级路由
import TheHeader from "./../components/TheHeader"
import TheAside from "./../components/TheAside"
Vue.use(Router);
//下面一定要写component
export default new Router({
routes: [
{
path:'/',
component:Login
},
{
path:'/home',
component:Home,
}
]
})
注意:在vue单页面中要用components
,而在配置路由则需要component
4、管理员页面
4.1、侧边菜单
TheAside.vue
<template>
<div class="sidebar">
<!--el-menu的mode 模式默认为vertical,即为垂直分布-->
<!--default-active:当前激活菜单的index-->
<!--collapse:是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)-->
<!--router:是否使用vue-router的模式,启用该模式会在激活导航时以index作为path进行路由跳转-->
<el-menu class="sidebar-el-menu"
:default-active="activeIndex"
:collapse="collapse"
background-color="#334256"
text-color="#ffffff"
active-text-color="#20a0ff"
router
>
<template v-for="item in items">
<template>
<el-menu-item :index="item.index" :key="item.index">
<i :class="item.icon"></i>
<span>{{item.title}}</span>
</el-menu-item>
</template>
</template>
</el-menu>
</div>
</template>
<script>
import bus from "./../assets/js/bus"
export default {
name: "TheAside",
data() {
return{
collapse: false,
items:[
{
icon: 'el-icon-document',
index: 'info',
title: '系统首页'
},
{
icon: 'el-icon-user',
index: 'consumer',
title: '用户管理'
},
{
icon: 'el-icon-document',
index: 'singer',
title: '歌手管理'
},
{
icon: 'el-icon-document',
index: 'songlist',
title: '歌单管理'
}
]
}
},
computed:{
//获取当前激活菜单的index
activeIndex() {
//this.$route.path格式为:/home/info
return this.$route.path.replace("/home/","");
}
},
created() {
bus.$on("collapse",value=>{
this.collapse = value;
})
},
methods:{
}
}
</script>
<style scoped>
/*页面内容禁止选中,仅输入框和文本域可选*/
* {
-webkit-touch-callout: none; /*系统默认菜单被禁用*/
-webkit-user-select: none; /*webkit浏览器*/
/*-khtml-user-select: none;*/ /*早期浏览器*/
-moz-user-select: none; /*火狐*/
-ms-user-select: none; /*IE10*/
user-select: none;
}
input {
-webkit-user-select: auto; /*webkit浏览器*/
}
textarea {
-webkit-user-select: auto; /*webkit浏览器*/
}
/*上面的代码为了页面内容禁止选中*/
.sidebar{
display: block;
position: absolute;
left: 0;
top: 70px;
bottom: 0;
background: #334256;
}
/*解决el-menu菜单收缩部分边框突出问题,需要加下面的代码*/
.sidebar-el-menu:not(.el-menu--collapse){
width: 150px;
}
.sidebar >ul {
height: 100%;
}
</style>
主要是运用el-menu和el-menu-item并配置相关属性。
这里引入的bus是用于两个组件之间进行数据传递
解决el-menu菜单收缩部分边框突出问题,上面的代码是解决方案。
如果在this.$router.push(“xxx”)刷新同一个值时,会报错。
解决方案就是在配置路由的index.js中添加下列代码:
// 解决ElementUI导航栏中的vue-router在3.0版本以上重复点菜单报错问题
const originalPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
};
4.2、头部页面
TheHeader.vue
<template>
<div class="header">
<div class="collapse-btn" @click="collapseChange">
<i class="el-icon-menu"></i>
</div>
<div class="logo">music后台管理</div>
<div class="header-right">
<div class="btn-fullscreen" @click="handleFullScreen">
<!--使用content属性来决定hover时的提示信息。由placement属性决定展示效果-->
<!--placement为bottom时,提示信息在该图标的正下方显示-->
<el-tooltip :content="fullscreen?`取消全屏`:`全屏`" placement="bottom">
<i class="el-icon-rank"></i>
</el-tooltip>
</div>
<div class="user-avator">
<img src="../assets/image/user.jpg" alt="管理员头像"/>
</div>
<!--@command="handleCommand"点击菜单项触发的事件回调,会有一个参数command-->
<!--在下面定义command="xxx",可以在方法中得到-->
<el-dropdown class="user-name" trigger="click" @command="handleCommand">
<span class="el-dropdown-link">
{{userName}}
<i class="el-icon-caret-bottom"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import bus from "./../assets/js/bus"
export default {
name: "TheHeader",
data() {
return {
collapse: false,
fullscreen: false
}
},
methods: {
//折叠侧边菜单
collapseChange() {
this.collapse = !this.collapse;
bus.$emit("collapse", this.collapse);
},
//全屏事件,适配各种浏览器
handleFullScreen(){
if(this.fullscreen){
if(document.exitFullscreen){
document.exitFullscreen();
}else if(document.webkitCancelFullScreen){ //safari 、Chrome
document.webkitCancelFullScreen();
}else if (document.mozCancelFullScreen){ //firefox
document.mozCancelFullScreen();
}else if(document.msExitFullScreen){ //ie
document.msExitFullScreen();
}
}else{
let element = document.documentElement;
if(element.requestFullscreen){
element.requestFullscreen();
}else if(element.webkitRequestFullScreen){ //safari 、Chrome
element.webkitRequestFullScreen();
}else if(element.mozRequestFullScreen){ //firefox
element.mozRequestFullScreen();
}else if (element.msRequestFullScreen){ //ie
element.msRequestFullScreen();
}
}
this.fullscreen = !this.fullscreen;
},
//退出登录
handleCommand(command) {
if (command === 'logout') {
//暂时不清空session
localStorage.removeItem("userName");
this.$router.push("/");
}
}
},
computed: {
//从本地缓存中拿到username
userName() {
return localStorage.getItem("userName");
}
}
}
</script>
<style scoped>
.header {
position: relative;
background-color: #253041;
box-sizing: border-box;
width: 100%;
height: 70px;
font-size: 22px;
color: #ffffff;
}
.collapse-btn {
float: left;
padding: 0 21px;
cursor: pointer;
line-height: 70px;
}
.header .logo {
float: left;
line-height: 70px;
}
.header-right {
float: right;
padding-right: 50px;
display: flex;
height: 70px;
align-items: center;
}
.btn-fullscreen {
transform: rotate(45deg);
margin-right: 5px;
font-size: 24px;
cursor: pointer;
}
.user-avator {
margin-left: 20px;
}
.user-avator img {
display: block;
width: 40px;
height: 40px;
border-radius: 50%;
}
.user-name {
margin-left: 10px;
}
.el-dropdown-link {
color: #ffffff;
cursor: pointer;
}
</style>
el-tooltip是提示信息的,el-dropdown是下拉菜单,里面有el-dropdown-menu和el-dropdown-item,具体的里面也有相应的注释。
bus.js
/**
* 在需要传值的两个组件引入bus
* 一个组件设置点击事件,触发methods,在方法中执行bus.$emit("xxx",要传递的值);
* 在另一个组件的created周期中执行
* bus.$on("xxx",value=>{
this.值 = value; 接受value值
})
*/
import Vue from 'vue'
// 使用 Event Bus
const bus = new Vue();
export default bus
4.3、整合页面
Home.vue
<template>
<div>
<the-header></the-header>
<the-aside></the-aside>
<!--动态添加content-collapse,作用使其在关闭侧边菜单时宽度变化,让右边内容靠拢-->
<div class="view-wrapper" :class="{'content-collapse':collapse}">
<router-view></router-view>
</div>
</div>
</template>
<script>
import TheHeader from "./TheHeader"
import TheAside from "./TheAside"
//解决侧边菜单收缩,右边内容不自动靠拢
import bus from "./../assets/js/bus"
export default {
name: "Home",
data() {
return{
collapse:false
}
},
components: {
TheHeader,
TheAside
},
created() {
bus.$on("collapse",value=>{
this.collapse = value;
})
}
}
</script>
<style scoped>
.view-wrapper {
position: absolute;
left: 150px;
right: 0;
bottom: 0;
top: 70px;
overflow-y: scroll;
-webkit-transition: left .3s ease-in-out;
transition: left .3s ease-in-out;
background: #f0f0f0;
}
.content-collapse {
left: 65px;
}
</style>
解决侧边菜单收缩,由于他还占有位置,所以右边内容不自动靠拢
解决方案就是动态添加一个样式,用来动态改变左边距就ok
前提是还需要引入bus.js,并动态改变collapse的值
4.4、路由配置
index.js
import Vue from 'vue'
import Router from 'vue-router'
// 解决ElementUI导航栏中的vue-router在3.0版本以上重复点菜单报错问题
const originalPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
};
//一级路由
import Login from "./../pages/Login"
import Home from "../components/Home"
// 二级路由
import InfoPage from "./../pages/InfoPage"
import ConsumerPage from "./../pages/ConsumerPage"
import SingerPage from "./../pages/SingerPage"
import SongListPage from "./../pages/SongListPage"
Vue.use(Router);
//下面一定要写component
/* 一级路由加"/",二级路由不加"/" */
export default new Router({
routes: [
{
path: '/',
component: Login
},
{
path: '/home',
component: Home,
children: [
{
path: '',
redirect: 'info'
},
{
path: 'info',
component: InfoPage
},
{
path: 'consumer',
component: ConsumerPage
},
{
path: 'singer',
component: SingerPage
},
{
path: 'songlist',
component: SongListPage
}
]
}
]
})