系列文章目录

​​Vue基础篇一:编写第一个Vue程序​​Vue基础篇二:Vue组件的核心概念
Vue基础篇三:Vue的计算属性与侦听器
Vue基础篇四:Vue的生命周期(秒杀案例实战)
Vue基础篇五:Vue的指令
Vue基础篇六:Vue使用JSX进行动态渲染
Vue提高篇一:使用Vuex进行状态管理
Vue提高篇二:使用vue-router实现静态路由
Vue提高篇三:使用vue-router实现动态路由
Vue提高篇四:使用Element UI组件库
Vue提高篇五:使用Jest进行单元测试
Vue提高篇六: 使用Vetur+ESLint+Prettier插件提升开发效率
Vue实战篇一: 使用Vue搭建注册登录界面
Vue实战篇二: 实现邮件验证码发送
Vue实战篇三:实现用户注册
Vue实战篇四:创建多步骤表单
Vue实战篇五:实现文件上传
Vue实战篇六:表格渲染动态数据
Vue实战篇七:表单校验
Vue实战篇八:实现弹出对话框进行交互
Vue实战篇九:使用省市区级联选择插件
Vue实战篇十:响应式布局
Vue实战篇十一:父组件获取子组件数据的常规方法
Vue实战篇十二:多项选择器的实际运用
Vue实战篇十三:实战分页组件
Vue实战篇十四:前端excel组件实现数据导入
Vue实战篇十五:表格数据多选在实际项目中的技巧
Vue实战篇十六:导航菜单
Vue实战篇十七:用树型组件实现一个知识目录
Vue实战篇十八:搭建一个知识库框架
Vue实战篇十九:使用printjs打印表单
Vue实战篇二十:自定义表格合计
Vue实战篇二十一:实战Prop的双向绑定
Vue实战篇二十二:生成二维码
Vue实战篇二十三:卡片风格与列表风格的切换
Vue实战篇二十四:分页显示
Vue实战篇二十五:使用ECharts绘制疫情折线图
Vue实战篇二十六:创建动态仪表盘
Vue实战篇二十七:实现走马灯效果的商品轮播图
Vue实战篇二十八:实现一个手机版的购物车
Vue实战篇二十九:模拟一个简易留言板
Vue项目实战篇一:实现一个完整的留言板(带前后端源码下载)
Vue实战篇三十:实现一个简易版的头条新闻
Vue实战篇三十一:实现一个改进版的头条新闻
Vue实战篇三十二:实现新闻的无限加载
Vue实战篇三十三:实现新闻的浏览历史
ue实战篇三十四:给新闻WebApp加入模拟注册登录功能

文章目录

  • ​​系列文章目录​​
  • ​​一、背景​​
  • ​​二、准备新闻数据接口​​
  • ​​2.1 获取新闻频道​​
  • ​​2.2 获取新闻​​
  • ​​2.3 编写前端Api接口​​
  • ​​三、WebApp设计​​
  • ​​3.1 新闻频道栏组件​​
  • ​​3.2 底部导航条组件​​
  • ​​3.2.1 封装底层的TabBarItem组件​​
  • ​​3.2.2 实现底部导航条TabBar​​
  • ​​3.2.3 在主组件App.vue中加入底部导航条​​
  • ​​3.3 设计新闻列表页面​​
  • ​​3.3.1 页面组成​​
  • ​​3.3.2 引入无限滚动组件​​
  • ​​3.3.3 动态加载新闻事件​​
  • ​​3.3.4 新闻列表页面源码与效果演示​​
  • ​​3.4 新闻阅读组件​​
  • ​​3.5 我的页面制作​​
  • ​​3.5.1 添加登录状态的状态管理器​​
  • ​​3.5.2 编写登录表单​​
  • ​​3.5.3 添加新的注册页面​​
  • ​​3.5.4 注册登录效果演示​​
  • ​​3.6 实现浏览历史功能​​
  • ​​3.6.1 添加浏览历史的状态管理器​​
  • ​​3.6.2 浏览内容存储​​
  • ​​3.6.3 在我的页面中添加浏览历史​​
  • ​​3.6.4 添加新的浏览历史页面​​
  • ​​3.6.5 浏览历史效果演示​​
  • ​​四、源码地址​​

一、背景

  • 这次我们将以项目实战的方式实现一个完整的新闻WebApp客户端,以下是该项目需要实现的功能:
    新闻频道:在主页面上显示新闻频道,用户可选择不同的频道。
    新闻列表:根据用户选择的频道加载对应的新闻,并以列表的方式显示新闻概要。
    新闻阅读:用户点击新闻列表中的项目, 显示具体的新闻明细。
    注册登录:模拟用户注册及登录
    浏览历史:存储用户每次阅读新闻的历史
    我的收藏:存储用户感兴趣的新闻(可模仿浏览历史的功能自行实现)
  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_Vue

  • 主要技术栈如下:
  1. vue-cli脚手架
  2. vue-router路由
  3. element组件库
  4. vscode编辑器
  5. vetur+eSLint+prettier插件
  • 创建项目
  • 请参考​​《使用脚手架vue-cli创建vue项目》​​

二、准备新闻数据接口

  • 在​​极速数据​​网站上申请一个免费的API,并编写获取新闻频道与获取新闻的API接口。

2.1 获取新闻频道

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_vue.js_02

2.2 获取新闻

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_前端_03

  • 注意:为了让前端直接可以访问以上的API接口,需要配置 vue.config.js 解决跨域问题
// vue.config.js
...
devServer: {
port: port,
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
// axios请求中带有/apis的url,就会触发代理机制
'/apis': {
target: 'http://api.jisuapi.com',
secure: false,
changeOrigin: true,
pathRewrite: { '^/apis': '' }
}
}
},
...

2.3 编写前端Api接口

  • 根据以上接口,在前端工程中定义api,供各页面组件调用。
import axios from 'axios'

axios.defaults.baseURL = '/apis'

// 向极速数据免费新闻接口获取新闻频道
export function getNewChannel() {
return new Promise((resolve, reject) => {
axios.get('/news/channel?appkey=自己在极速数据上申请的appkey')
.then(res => {
resolve(res)
}).catch(error => { reject(error) })
})
}

// 向极速数据免费新闻接口获取新闻列表
export function getNewList(type) {
return new Promise((resolve, reject) => {
axios.get('/news/get?channel=' + type + '&start=0&num=30&appkey=自己在极速数据上申请的appkey')
.then(res => {
resolve(res)
}).catch(error => { reject(error) })
})
}

三、WebApp设计

3.1 新闻频道栏组件

  • 我们需要单独编写一个频道组件,主要包含以下这些功能:
    1、横向展示频道列表
  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_ico_04

  • 2、可左右滑动
  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_前端_05

  • 3、选择频道后,获取频道对应的新闻列表
  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_web app_06

  • 以下是频道组件的完整代码
  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_前端_07

<template>
<div class="channel-box">
<div class="channel-list">
<ul ref="channelList" :style="{ left: -nowLeft + 'px' }">
<li v-for="(item, index) in list" :key="index" :ref="'li' + index">
<span class="channel" :class="{ 'channel-active': id === item.id }" @click="selectChannel(item)">
{{ item.tag_name }}
</span>
</li>
</ul>
</div>
<div class="icon-lf">
<i v-show="showLf" class="el-icon-arrow-left" @click="handleLeft" />
</div>
<div class="icon-rt">
<i v-show="showRt" class="el-icon-arrow-right" @click="handleRight" />
</div>
</div>
</template>

<script>
import { getNewChannel, getNewList } from '@/api/news'
export default {
data() {
return {
list: [],
id: 0,
fixedWidth: 200,
nowNum: 0,
showLf: false,
showRt: false,
allWidth: 0,
nowLeft: 0,
nowIndex: 0 }
},
mounted() {
this.getChannel().then(res => {
console.log('channel', res)
if (res && res.status === 200 && res.data.status === 0) {
this.list = []
res.data.result.forEach((element, index) => {
this.list.push({ 'id': index, 'tag_name': element })
})
this.$nextTick(() => {
this.allWidth = this.$refs.channelList.offsetWidth
if (this.allWidth > this.fixedWidth) {
this.showRt = true
}
this.selectChannel({ id: 0, tag_name: '头条' })
})
}
})
},
methods: {
// 异步获取频道
async getChannel() {
const data = await getNewChannel()
return data
},
// 选择频道,根据频道获取新闻
selectChannel(item) {
this.$store.commit('SET_LOADING', true)
this.$store.commit('SET_CHANNEL', item.tag_name)
this.$store.commit('SET_START', 0)
this.getNews(item.tag_name).then(res => {
console.log('news', res)
if (res) {
scrollTo(0, 0)
this.$store.commit('SET_NEWS', res.data.result.list)
this.id = item.id
}
this.$store.commit('SET_LOADING', false)
})
},
// 异步获取新闻
async getNews(channel) {
const data = await getNewList(channel, this.$store.state.news.start, this.$store.state.news.num)
return data
},
// 频道列表左移
handleLeft() {
if (this.nowLeft > 0) {
this.nowNum--
this.showRt = true
if (this.nowNum > 0) {
let nw = 0
for (let j = this.list.length; j >= 0; j--) {
if (j < this.nowIndex) {
nw += this.$refs['li' + j][0].offsetWidth
if (nw >= this.fixedWidth) {
nw -= this.$refs['li' + j][0].offsetWidth
this.nowLeft -= nw
this.nowIndex = j + 1
break
}
}
}
} else {
this.nowLeft = 0
this.nowIndex = 0
this.showLf = false
}
}
},
// 频道列表右移
handleRight() {
if (this.nowLeft + this.fixedWidth < this.allWidth) {
this.nowNum++
this.showLf = true
let nw = 0
for (let i = 0; i < this.list.length; i++) {
if (i >= this.nowIndex) {
nw += this.$refs['li' + i][0].offsetWidth
if (nw > this.fixedWidth) {
nw -= this.$refs['li' + i][0].offsetWidth
this.nowLeft += nw
this.nowIndex = i
break
}
}
}
if (this.nowLeft + this.fixedWidth >= this.allWidth) {
this.showRt = false
}
}
}
}
}
</script>

<style lang="scss" scoped>
.channel-box {
width: 100%;
padding: 0 20px;
height: 46px;
position: fixed;
align-items: center;
top: 1.2rem;
font-size: 18px;
letter-spacing: 3px;
background-color: rgb(252, 248, 248);
.channel-list {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
margin-top: 0.2rem;
ul {
transition-duration: 0.3s;
position: absolute;
top: 0px;
left: 0px;
margin: 0;
padding: 0;
display: flex;
flex-wrap: nowrap;
li {
white-space: nowrap;
display: inline-block;
white-space: nowrap;
padding: 0 10px;
}
li:first-child {
padding-left: 0;
}
li:last-child {
padding-right: 0;
}
}
.channel {
cursor: pointer;
display: inline-block;
height: 28px;
line-height: 28px;
transition: border-color 0.2s;
&:hover {
color: #e72521;
}
}
.channel-active {
color: #e72521;
}
}
.icon-lf {
cursor: pointer;
line-height: 30px;
position: absolute;
left: 5px;
top: 6px;
}
.icon-rt {
line-height: 30px;
cursor: pointer;
position: absolute;
right: 5px;
top: 6px;
}
}
</style>
  • 注意,我们需要将用户当前选择的频道、加载的新闻列表及加载状态放入公共状态管理器中存储,供其它组件调用。
  • 以下是公共状态存储管理器的源码
// 公共状态存储管理器
const news = {
state: {
// 用户选择的频道
channel: '',
// 起始位置
start: 0,
// 一次向接口接取新闻的条数
num: 10,
// 存放拉取下来的新闻
newsData: [],
// 当前正在查看的新闻
newsIndex: -1,
// 是否加载状态
loading: false
},

mutations: {

SET_CHANNEL: (state, channel) => {
state.channel = channel
},
SET_START: (state, start) => {
state.start = start
},
SET_NUM: (state, num) => {
state.num = num
},

SET_NEWS: (state, news) => {
state.newsData = news
},

SET_NEWS_INDEX: (state, newsIndex) => {
state.newsIndex = newsIndex
},
SET_LOADING: (state, loading) => {
state.loading = loading
}

},

actions: {
setChannel({ commit }, channel) {
return new Promise(resolve => {
commit('SET_CHANNEL', channel)
})
},

setStart({ commit }, start) {
return new Promise(resolve => {
commit('SET_START', start)
})
},

setNum({ commit }, num) {
return new Promise(resolve => {
commit('SET_NUM', num)
})
},

setNews({ commit }, news) {
return new Promise(resolve => {
commit('SET_NEWS', news)
})
},
setNewsIndex({ commit }, newsIndex) {
return new Promise(resolve => {
commit('SET_NEWS_INDEX', newsIndex)
})
},
setLoading({ commit }, loading) {
return new Promise(resolve => {
commit('SET_LOADING', loading)
})
}
}
}

export default news

3.2 底部导航条组件

  • 在制作主页面组件前,我们需要先实现底部导航条;
  • 底部导航条中包含首页,我的两个菜单。

3.2.1 封装底层的TabBarItem组件

– 设置标题、激活/未激活的图片插槽;设置路由地址传值参数;设置click点击行为,根据路由地址跳转。

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_前端_08


Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_vue.js_09

– TabBarItem组件实现代码

<template>
<div class="tab-bar-item" @click="itemClick">
<div v-if="!isActive">
<slot name="item-icon" />
</div>
<div v-else>
<slot name="item-icon-active" />
</div>
<div><slot name="item-text" /></div>
</div>
</template>

<script>
export default {
name: 'TabBarItem',
props: {
path: {
type: String,
default: ''
}
},
data() {
return {
}
},
computed: {
// 判断当前条目是否被选中
isActive() {
return !this.$route.path.indexOf(this.path)
}
},
methods: {
// 跳转路由
itemClick() {
this.$router.replace(this.path)
}
}

}
</script>

<style>
.tab-bar-item {
display: flex;
flex: 1;
justify-content: center;
text-align: center;
height: 49px;
line-height: 49px;
cursor: pointer;
}
</style>

3.2.2 实现底部导航条TabBar

– 通过css设置将导航条固定在底部
– 调用TabBarItem进行组合

<template>

<div id="tab-bar">
<tab-bar-item path="/home">
<i slot="item-icon-active" class="el-icon-message-solid" />
<i slot="item-icon" class="el-icon-bell" />
<div slot="item-text">首页</div>
</tab-bar-item>

<tab-bar-item path="/my">
<i slot="item-icon-active" class="el-icon-user-solid" />
<i slot="item-icon" class="el-icon-user" />
<div slot="item-text">我的</div>
</tab-bar-item>

</div>

</template>

<script>
import TabBarItem from '@/components/TabBar/tabBarItem'
export default {
name: 'TabBar',
components: { TabBarItem },
data() {
return {

}
},
methods: {

}
}

</script>

<style>
#tab-bar {
display: flex;
background-color: #f6f6f6;
position: fixed;
left: 0;
right: 0;
bottom: 0;
box-shadow: 0 -1px 1px rgba(100, 100, 100, 0.08);
}
</style>

3.2.3 在主组件App.vue中加入底部导航条

<template>
<div id="app">
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
<tab-bar />
</div>
</template>

<script>
import TabBar from '@/components/TabBar/tabBar'
export default {
name: 'App',
components: { TabBar }
}
</script>

<style>
img {
width: 100%;
}
html, body {
overflow: hidden;
height:calc(100vh - 49px);
}

</style>

3.3 设计新闻列表页面

3.3.1 页面组成

  • 页面由标题栏、新闻频道栏组件、列表页组成。

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_web app_10

  • 当用户滚动新闻列表到达底部时,需要自动加载下一页新闻。

3.3.2 引入无限滚动组件

  • 为了实现列表滚动到底部时,自动加载下一页新闻,我们需要引入element-ui的InfiniteScroll组件,该组件可以判断容器的垂直滚动条滚动至底部时,自动执行加载方法。

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_web app_11

  • 基础代码
<template>
<ul class="infinite-list" v-infinite-scroll="load" style="overflow:auto">
<li v-for="i in count" class="infinite-list-item">{{ i }}</li>
</ul>
</template>

<script>
export default {
data () {
return {
count: 0
}
},
methods: {
load () {
this.count += 2
}
}
}
</script>

3.3.3 动态加载新闻事件

  • 在极数数据接口中,我们观察到请求参数中,有个start参数,可以通过传入偏移值offset,来获取下一页的新闻。
  • 即当前浏览的是第1页的新闻,当滚动条到达底部触发自动加载方法时,需要给start参数传值为2,即加载第2页的新闻,依次类推。

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_前端_12

  • 编写加载方法
methods: {
load() {
// 如果状态管理器中的新闻列表是空的,说明刚初始化数据,不需要判断是否到达底部
if (this.newData.length === 0) {
return
}
// 获取下一页新闻
console.log('已到达底部,自动触发加载方法')
let start = this.$store.state.news.start
start++
if (start < 400) {
this.getNews(this.$store.state.news.channel, start, this.$store.state.news.num).then(res => {
console.log('加载下一页新闻列表', res)
if (res && res.data.result) {
const newsData = this.$store.state.news.newsData
newsData.push.apply(newsData, res.data.result.list)
this.$store.commit('SET_NEWS', newsData)
this.$store.commit('SET_START', start)
}
})
}
},
// 异步获取新闻
async getNews(channel, start, num) {
const data = await getNewList(channel, start, num)
return data
},

}

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_vue.js_13

3.3.4 新闻列表页面源码与效果演示

  • 完成了自动无限加载的关键一步,我们就可以来编写完整的页面了。
<template>
<div>
<!-- 标题栏 -->
<div class="header">
<span />
<span>新闻</span>
<span />
</div>
<channel />
<div ref="container" class="nav-content">
<!-- 在新闻列表中引入无限滚动加载功能 -->
<div v-if="loading == false" ref="scroll" v-infinite-scroll="load" class="news-list">
<div
v-for="(item, index) in newData"
:key="index"
class="section"
@click="toNews(index)"
>
<div class="news">
<div class="news-left">
<img :src="item.pic" alt="">
</div>
<div class="news-right">
<div class="newsTitle">{{ item.title }}</div>
<div class="newsMessage">
<span>{{ item.time }}</span>
<span>{{ item.src }}</span>
</div>
</div>
</div>
</div>
<!-- 在底部放入加载条 -->
<div class="loading-more">正在努力加载</div>
</div>
<el-main
v-else
v-loading="loading"
class="load"
element-loading-background="rgba(0,0,0,0)"
element-loading-text="正在加载中"
/>
</div>
</div>
</template>

<script>
import Channel from './channel'
import { getNewList } from '@/api/news'
export default {
name: 'Home',
components: { Channel },
beforeRouteLeave(to, from, next) {
this.scroll = this.$refs.scroll.scrollTop
next()
},
data() {
return {
scrollTop: 0
}
},
computed: {
newData() {
return this.$store.state.news.newsData
},
loading() {
return this.$store.state.news.loading
}
},
activated() {
this.$refs.scroll.scrollTop = this.scroll
},
methods: {
load() {
// 如果状态管理器中的新闻列表是空的,说明刚初始化数据,不需要判断是否到达底部
if (this.newData.length === 0) {
return
}
// 获取下一页新闻
console.log('已到达底部,自动触发加载方法')
let start = this.$store.state.news.start
start++
if (start < 400) {
this.getNews(this.$store.state.news.channel, start, this.$store.state.news.num).then(res => {
console.log('加载下一页新闻列表', res)
if (res && res.data.result) {
const newsData = this.$store.state.news.newsData
newsData.push.apply(newsData, res.data.result.list)
this.$store.commit('SET_NEWS', newsData)
this.$store.commit('SET_START', start)
}
})
}
},
// 异步获取新闻
async getNews(channel, start, num) {
const data = await getNewList(channel, start, num)
return data
},
// 打开新闻阅读
toNews(index) {
// 存储浏览历史
this.$store.commit('SET_HISTROY', this.newData[index])
// 打开明细
this.$store.commit('SET_NEWS_INDEX', index)
this.$router.push('/news')
}
}

}
</script>

<style lang="scss" scoped>
.header {
width: 100%;
height: 1.2rem;
background-color: #d43d3d;
display: flex;
justify-content: space-between;
align-items: center;
color: #fff;
font-size: 20px;
font-weight: 700;
letter-spacing: 3px;
z-index: 99;
position: fixed;
top: 0;
img {
width: 0.67rem;
height: 0.67rem;
cursor: pointer;
}
}

.nav-content {
margin-top: 2.4rem;
}

.news-list {
position: relative;
height:calc(100vh - 2.4rem - 49px);
overflow-y:auto;
width: 100%;
}

.section {
width: 100%;
height: 2.5rem;
border-bottom: 1px solid #ccc;
}

.news {
height: 2.25rem;
box-sizing: border-box;
margin: 10px 10px;
display: flex;
}
.news-left {
height: 100%;
width: 2.8rem;
display: inline-block;
}
.news-left img {
width: 100%;
height: 100%;
}
.news-right {
flex: 1;
padding-left: 10px;
}
.newsTitle {
width: 100%;
height: 62%;
color: #404040;
font-size: 17px;
overflow: hidden;
}
.newsMessage {
width: 100%;
height: 38%;
display: flex;
align-items: flex-end;
color: #888;
justify-content: space-between;
}
.load {
width: 100%;
height: 100%;
overflow: hidden;
}
.loading-more {
margin-top: 5px;
width: 100%;
height: 20px;
text-align: center;
}
</style>

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_Vue_14

3.4 新闻阅读组件

  • 用户在列表页面上点击新闻,展示新闻详情进行阅读
  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_ico_15


  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_web app_16


<template lang="html">
<div>
<div class="header">
<img src="@/assets/images/back.png" @click="back">
<span>{{ $store.state.news.channel }}新闻</span>
<span />
</div>
<div class="content">
<div ref="container" class="container">
<div class="title">{{ newsData && newsData.title }}</div>
<div class="message">
<span>{{ newsData && newsData.time }}</span>
</div>
<img :src="newsData && newsData.pic">
<div class="newsContent" v-html="newsData && newsData.content" />
</div>
</div>
</div>
</template>

<script>
export default {
computed: {
newsData() {
return this.$store.state.news.newsData[this.$store.state.news.newsIndex]
}
},
methods: {
back() {
this.$router.back()
}
}
}
</script>

<style lang="css" scoped>
.header {
width: 100%;
height: 1.33rem;
background-color: #d43d3d;
color: #fff;
font-size: 20px;
font-weight: 700;
letter-spacing: 3px;
display: flex;
align-items: center;
justify-content: space-between;
position: fixed;
top: 0;
}
.header img {
width: 0.67rem;
height: 0.67rem;
cursor: pointer;
}
.content {
position: relative;
top: 1.33rem;
height:calc(100vh - 1.33rem - 49px);
overflow-y:auto;
}
.container {
margin: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
text-align: center;
}
.message {
text-align: center;
margin: 20px 0;
color: #888;
}
.message span:last-child {
margin-left: 10px;
}
.container img {
width: 100%;
margin-bottom: 20px;
}
.newsContent {
font-size: 18px;
line-height: 30px;
}
</style>

3.5 我的页面制作

  • 在我的页面中有一个是否登录状态,如果状态为已登录,则显示浏览历史、收藏等信息,如果状态为未登录,则显示登录页面。
  • 注意,本文仅模拟登录的功能,没有进行后台验证(后续将提供完整的前后端功能)

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_Vue_17

3.5.1 添加登录状态的状态管理器

  • 为了记录该登录状态,我们需要在状态管理器源码中,加入登录状态的管理
  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_ico_18


const my = {
state: {
...
// 是否已登录
logined: false
},

mutations: {

...
SET_LOGIN: (state, login) => {
state.logined = login
}

},

actions: {
...
setLogin({ commit }, login) {
return new Promise(resolve => {
commit('SET_LOGIN', login)
})
}
}
}

export default my

3.5.2 编写登录表单

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_ico_19

  • 以下是改造后完整源码
<template>
<!-- 已登录状态显示我的信息 -->
<div v-if="logined == true" class="content">
<div class="header">
<div class="user">
<img class="avatar" src="@/assets/images/avatar.png">
<p class="user-name">{{ loginForm.username }}</p>
<img class="right" src="@/assets/images/right.png">
</div>
<div class="info">
<div class="histroy" @click="toHistroy()">
<span class="histroy-count">{{ histroryCount }}</span>
<span class="histroy-text">{{ '浏览历史' }}</span>
</div>
<div class="fav">
<span class="fav-count">{{ favCount }}</span>
<span class="fav-text">{{ '我的收藏' }}</span>
</div>
</div>
</div>
<div class="logout">
<el-button
size="medium"
type="danger"
style="width: 90%"
@click.native.prevent="logout"
>
<span>退 出 登 录</span>
</el-button>
</div>
</div>
<!-- 未登录状态显示登录页面 -->
<div v-else>
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
label-position="left"
label-width="0px"
class="login-form"
>
<h2 class="title">欢迎使用</h2>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="账号"
>
<svg-icon
slot="prefix"
icon-class="user"
class="el-input__icon input-icon"
/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<svg-icon
slot="prefix"
icon-class="password"
class="el-input__icon input-icon"
/>
</el-input>
</el-form-item>
<el-form-item style="width: 100%">
<el-button
:loading="loading"
size="medium"
type="danger"
style="width: 100%"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>

<p class="register">
<!-- <span class="memo">请使用Chrome,Firefox,IE 10+ </span> -->
还没有帐号?
<a href="/register" type="primary">立即注册</a>
</p>
</el-form>
</div>
</template>

<script>

export default {
data() {
return {
loginForm: {
username: '',
password: ''
},
loginRules: {
username: [
{ required: true, trigger: 'blur', message: '用户名不能为空' }
],
password: [
{ required: true, trigger: 'blur', message: '密码不能为空' }
]
},
loading: false
}
},
computed: {
histroryCount() {
return this.$store.state.my.histroy.length
},
favCount() {
return this.$store.state.my.favourite.length
},
// 从状态管理器中获取登录状态
logined() {
return this.$store.state.my.logined
}
},
methods: {
// 模拟登录成功
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true
this.$store.commit('SET_LOGIN', true)
this.loading = false
} else {
console.log('error submit!!')
return false
}
})
},
// 模拟注销登录
logout() {
this.$store.commit('SET_LOGIN', false)
this.loginForm.username = ''
this.loginForm.password = ''
},
toHistroy() {
if (this.histroryCount > 0) {
this.$router.push('/list')
}
}
}
}

</script>

<style lang="scss" scoped>
.login-form {
border-radius: 6px;
background: #ffffff;
width: 100%;
padding: 25px 25px 5px 25px;
margin-top: 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}

.title {
margin: 0 auto 30px auto;
text-align: center;
color: #707070;
}

.register {
float: right;
font-size: 13px;
// color: rgb(24, 144, 255);
}
a {
color: #e72521;
text-decoration: none;
background-color: transparent;
outline: none;
cursor: pointer;
transition: color 0.3s;
}
a:hover {
color: #e72521;
}

.content {
width: 100%;
height: 100%;
background-color: rgb(252, 248, 248);
}
.header {
width: 100%;
height: 5.33rem;
background-color: #fff;
}

.user {
margin-top: 0.5rem;
overflow: hidden;
padding: 0.5rem;
height: 2.5rem;
width: 100%;
}

.avatar {
float: left;
width: 1.8rem;
height: 1.8rem;
border-radius: 50%;
}

.user-name {
float: left;
margin-top: 0.6rem;
margin-left: 0.5rem;
color: #404040;
font-size: 18px;
}

.right {
float: right;
width: 0.8rem;
height: 0.8rem;
margin-top: 0.6rem;
}

.info {
float: left;
padding: 1rem;
height: 2.5rem;
width: 100%;
}
.histroy {
display: flex;
float: left;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}

.histroy-count {
color: #404040;
font-size: 18px;
}

.histroy-text {
margin-top: 0.1rem;
color: #9b9191;
font-size: 14px;
}

.fav {
display: flex;
float: right;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}

.fav-count {
color: #404040;
font-size: 18px;
}

.fav-text {
margin-top: 0.1rem;
color: #9b9191;
font-size: 14px;
}

.logout {
display: flex;
align-items: center;
justify-content: center;
margin-top: 2rem;

}
</style>

3.5.3 添加新的注册页面

1、用户在登录前,需要向系统进行注册。

2、注册时输入用户名,密码(密码需两次输入一致)

3、本文只实现模拟注册(后续将提供完整的前后端功能)

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_vue.js_20

  • 完整的注册页面源码:
<template>
<div>
<el-form
ref="signupForm"
:model="signupForm"
:rules="signupRules"
label-position="left"
label-width="0px"
class="login-form"
>
<h2 class="title">欢迎注册</h2>
<el-form-item prop="username">
<el-input
v-model="signupForm.username"
type="text"
auto-complete="off"
placeholder="账号"
>
<svg-icon
slot="prefix"
icon-class="user"
class="el-input__icon input-icon"
/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="signupForm.password"
type="password"
auto-complete="off"
placeholder="密码"
>
<svg-icon
slot="prefix"
icon-class="password"
class="el-input__icon input-icon"
/>
</el-input>
</el-form-item>
<el-form-item prop="password2">
<el-input
v-model="signupForm.password2"
type="password"
auto-complete="off"
placeholder="确认密码"
@keyup.enter.native="handleSignup"
>
<svg-icon
slot="prefix"
icon-class="password"
class="el-input__icon input-icon"
/>
</el-input>
</el-form-item>
<el-form-item style="width: 100%">
<el-button
:loading="loading"
size="medium"
type="danger"
style="width: 100%"
@click.native.prevent="handleSignup"
>
<span v-if="!loading">注 册</span>
<span v-else>注 册 中...</span>
</el-button>
</el-form-item>
</el-form>
</div>
</template>

<script>

export default {
data() {
// 二次密码输入校验
var checkpass = (rule, value, callback) => {
console.log(value)
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.signupForm.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
}
return {
signupForm: {
username: '',
password: '',
password2: ''
},
signupRules: {
username: [
{ required: true, trigger: 'blur', message: '用户名不能为空' }
],
password: [
{ required: true, trigger: 'blur', message: '密码不能为空' }
],
password2: [
{ trigger: 'blur', validator: checkpass }
]

},
loading: false
}
},
methods: {
// 模拟注册成功
handleSignup() {
this.$refs.signupForm.validate((valid) => {
if (valid) {
this.loading = true
this.$store.commit('SET_LOGIN', true)
this.loading = false
this.$router.push('/my')
} else {
console.log('error signup!!')
return false
}
})
}
}
}

</script>

<style lang="scss" scoped>
.login-form {
border-radius: 6px;
background: #ffffff;
width: 100%;
padding: 25px 25px 5px 25px;
margin-top: 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}

.title {
margin: 0 auto 30px auto;
text-align: center;
color: #707070;
}

</style>

3.5.4 注册登录效果演示

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_vue.js_21

3.6 实现浏览历史功能

3.6.1 添加浏览历史的状态管理器

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_Vue_22

  • 代码
const my = {
state: {
// 存储浏览历史
histroy: [],
// 存储我的收藏
favourite: []
},

mutations: {

SET_HISTROY: (state, histroy) => {
state.histroy.unshift({ createTime: new Date(), histroy: histroy })
},
SET_FAVOURITE: (state, favourite) => {
state.favourite.unshift(favourite)
}

},

actions: {
setHistroy({ commit }, histroy) {
return new Promise(resolve => {
commit('SET_HISTROY', histroy)
})
},

setFavourite({ commit }, favourite) {
return new Promise(resolve => {
commit('SET_FAVOURITE', favourite)
})
}
}
}

export default my

3.6.2 浏览内容存储

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_前端_23

  • 代码改造
<template>
<div>
<!-- 标题栏 -->
<div class="header">
<span />
<span>新闻</span>
<span />
</div>
<channel />
<div ref="container" class="nav-content">
<!-- 在新闻列表中引入无限滚动加载功能 -->
<div v-if="loading == false" ref="scroll" v-infinite-scroll="load" class="news-list">
<div
v-for="(item, index) in newData"
:key="index"
class="section"
@click="toNews(index)"
>
<div class="news">
<div class="news-left">
<img :src="item.pic" alt="">
</div>
<div class="news-right">
<div class="newsTitle">{{ item.title }}</div>
<div class="newsMessage">
<span>{{ item.time }}</span>
<span>{{ item.src }}</span>
</div>
</div>
</div>
</div>

</div>

</div>
</div>
</template>

<script>

export default {
...
methods: {
...
// 打开新闻阅读
toNews(index) {
// 存储浏览历史
this.$store.commit('SET_HISTROY', this.newData[index])
// 打开明细
this.$store.commit('SET_NEWS_INDEX', index)
this.$router.push('/news')
}
}

}
</script>

3.6.3 在我的页面中添加浏览历史

  • 我们在我的页面中添加浏览历史,直接调取存储管理器的计数,进行显示。
  • Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_vue.js_24


<template>
<div class="content">
<div class="header">
<div class="user">
<img class="avatar" src="@/assets/images/avatar.png">
<p class="user-name">{{ 'zhuhuix' }}</p>
<img class="right" src="@/assets/images/right.png">
</div>
<div class="info">
<div class="histroy" @click="toHistroy()">
<span class="histroy-count">{{ histroryCount }}</span>
<span class="histroy-text">{{ '浏览历史' }}</span>
</div>
<div class="fav">
<span class="fav-count">{{ favCount }}</span>
<span class="fav-text">{{ '我的收藏' }}</span>
</div>
</div>
</div>

</div>
</template>

<script>

export default {
computed: {
// 调取浏览历史存储管理器中的计数
histroryCount() {
return this.$store.state.my.histroy.length
},
favCount() {
return this.$store.state.my.favourite.length
}
},
methods: {
toHistroy() {
if (this.histroryCount > 0) {
this.$router.push('/list')
}
}
}
}

</script>

<style lang="css" scoped>
.content {
width: 100%;
height: 100%;
background-color: rgb(252, 248, 248);
}
.header {
width: 100%;
height: 5.33rem;
background-color: #fff;
}

.user {
margin-top: 0.5rem;
overflow: hidden;
padding: 0.5rem;
height: 2.5rem;
width: 100%;
}

.avatar {
float: left;
width: 1.8rem;
height: 1.8rem;
border-radius: 50%;
}

.user-name {
float: left;
margin-top: 0.6rem;
margin-left: 0.5rem;
color: #404040;
font-size: 18px;
}

.right {
float: right;
width: 0.8rem;
height: 0.8rem;
margin-top: 0.6rem;
}

.info {
float: left;
padding: 1rem;
height: 2.5rem;
width: 100%;

}
.histroy {
display: flex;
float: left;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}

.histroy-count {
color: #404040;
font-size: 18px;

}

.histroy-text {
margin-top: 0.1rem;
color: #9b9191;
font-size: 14px;

}

.fav {
display: flex;
float: right;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}

.fav-count {
color: #404040;
font-size: 18px;
}

.fav-text {
margin-top: 0.1rem;
color: #9b9191;
font-size: 14px;
}

</style>

3.6.4 添加新的浏览历史页面

1、增加一个新的浏览历史页面,该页面类似于新闻列表页面。
2、该页面按浏览时间从新到旧显示列表内容。

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_vue.js_25

  • 完整的页面源码:
<template>
<div>
<!-- 标题栏 -->
<div class="header">
<img src="@/assets/images/back.png" @click="back">
<span>浏览历史</span>
<span />
</div>
<div ref="container" class="nav-content">
<!-- 在新闻列表中引入无限滚动加载功能 -->
<div ref="scroll" v-infinite-scroll="load" class="news-list">
<div
v-for="(item, index) in data"
:key="index"
class="section"
@click="toNews(index)"
>
<div class="news">
<div class="news-left">
<img :src="item.histroy.pic" alt="">
</div>
<div class="news-right">
<div class="newsTitle">{{ item.histroy.title }}</div>
<div class="newsMessage">
<span>{{ item.histroy.time }}</span>
<span>{{ item.histroy.src }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'List',
beforeRouteLeave(to, from, next) {
this.scroll = this.$refs.scroll.scrollTop
next()
},
data() {
return {
data: [],
scrollTop: 0,
currentPage: 0
}
},
computed: {
histroy() {
return this.$store.state.my.histroy
}
},
activated() {
this.$refs.scroll.scrollTop = this.scroll
},
methods: {
// 获取下一页历史
load() {
// 判断当前已加载多少页
console.log('currentPage', this.currentPage)
const totalPage = Math.floor(this.$store.state.my.histroy.length / this.$store.state.news.num)
console.log('totalPage', totalPage)
if (this.currentPage <= totalPage) {
// 当前加载的起始位置
const start = this.currentPage * this.$store.state.news.num
// 结束位置
let end = start + this.$store.state.news.num
// 如果剩余未加载的条数小于固定加载条数,则取剩余条数
const banlance = this.histroy.length - this.currentPage * this.$store.state.news.num
if (banlance < this.$store.state.news.num) {
end = start + banlance
}
console.log(start)
console.log(end)
for (let i = start; i < end; i++) { this.data.push(this.histroy[i]) }
console.log(this.data)
this.currentPage++
}
},
// 打开新闻阅读
toNews(index) {
// 打开明细
this.$store.commit('SET_NEWS_INDEX', index)
this.$router.push('/news')
},
back() {
this.$router.back()
}
}

}
</script>

<style lang="scss" scoped>
.header {
width: 100%;
height: 1.33rem;
background-color: #d43d3d;
color: #fff;
font-size: 20px;
font-weight: 700;
letter-spacing: 3px;
display: flex;
align-items: center;
justify-content: space-between;
position: fixed;
top: 0;
}
.header img {
width: 0.67rem;
height: 0.67rem;
cursor: pointer;
}

.nav-content {
margin-top: 1.4rem;
}

.news-list {
position: relative;
height:calc(100vh - 1.4rem - 49px);
overflow-y:auto;
width: 100%;
}

.section {
width: 100%;
height: 2.5rem;
border-bottom: 1px solid #ccc;
}

.news {
height: 2.25rem;
box-sizing: border-box;
margin: 10px 10px;
display: flex;
}
.news-left {
height: 100%;
width: 2.8rem;
display: inline-block;
}
.news-left img {
width: 100%;
height: 100%;
}
.news-right {
flex: 1;
padding-left: 10px;
}
.newsTitle {
width: 100%;
height: 62%;
color: #404040;
font-size: 17px;
overflow: hidden;
}
.newsMessage {
width: 100%;
height: 38%;
display: flex;
align-items: flex-end;
color: #888;
justify-content: space-between;
}
.load {
width: 100%;
height: 100%;
overflow: hidden;
}
.loading-more {
margin-top: 5px;
width: 100%;
height: 20px;
text-align: center;
}
</style>

3.6.5 浏览历史效果演示

Vue项目实战篇二:实现一个完整的新闻WebApp客户端(带前端源码下载)_ico_26