网络请求api的封装
requiest/index.js
import axios from 'axios';
let service = axios.create({
baseURL: 'http://localhost:3000/', //baseURL自动添加在请求地址上
timeout: 3000,
withCredentials: true, //允许跨域
})
export default service
request/home/api.js
import service from '..';
//邮箱登录接口
export function getEmailLogin(user) {
// 此时的service已经配置了baseURL和timeout
return service({
method: 'GET',
url: `/login?email=${user.email}&password=${user.password}`
})
}
//其他接口
export function getXXX(params){
returnservice({
methods:'',
url:`...`
})
}
登录功能
这是第一次真正实现登录功能,所谓的登录,就是前端向后端传递必要参数(用户名,密码)过后,后端返回cookie(token)及其他信息,前端将cookie(token)保存在浏览器localStorage里,此后基于登录的请求头都需要添加上cookie(token),才能合法访问服务器资源。
store/actions.js
import { getEmailLogin } from '@/request/api/home.js'
actions: {
async getLogin(context, value) {
let res = await getEmailLogin(value)
console.log(res);
return res
}
},
login.vue
methods: {
async login() {
//触发store/actions.js中的getLogin函数
let res = await this.$store.dispatch("getLogin", {
email: this.email,
password: this.password,
});
//res为登录返回信息,包括cookie、token、和用户信息
if (res.data.code == 200) {
//说明登陆成功
/*修改登录状态值、设置token、获取用户信息并添加到user、跳转路由到用户中心 */
this.$store.commit("updateIsLogin", true);//修改登录状态值
this.$store.commit("updateToken", res.data.token); //存储token到store里
let userinfo = await getUser(res.data.account.id); //获取用户详细信息,包含userId
//将用户信息保存到store.state.user中
this.$store.commit("updateUser", userinfo);
//跳转到个人中心,并携带userId
this.$router.push({
path: "/default/userinfo",
query: { uid: userinfo.data.profile.userId },
});
} else {
this.$message({
message: "登录失败:" + res.message,
type: "warning",
});
}
},
},
UserInfo.vue
/* 进入用户中心前有路由守卫,进入后需要获取用户详情 */
computed: {
...mapState(["user"]), //未登录状态为{}
},
mounted() {
//复制$store.state.user,若是从登录界面跳转,则user刚得到保存不为空
//若在登录后刷新浏览器,则$store.state.user为空
this.userinfo = this.user;
if (JSON.stringify(this.userinfo) !== "{}") {
// 如果$store.state.user不为空(说明在登录),则将最新用户的信息存储到localStorage中
localStorage.setItem("userInfo", JSON.stringify(this.userinfo)); //obj转json
//同时将登录界面传递的uid路由参数赋值给uid
this.uid = this.$route.query.uid;
} else {
//若为空{},则说明未登录或登陆后浏览器被刷新,则从localStorage取值
this.userinfo = JSON.parse(localStorage.getItem("userInfo")); //json转obj
}
//此时userinfo或从最新的vuex中取到值(从登录界面跳转),
//或从本地lacalStorage取值(其他界面跳转或浏览器刷新过),一定为用户信息
//如果query.uid为undefined,说明不是从登录页面跳转,则从localStorage中取uid
if (this.uid == undefined || this.uid == '' || this.uid == {}) {
this.uid = this.userinfo.data.profile.userId;
console.log("this.uid",this.uid);
}
//此时uid一定为用户id,然后就可以根据uid请求用户其他信息并渲染到页面即可。
},
搜索历史

localStorage实现持久化,存在data函数中实现响应式
//每次进入该页面均从localStorage中获取搜索记录
mounted() {
this.keyWordList = JSON.parse(localStorage.getItem("keyWordList")) || []; //json转对象
},
methods:{
//添加搜索历史
addHistory(searchKeyWord) {
//如果关键词不为空则将关键词添加到keyWordList列表中
if (this.searchKeyWord !== "") {
this.keyWordList.unshift(searchKeyWord);
//去重
let arr = new Set(this.keyWordList); //一个没有重复内容的类数组
this.keyWordList = [...arr]; //将类数组变成数组并赋值给keyWordList
//固定搜索记录长度
if (this.keyWordList.length > 10) {
this.keyWordList.splice(this.keyWordList.length - 1, 1); //从最后一个开始删除一个元素
}
//将搜索记录添加在本地存储中
localStorage.setItem("keyWordList", JSON.stringify(this.keyWordList)); //对象转JSON
}
}
进度条的实现

播放组件(不可见)、slider滑块组件
<audio
ref="audio"
preload="metadata"
:src="`https://music.163.com/song/media/outer/url?id=${id}.mp3`"
></audio>
<!--input事件在拖动滑块时触发,回调参数为实时值;change事件在拖拽结束时触发,参数为拖拽结束位置的值-->
<el-slider
@input="sliderStop"
@change="sliderPlay"
:show-tooltip="false"
:min="0"
:max="duration"
:format-tooltip="(val) => durationFormat(val)"
v-model="currentTime"
@mousedown.native="isChange = true"
@mouseup.native="isChange = false"
></el-slider>
data(){
return{
interVal: 0, //定时器
isChange: false, //是否正在被拖动
}
}
mounted() {
//获取音频时长
setTimeout(() => {
//audio.duration较为特殊,不能直接获取,需要延迟才能拿到该属性
this.$store.commit("updateDuration", this.$refs.audio.duration);
}, 3400);
},
methods:{
//每秒更新一次store里的currentTime值
updateTime() {
this.interVal = setInterval(() => {
this.$store.commit("updateCurrentTime", this.$refs.audio.currentTime);
}, 1000);
},
play() {
if (this.$refs.audio.paused) {
//如果暂停时play则播放
this.$refs.audio.play();
this.updatePlaying(false); //隐藏暂停键
this.updateTime(); //播放就一直更新currentTime
} else {
//如果播放时play则暂停
this.$refs.audio.pause();
this.updatePlaying(true); //显示暂停键
clearInterval(this.interVal); //清除定时器,停止更新currentTime
}
},
//正在被拖动时执行
sliderStop(value) {
if (this.isChange) {
clearInterval(this.interVal); //清除定时器
let audio = this.$refs.audio;
audio.pause(); //暂停播放音乐
audio.currentTime = value; //[1]将audio的currentTime属性设为拖动值
this.$store.commit("updateCurrentTime", value); //[2]防止进度条反弹,快速响应
this.playing = false;
}
},
//拖拽结束时执行
sliderPlay(value) {
this.play();
this.isChange = false;
}
}
面包屑组件
router.js
import Vue from 'vue';
import vueRouter from 'vue-router'
Vue.use(vueRouter)
const routes = [
{ path: '*', name: 'NotFound', component: NotFound, hidden: true },
{
path: '/home',
name: '学生管理',
iconClass: 'fa fa-user',
component: () => import('@/components/Home.vue'),
children: [
{
path: '/home/student',
name: '学生列表',
iconClass: 'fa fa-list',
component: () => import('@/components/students/StudentsList.vue'),
},
{
path: '/home/info',
name: '信息列表',
iconClass: 'fa fa-list-alt',
component: () => import('@/components/students/InfoList.vue'),
},
]
},
]
const router = new vueRouter({
routes,
})
export default router
面包屑公共组件
<el-card>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item
v-for="(item, index) in $route.matched"
:key="index"
>{{ item.name }}</el-breadcrumb-item>
</el-breadcrumb>
</el-card>

其他补充
组件传参:
路由传参
登录页跳转个人中心:
login.vue
login(){
...
this.$router.push({
path: "/default/userinfo",
query: { uid: userinfo.data.profile.userId },
});
//uid为参数
}
个人中心
this.uid = this.$route.query.uid;
父子组件传参
父组件Footer.vue
<!-- 底部播放组件传值给弹出框组件 -->
<el-dialog :visible.sync="dialogVisible" width="30%" :fullscreen="true">
<Song :dt="duration" />
</el-dialog>
子组件Song.vue:
export default {
props: ["dt"],
}
CSS
超长文本用省略号代替

<div class="des" @click="centerDialogVisible = true">
{{ playListInfo.description }}
</div>
<el-dialog
title="歌单介绍"
:visible.sync="centerDialogVisible"
width="50%"
center
>
<div>{{ playListInfo.description }}</div>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="centerDialogVisible = false">
确定
</el-button>
</span>
</el-dialog>
.des {
display: -webkit-box;
text-overflow: ellipsis; /*超出内容用省略号*/
overflow: hidden; /*内容超出后隐藏*/
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; /*2行*/
word-break: break-all;
cursor: pointer;
}
内容横向滚动

<div class="artists">
<div class="artistItem" v-for="(item, index) in topSingers" :key="index">
<router-link :to="{ path: '/singer', query: { id: item.id } }">
<img :src="item.img1v1Url" />
<p>{{ item.name }}</p>
</router-link>
</div>
</div>
<style>
.artists {
width: 100%;
overflow: scroll;
overflow-y: hidden;
overflow-x: auto;
display: flex;
.artistItem {
margin-left: 12px;
img {
border-radius: 50%;
}
text-align: center;
}
}
</style>