1.项目搭建
1.1 项目创建
使用vue.clb脚手架搭建vue2框架
vue create interviewexp -m npm
项目的配置为:
1.2 导入模板
安装vant
npm i vant@latest-v2 --legacy-peer-deps
使用全局方式导入
import Vant from 'vant'
import 'vant/lib/index.css'
Vue.use(Vant)
进行适配
npm i postcss-px-to-viewport@1.1.1 -D --legacy-peer-deps
在根目录下,新建postcss的配置文件:postcss.config.js,里面的配置代码为
// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
// 设计稿如果是2倍图,宽是750,则 750/2 = 375,下面就写375
// 设计稿如果是3倍图,宽是 1080,则 1080/3 = 360,下面就写360
viewportWidth: 375
}
}
}
2.项目设计
2.1 配置路由
在router文件中的index.js配置文件中进行路由配置
const routes = [
{ path: '/register', component: Register },
{ path: '/login', component: Login },
{ path: '/detail', component: Detail },
{
path: '/', component: Home,
redirect: '/article',
children: [
{ path: '/article', component: Article },
{ path: '/collect', component: Collect },
{ path: '/like', component: Like },
{ path: '/user', component: User }
]
},
{ path: '*', component: NotFound }
]
同时在App.vue和Home.vue中设置路由锚点
2.2 页面设计
将页面分为两类,面试经验、收藏、点赞、我的这四个页面有下侧框,属于一类;
登录、注册、详情和main为一类。
2.2.1 axios抽取
使用axios实现前后端的数据的传输。为了解决冗余问题以及携带数据问题,使用axios提供的create方法实现对axios的改造。
//1.引入axios
import axios from "axios";
//2.复制并进行配置
let request = axios.create({
baseURL: 'http://interview-api-t.itheima.net',//基础路径
timeout: 5000,//超时计数器
});
//3.输出
export default request;
2.2.2 用户注册
页面设计
<template>
<div class="register-page">
<!-- 标题 -->
<van-nav-bar title="面经注册" />
<!-- 注册表单 -->
<van-form @submit="onSubmit">
<!--
用户名
v-model 双向绑定
name
label 框前面文字
placeholder 框中提示文字
rules 校验规则
-->
<van-field v-model="username" name="username" label="用户名" placeholder="用户名"
:rules="[{ required: true, message: '请填写用户名' }]" />
<!-- 密码 -->
<van-field v-model="password" type="password" name="password" label="密码" placeholder="密码"
:rules="[{ required: true, message: '请填写密码' }]" />
<!-- 按钮 -->
<div style="margin: 16px;">
<van-button square block type="info" native-type="submit">提交</van-button>
</div>
</van-form>
<!-- 超链接 -->
<router-link class="link" to="/login">有账号,去登录</router-link>
</div>
</template>
<style lang="less" scoped>
.link {
font-size: 13px;
float: right;
margin-right: 20px;
color: dodgerblue;
}
</style>
功能设计:
<script>
import request from "@/utils/request"
export default {
name: 'register-page',
data() {
return {
username: '',
password: ''
}
},
methods: {
async onSubmit(values) {
try {
let { data: res } = await request({
method: 'post',
url: '/h5/user/register',
data: values,
});
console.log(res.data);
this.$toast.success('注册成功');
this.username = this.password = '';
} catch (error) {
if (error.response) {
this.$toast.fail(error.response.data.message);
} else {
this.$toast.fail('注册失败,请联系管理员')
}
}
}
}
}
</script>
2.2.3 用户登录
页面设计:
<template>
<div class="login-page">
<!-- 标题文字 -->
<van-nav-bar title="面经登录" />
<!-- 表单 -->
<van-form @submit="onSubmit">
<van-field v-model="username" name="username" label="用户名" placeholder="用户名"
:rules="[{ required: true, message: '请填写用户名' }]" />
<van-field v-model="password" type="password" name="password" label="密码" placeholder="密码"
:rules="[{ required: true, message: '请填写密码' }]" />
<div style="margin: 16px;">
<van-button square block type="info" native-type="submit">登录</van-button>
</div>
</van-form>
<!-- 超链接 -->
<router-link class="link" to="/register">没有账号, 去注册</router-link>
</div>
</template>
<style lang="less" scoped>
.link {
float: right;
margin-right: 20px;
color: #1989fa;
}
</style>
页面操作:
<script>
import request from '@/utils/request'
export default {
name: 'login-page',
data() {
return {
username: '',
password: '',
data: values,
}
},
methods: {
async onSubmit(values) {
try {
let { data: res } = await request({
method: 'post',
url: '/h5/user/login',
data:values
});
this.$toast.success('登录成功');
console.log(res.data);
localStorage.setItem('mj-token', res.data.token)//存储token
this.username = this.password = '';//将页面转换为原来情况
this.$router.push('/article');//进行跳转
} catch (error) {
if (error.response) {
this.$toast.fail(error.response.$router.data.message);
} else {
this.$toast.fail('登录失败');
}
}
}
}
}
</script>
2.2.4 面经列表
在request.js中添加拦截器请求头,因为只有在登录成功后获取了服务器给的token才能访问个人页面。
// 添加请求拦截器
request.interceptors.request.use(function (config) {
// 添加请求头
config.headers.Authorization = "Bearer "+localStorage.getItem('mj-token');
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
一般执行的操作是查,就先实行js;执行的是增删改,就先设计页面。
<script>
import request from '@/utils/request';
export default {
name: 'article-page',
data() {
return {
current: 1,
pageSize: 5,
sorter: 'weight_desc',
articleList:[]//准备集合接收返回数据
}
},
methods: {
//查询面经列表
async findList() {
try {
let { data: res } = await request({
method: 'get',
url: '/h5/interview/query',
params: {
'current': this.current,
'pageSize': this.pageSize,
'sorter': this.sorter
}
})
this.articleList = res.data.rows;//获取数据列表
console.log(this.articleList);
} catch (err) {
if (err.response) {
this.$toast.fail(err.response.data.message);
} else {
this.$toast.fail('获取列表失败');
}
}
}
},
created() {
//进入页面执行查询列表的方法
this.findList()
}
}
</script>
提取组件,展示列表可以做成一个组件,在components中创建ArticleItem.vue组件
<template>
<van-cell class="article-item">
<template #title>
<div class="head">
<img :src="article.avatar" alt="" />
<div class="con">
<p class="title van-ellipsis">{{ article.stem }}</p>
<p class="other">{{ article.creator }} | {{ article.createdAt }}</p>
</div>
</div>
</template>
<template #label>
<div class="body van-multi-ellipsis--l2">
{{ removeHTMLTags(article.content) }}
</div>
<div class="foot">点赞 {{ article.likeCount }} | 浏览 {{ article.views }}</div>
</template>
</van-cell>
</template>
<script>
export default {
//接收父组件传入的变量
props: ['article'],
methods: {
//去除字符串中的html标签
removeHTMLTags(htmlString) {
return htmlString.replace(/<[^>]+>/g, '');
}
}
}
</script>
<style lang="less" scoped>
.article-item {
.head {
display: flex;
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.con {
flex: 1;
overflow: hidden;
padding-left: 10px;
p {
margin: 0;
line-height: 1.5;
&.title {
width: 280px;
}
&.other {
font-size: 10px;
color: #999;
}
}
}
}
.body {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-top: 10px;
}
.foot {
font-size: 12px;
color: #999;
margin-top: 10px;
}
}
</style>
将组件声明为全局组件
import ArticleItem from '@/components/ArticleItem.vue'
Vue.component('ArticleItem', ArticleItem)
最终的article页面代码:
<template>
<div class="article-page">
<nav class="my-nav van-hairline--bottom">
<a href="javascript:;">推荐</a>
<a href="javascript:;">最新</a>
<div class="logo"><img src="@/assets/logo.png" alt=""></div>
</nav>
<!--
van-list 分页列表组件
loading: false, 是否正在加载数据, false:没有在加载,此时就可以调用findList方法, 一旦开始调用了,组件会立即将loading设置为true, 表示正在加载, 此时无法进行二次加载
finished: false, 表示数据是否全部加载完, false: 没加载完, 还可以继续加载下一页; true, 表示没有可以加载的数据了, 就不能再加载下一页了
@load="查询数据时候要调用的方法"
-->
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="findList">
<!-- 使用组件 -->
<article-item v-for="article in articleList" :article="article" :key="article.id"></article-item>
</van-list>
</div>
</template>
<script>
import request from '@/utils/request';
export default {
name: 'article-page',
data() {
return {
loading: false, //是否正在加载数据, false:没有在加载,此时就可以调用findList方法, 一旦开始调用了,组件会立即将loading设置为true, 表示正在加载, 此时无法进行二次加载
finished: false,//表示数据是否全部加载完, false: 没加载完, 还可以继续加载下一页; true, 表示没有可以加载的数据了, 就不能再加载下一页了
//推荐数据中的前5条
current: 1,//默认第一页
pageSize: 10, //每页5条
sorter: 'weight_desc', // weight_desc: 推荐数据 null: 最新的数据
articleList: [],//准备集合接收返回数据
}
},
methods: {
//查询面经列表
async findList() {
try {
let { data: res } = await request({
method: 'get',
url: '/h5/interview/query',
params: {
'current': this.current,
'pageSize': this.pageSize,
'sorter': this.sorter
}
})
//不能再使用=复制,而是每次查询回来的数据追加到集合中
this.articleList.push(...res.data.rows);
console.log(this.articleList);
//本次加载已经完毕, 后续可以进行下一次加载了
this.loading = false;
//将页码+1
this.current++;
//当页码已经超过了最大页码, 不能继续加载了
if(this.current > res.data.pageTotal){
this.finished = true
}
} catch (err) {
console.log(err);
if (err.response) {
this.$toast.fail(err.response.data.message);
} else {
this.$toast.fail('面经列表查询失败,请联系管理员');
}
}
}
},
// created() {
// //页面加载完成之后,获取面经列表数据
// this.findList();
// }
}
</script>
2.2.4 页面详情
页面设计
<template>
<div class="detail-page">
<van-nav-bar left-text="返回" @click-left="$router.back()" fixed title="面经详细" />
<header class="header">
<h1>{{ article.stem }}</h1>
<p>
创建时间:{{ article.createdAt }} | 浏览量:{{ article.views }} | 点赞数:{{ article.likeCount }}
</p>
<p>
<img :src="article.avatar" alt />
<span>{{ article.creator }}</span>
</p>
</header>
<main class="body" v-html="article.content"></main>
<div class="opt">
<van-icon name="like-o" :class="{ active: article.likeFlag }" />
<van-icon name="star-o" :class="{ active: article.collectFlag }" />
</div>
</div>
</template>
<style lang="less" scoped>
.detail-page {
margin-top: 44px;
overflow: hidden;
padding: 0 15px;
.header {
h1 {
font-size: 24px;
}
p {
color: #999;
font-size: 12px;
display: flex;
align-items: center;
}
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
}
.opt {
position: fixed;
bottom: 100px;
right: 0;
>.van-icon {
margin-right: 20px;
background: #fff;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 50%;
box-shadow: 2px 2px 10px #ccc;
font-size: 18px;
&.active {
background: #fec635;
color: #fff;
}
}
}
}
</style>
操作:
<script>
import request from '@/utils/request';
export default {
name: 'detail-page',
data() {
return {
article: {},//接受远程服务器传来的对象
}
},
methods: {
findById() {
try {
let { data: res } = request({
method: 'get',
url: '/h5/interview/show',
params: {
'id': this.id
}
});
console.log(res);
this.article = res.data;
} catch (error) {
console.log(err);
if (err.response) {
this.$toast.fail(err.response.data.message);
} else {
this.$toast.fail('获取面经详情失败,请重试');
}
}
}
},
created() {
this.findById();
}
}
</script>
点赞和收藏:
<template>
<div class="detail-page">
.......省略
<div class="opt">
<van-icon name="like-o" :class="{ 'active': article.collectFlag }" @click="opt(2)" />
<van-icon name="star-o" :class="{ 'active': article.likeFlag }" @click="opt(1)" />
</div>
</div>
</template>
//js中的代码
async opt(optType) {
try {
let { data: res } = await request({
method: 'post',
url: '/h5/interview/opt',
data: {
'id': this.article.id ,
'optType':optType
}
})
console.log(res);
//执行操作成功
this.$toast.success('操作成功');
//重新查询一次
this.findById();
} catch (err) {
console.log(err);
if (err.response) {
this.$toast.fail(err.response.data.message);
} else {
this.$toast.fail('点赞/收藏失败,请重试');
}
}
},
2.2.5 个人中心
<template>
<div class="user-page">
<div class="user">
<img :src="user.avatar" alt />
<h3>{{ user.username }}</h3>
</div>
<van-grid clickable :column-num="3" :border="false">
<van-grid-item icon="clock-o" text="历史记录" to="/" />
<van-grid-item icon="bookmark-o" text="我的收藏" to="/collect" />
<van-grid-item icon="thumb-circle-o" text="我的点赞" to="/like" />
</van-grid>
<van-cell-group class="mt20">
<van-cell title="推荐分享" is-link />
<van-cell title="意见反馈" is-link />
<van-cell title="关于我们" is-link />
<van-cell title="退出登录" is-link @click="logout()" />
</van-cell-group>
</div>
</template>
<script>
import request from '@/utils/request';
export default {
name: 'user-page',
data() {
return {
//个人信息
user: {}
}
},
methods: {
//退出
logout() {
//清理token
localStorage.removeItem('mj-token');
//跳转到登录页面
this.$router.push("/login")
},
//查询个人信息
async findUserInfo() {
try {
let { data: res } = await request({
method: 'get',
url: '/h5/user/currentUser',
})
//获取返回结果中的个人信息赋值到user
this.user = res.data;
} catch (err) {
console.log(err);
if (err.response) {
this.$toast.fail(err.response.data.message);
} else {
this.$toast.fail('个人信息查询失败,请重试');
}
}
}
},
created() {
//调用查询个人信息
this.findUserInfo()
}
}
</script>
<style lang="less" scoped>
.user-page {
padding: 0 10px;
background: #f5f5f5;
height: 100vh;
.mt20 {
margin-top: 20px;
}
.user {
display: flex;
padding: 20px 0;
align-items: center;
img {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
}
h3 {
margin: 0;
padding-left: 20px;
font-size: 18px;
}
}
}
</style>
2.2.5 我的收藏
<template>
<div class="collect-page">
<!-- 标题文字 -->
<van-nav-bar title="我的收藏" />
<!-- 收藏列表 -->
<!-- 分页组件
loading变量:控制是否显示加载中按钮 true:显示提示,不能再执行加载操作 false:不显示,可以再执行加载操作
@load="获取数据的方法"
-->
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="findList">
<article-item v-for="article in articleList" :article="article" :key="article.id"></article-item>
</van-list>
</div>
</template>
<script>
import request from '@/utils/request'
export default {
name: 'collect-page',
data() {
return {
loading: false,
finished: false,
current: 1,
pageSize: 5,
articleList: []//准备集合接收返回数据
}
},
methods: {
//查询面经列表
async findList() {
try {
let { data: res } = await request({
method: 'get',
url: '/h5/interview/opt/list',
params: {
'page': this.current,
'pageSize': this.pageSize,
'optType': 2
}
})
this.articleList.push(...res.data.rows);//获取数据列表,追加到list
//去除加载中字样
this.loading = false;
//页码+1
this.current++;
//如果加载完毕
if (this.current > res.data.pageTotal) {
this.finished = true;
}
} catch (err) {
console.log(err);
if (err.response) {
this.$toast.fail(err.response.data.message);
} else {
this.$toast.fail('获取列表失败');
}
}
}
}
}
</script>
<style lang="less" scoped>
.collect-page {
margin-bottom: 50px;
}
</style>
2.2.6 我的点赞
<template>
<div class="collect-page">
<!-- 标题 -->
<van-nav-bar title="我的点赞" />
<!--
van-list 分页列表组件
loading: false, 是否正在加载数据, false:没有在加载,此时就可以调用findList方法, 一旦开始调用了,组件会立即将loading设置为true, 表示正在加载, 此时无法进行二次加载
finished: false, 表示数据是否全部加载完, false: 没加载完, 还可以继续加载下一页; true, 表示没有可以加载的数据了, 就不能再加载下一页了
@load="查询数据时候要调用的方法"
-->
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="findList">
<!-- 使用组件 -->
<article-item v-for="article in articleList" :article="article" :key="article.id"></article-item>
</van-list>
</div>
</template>
<script>
import request from '@/utils/request';
export default {
name: 'like-page',
data() {
return {
loading: false, //是否正在加载数据, false:没有在加载,此时就可以调用findList方法, 一旦开始调用了,组件会立即将loading设置为true, 表示正在加载, 此时无法进行二次加载
finished: false,//表示数据是否全部加载完, false: 没加载完, 还可以继续加载下一页; true, 表示没有可以加载的数据了, 就不能再加载下一页了
optType: 1,
pageSize: 10,
page: 1,
articleList: []
}
},
methods: {
//查询面经列表
async findList() {
try {
let { data: res } = await request({
method: 'get',
url: '/h5/interview/opt/list',
params: {
'page': this.page,
'pageSize': this.pageSize,
'optType': this.optType
}
})
//不能再使用=复制,而是每次查询回来的数据追加到集合中
this.articleList.push(...res.data.rows);
console.log(this.articleList);
//本次加载已经完毕, 后续可以进行下一次加载了
this.loading = false;
//将页码+1
this.page++;
//当页码已经超过了最大页码, 不能继续加载了
if (this.page > res.data.pageTotal) {
this.finished = true
}
} catch (err) {
console.log(err);
if (err.response) {
this.$toast.fail(err.response.data.message);
} else {
this.$toast.fail('点赞列表查询失败,请联系管理员');
}
}
}
},
}
</script>
<style lang="less" scoped>
.collect-page {
margin-bottom: 50px;
}
</style>
2.2.7 响应拦截器
import router from "@/router";
request.interceptors.response.use(function (response) {
// 响应码2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
if (error.response) {
if (error.response.status === 401) {
//移除token
localStorage.removeItem('mj-token')
//跳转到登录页面
router.push('/login')
}
}
return Promise.reject(error)
})
2.2.8 主题定制
引入less
import Vant from 'vant'
在vue.config.js文件中进行配置
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
css: {
loaderOptions: {
less: {
// 若 less-loader 版本小于 6.0,请移除 lessOptions 这一级,直接配置选项。
lessOptions: {
modifyVars: {
// 直接覆盖变量
blue: '#FA6D1D',
// 'text-color': '#111',
// 'border-color': '#eee'
// 或者可以通过 less 文件覆盖(文件路径为绝对路径)
// hack: 'true; @import "your-less-file-path.less";'
}
}
}
}
}
})
3.注:
个人中心中仅实现用户名和用户头像以及退出登录功能。
源代码链接:https://pan.baidu.com/s/1pWfAASzsf2pOu0d88vxY0Q?pwd=vje2
提取码:vje2
--来自百度网盘超级会员V5的分享