vue 追书神器
app.vue页面
初始化数据,通过调用vuex mutation里定义的方法 调用保存到localstorage中的书架信息、搜索历史记录、字体大小和皮肤
并把这些数据保存的vuex state中
1.书架页 Myshelf
url:http://localhost:8080/myshelf
myshelf页面有 header、shelf-list、tabbar组件
重点:
1.从vuex state中获取已经保存的书架信息
2.处理时间,返回距离现在几小时的格式
<span class="time red">{{ book.updated | time}}</span> import moment from 'moment'; filters: { time(updated) { moment.locale('zh-cn'); return moment(updated).fromNow(); } },
3.删除书架书籍提示是否确认删除
调用mint-ui的messagebox,MessageBox.confirm() 返回的是一个promise对象,点击确认可被then捕获,点击取消可被catch捕获
import { MessageBox } from 'mint-ui';
showDialog(book_id){
MessageBox.confirm('确定要从书架中删除?', '温馨提示').then(action => {
if (action == 'confirm') { //确认的回调
console.log(1);
this.deleteFromShelft(book_id)
}
}).catch(err => {
if (err == 'cancel') { //取消的回调
console.log('取消');
}
})
}
调用写在mutation中的deleteFromShelft,通过filter筛选出对象,再重新保存到localstrage覆盖之前的保存信息
// 删除特定书从书架 deleteFromShelft(state,book_id){ state.shelf_book_list = state.shelf_book_list.filter(value => { return !book_id.includes(value.id); }); // 重现覆盖localstorage setStore('SHEFL_BOOK_LIST', state.shelf_book_list); }, // 删除全部书从书架 deleteAllShelft(state){ state.shelf_book_list.clear(); removeStore('SHEFL_BOOK_LIST') },
补充:数组includes用法
语法:arr.includes(searchElement)
arr.includes(searchElement, fromIndex)
includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。
2.主页面home
吸顶效果
<div class="tabs-warp" :class="searchBarFixed == true ? 'isFixed' :''"> <div ref="tabsContent" class="tabs-content"> <!-- 导航 --> </div> <div v-if="searchBarFixed" class="replace-tab"></div>
定义函数 当滑动距离超过tab栏到顶部距离的时候,应用isFixed样式。
handleScroll () { var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop if(this.offsetTop==0){ this.offsetTop=document.querySelector('.tabs-warp').offsetTop-document.querySelector('.header').offsetHeight } if (scrollTop > this.offsetTop) { this.searchBarFixed = true } else { this.searchBarFixed = false } },
在mounted阶段,监听滚动,当滚动的时候实现上面的函数
mounted(){ window.addEventListener('scroll', this.handleScroll) },
当离开组件的时候需要移除
destroyed () { window.removeEventListener('scroll', this.handleScroll) },
滑动内容带动tab栏滑动
<div class="tabs-warp" :class="searchBarFixed == true ? 'isFixed' :''"> <div ref="tabsContent" class="tabs-content"> <div style="display: inline-block"> <!--PC端运行,加上这个div可修复tab-bar错位的问题 --> <ul class="tabs" ref="tabs"> <li class="tab" v-for="(tab,i) in tabs" :class="{active: i===curIndex}" :key="i" @click="changeTab(i)">{{tab.name}}</li> </ul> <div class="tab-bar" :style="{left: barLeft}"></div> </div> </div> </div> <!-- --> <div v-if="searchBarFixed" class="replace-tab"></div> <!--轮播--> <swiper ref="mySwiper" :options="swiperOption"> <swiper-slide> <!-- 精选 --> <div v-for="(feature_item,index) in feature_channel" :key="index"> <v-home-list :channel="feature_item"></v-home-list> </div> </swiper-slide> <swiper-slide> <!-- 男频 --> <div v-for="(male_item,index) in male_channel" :key="index"> <v-home-list :channel="male_item"></v-home-list> </div> </swiper-slide> <swiper-slide> <!-- 女频 --> <div v-for="(female_item,index) in female_channel" :key="index"> <v-home-list :channel="female_item"></v-home-list> </div> </swiper-slide> <swiper-slide> <!-- 限免 --> <div v-for="(free_item,index) in free_channel" :key="index"> <v-home-list :channel="free_item"></v-home-list> </div> </swiper-slide> <swiper-slide> <!-- 出版 --> <div v-for="(publish_item,index) in publish_channel" :key="index"> <v-home-list :channel="publish_item"></v-home-list> </div> </swiper-slide> </swiper>
引入插件
import { swiper, swiperSlide } from 'vue-awesome-swiper'
设置swiper
data () { return { curIndex: 0, // 当前tab的下标 tabScrollLeft: 0, // 菜单滚动条的位置 swiperOption: { // 轮播配置 on: { transitionEnd: () => { this.changeTab(this.swiper.activeIndex) } } }, isMounted:false, searchBarFixed:false, offsetTop:0, //吸顶 } },
监听轮播
computed: { swiper () { // 轮播对象 return this.$refs.mySwiper.swiper }, barLeft () { // 红线的位置 // 需要等dom加载完才能获取到dom if(this.isMounted){ var tabWidth=document.getElementsByClassName('tab')[0].offsetWidth var barWidth=document.getElementsByClassName('tab-bar')[0].offsetWidth return (tabWidth * this.curIndex + (tabWidth - barWidth) / 2) + 'px' } } },
当轮播切换的时候执行changeTab,tabIndex为轮播的索引
methods: { // 切换菜单 changeTab (tabIndex) { if (this.curIndex === tabIndex) return; // 避免重复调用 let curTab = this.tabs[this.curIndex];// 当前列表 let newTab = this.tabs[tabIndex];// 新转换的列表 this.curIndex = tabIndex; // 切换菜单 curTab.mescroll this.swiper.slideTo(tabIndex); let tabsContent = this.$refs.tabsContent; let tabDom = tabsContent.getElementsByClassName('tab')[tabIndex]; let star = tabsContent.scrollLeft;// 当前位置 let end = tabDom.offsetLeft + tabDom.clientWidth / 2 - document.body.clientWidth / 2; // 居中 this.tabScrollLeft = end; tabsContent.scrollLeft = end; }, }, }
3.分类页面
1.排行榜页面rank.vue
url:http://localhost:8080/rank
rank.vue里只有通用booklist组件
1.create
请求数据,并保持数据到data,并默认加载全部
2.watch
监听rank.id,rank.id对应左边标签,当rank.id改变的时候重新请求数据
3.组件
父组件通过prop 传递bookList数据给booklist组件
补充:图片资源的路径和数据请求的路径不同,需要区分
通用组件
1.booklist组件
2.computed 计算处理data
3.filter用于处理data数据的返回特定格式
2.图书详情页book.vue
url:http://localhost:8080/book/55eef8b27445ad27755670b9
book里有bookinfo review recommend bookbar
1.create
判断该书是否保存在书架
1.如果当前书籍已存在书架中,则书架中的书籍设置为当前书籍
2.如果不在 调用vuex中的mutation 保存自定义的book对象,并保持 this.$route.params.id
this.SET_CUR_BOOK({ id: this.$route.params.id, //书籍id title: '', //书名 cover: '', //封面 author: '', //作者 lastChapter: '', //已更新的最新章节 updated: '', //更新时间 readChapter: '', //已读章节 isInShelf: false, //是否已在书架中,false:不在,true:在 sort: false //目录顺序,false:正序, true:倒序 });
2.组件
1.bookinfo组件
create请求数据 调用vuex 参数为父级保存的 id: this.$route.params.id
2.revew组件
。。。
3.阅读页面 read.vue
1.create
beforeRouteLeave(to, from, next) { //当书籍不在书籍并且不是从目录过来 if (!this.curBook.isInShelf && !this.isFromMenu) { //显示提示框 this.showDialog = true; //调用子组件dialog的confirm方法 this.$refs.dialog.confirm().then(() => { this.showDialog = false; let book = this.curBook; //添加到书架保存 book.isInShelf = true; this.SET_CUR_BOOK(book); this.ADD_TO_SHELF(book); next(); }).catch(() => { this.showDialog = false; next(); }) } else { next() } }
"诺德王国一年只有三个月的时间会是温暖的,别的时间这里似乎总是被风雪和寒冷笼罩,这里的冬天是如此地寒冷以至于晨曦之神能带给这里晨光却无法带来温暖,农业女神的信徒们总是努力地想方设法在那些肥沃可是终年寒冷的土地上播种耐寒的作物,如小麦、大麦、油菜等,一年一熟的农业勉强供给着这个国家的口粮所需。↵↵ 夜晚,位于诺德领北方的乌兰镇静悄悄,又是一年的隆冬时节,天空一如既往地飘起了鹅毛大雪,落在家家户户的屋顶,就连道路上也落得厚厚一层积雪,小镇的空气静谧无声,房屋的烟囱冒出浓浓黑烟,天上的星辰带给地面微弱的星光,大雪却把一切掩埋。↵↵ 这个季节,太阳下山的时间很早,天黑之后,快一分回到城镇就多一分安全,慢一分回到城镇就多一分危险,日落之后的野外什么都有,强盗、野兽人、不时来劫掠的北方蛮族、甚至是可怕的绿皮或者亡灵都时有出没,小镇的居民们基本不会选择在这个时候外出,只有家里的房门和温暖的壁炉能带给他们安全感。↵↵ 一个银装素裹的世界,除了偶尔经过的巡逻卫兵,小镇内一片黑暗,像一个死城,隆冬的寒风不断地呼啸而过,雪花在天际间飘洒,在这个一年最寒冷的季节,小镇的居民们都只能躲在自己的家里,盼望着能够早日等来开春的一刻。↵↵ 一支黑色的皮靴踩在城门口的台阶上" 1.str = str.replace(/↵/g,"<br/>"); 2.str=str.replace(/\n/g,"<br/>") 3.array=array.split('\n')
具体哪一种需要看编辑器的换行符,不一定生效
功能
1.页面弹出设置栏效果
<div class="config"> <div :class="['config-top','bg-black', { 'config-enter': show_config }]">顶部栏</div> <div :class="['config-right','bg-black', { 'config-enter': show_config }]">加入书架</div> <div :class="['config-bottom','bg-black', { 'config-enter': show_config }]"> <div class="config-bootom-item">目录</div> <div class="config-bootom-item" @click="showNightMode()">夜间模式</div> <div class="config-bootom-item">设置</div> </div> </div>
.bg-black{
color: #fff;
background-color: rgba(0, 0, 0, 0.9);
transition: transform 0.15s ease 0s;
text-align: center;
}
.config-top{
position: fixed;
width: 100%;
height: .8rem;
top:0;
left:0;
transform: translateY(-100%);
}
.config-right{
position: fixed;
right: 0;
top:20%;
width: 1.6rem;
height: .6rem;
line-height: .6rem;
transform: translateX(100%);
border-top-left-radius: .3rem;
border-bottom-left-radius: .3rem;
}
.config-bottom{
position: fixed;
display: flex;
width: 100%;
height: 1rem;
bottom: 0;
left: 0;
justify-content: space-between;
transform: translateY(100%);
&>.config-bootom-item{
flex:0 0 33.3%;
}
}
.config-enter{
transform: translate(0%, 0%);
}
首页设置transform偏移到屏幕外,并设置transition效果
2.页面只有点击中心区域弹出设置栏,点击其他区域不弹出
<div class="read-touch" @click="showConfig()"></div> .read-touch{ position: fixed; width: 60%; height: 40%; top: 30%; left: 20%; z-index: 1000; }
3.目录排序,正序和倒序
目录是一个数组,可以通过数组自带reverse()来排序
4.点击目录弹出目录
重点:Read.vue包含两个组件 1.ReadContent书页面 和 2.Chapter目录页面
1.点击弹出目录的按钮在ReadContent组件,2目录显示在Chapter组件。
解决方法:通过$emit 将ReadContent(子组件)的信息传到Read.vue(父组件),再由父组件prop传递到 Chapter(子组件)
Read.vue组件
<template> <section class="read"> <v-read-content :read-content="read_content" @show-chapter="showChapter()"></v-read-content> <v-chapter :chapter-name="chapter_name" :chapter-show="chapter_show" @select-chapter="selectChapterData"></v-chapter> </section> </template> <script> import http from '../http/api' import {mapState,mapMutations} from 'vuex'; import ReadCotent from '../components/common/ReadContent' import Chapter from '../components/common/Chapter' export default { name:'read', components:{ 'v-read-content':ReadCotent, 'v-chapter':Chapter }, data(){ return{ book_id:'', chapter_name:[], chapter_show:false, read_content:[], read_index:0 } }, watch:{ // 监听章节和阅读到第几章变化 chapter_name(){ this.getChapterData(this.chapter_name[this.read_index].id) }, read_index(){ } }, methods:{ // 获取所有章节名 getChapterName(book_id){ http.getChapters(book_id) .then(data => { this.chapter_name=data }) }, // 获取特定章节内容 getChapterData(chapter_id){ http.getChapterContent(chapter_id) .then(data => { this.read_content.push({ content_title: data.title, content_list: data.isVip ? ['vip章节,请到正版网站阅读'] : data.cpContent.split('\n') //换行符分割文章 }); var aa=data.cpContent.split('\n') }) }, // ReadContent组件传出的是否显示章节 showChapter(){ this.chapter_show=true }, selectChapterData(chapter_id){ // 先清空原有书籍 // this.readContent.splice(0, this.readContent.length); this.read_content=[] this.getChapterData(chapter_id) this.chapter_show=false } }, created(){ this.book_id=this.$route.params.id; this.getChapterName(this.book_id) } } </script>
ReadContent.vue组件
<template> <section :class="['read-content',skin_color,{'night-color':night_mode}]" v-if="readContent.length>0" > <div class="config"> <div :class="['config-top','bg-black', { 'config-enter': show_config }]">顶部栏</div> <div :class="['config-right','bg-black', { 'config-enter': show_config }]">加入书架</div> <div :class="['config-bottom','bg-black', { 'config-enter': show_config }]"> <div class="config-bootom-item" @click="showChapter()">目录</div> <div class="config-bootom-item" @click="showNightMode()">夜间模式</div> <div class="config-bootom-item" @click="showConfigPop()">设置</div> </div> <!-- 设置字体颜色弹出层 --> <div class='config-bootom-pop' v-show="show_config_pop"> <ul class="config-skin-color"> <li class="color-item" v-for="(skin,index) in skin_list" :key="index"> <span :class="['color-round-btn',skin,{'skin_color_active':skin==skin_color}]" @click="changeSkinColor(skin)"></span> </li> </ul> <div class="config-control-fontsize"> <button @click="changeFontSize(false)">A-</button> <button @click="changeFontSize(true)">A+</button> </div> </div> </div> <div class="read-tag">民国谍影</div> <h4 class="read-title">{{readContent[0].content_title}}</h4> <div class="read-touch" @click="showConfig()"></div> <ul > <li :style="{ fontSize: font_size + 'px' }" v-for="(item,index) in readContent[0].content_list" :key="index"> {{item}} </li> </ul> </section> </template> <script> export default { name:'readcontent', props:{ readContent:Array }, data(){ return{ show_config:false, //设置弹出层 show_config_pop:false, //设置皮肤字体弹出层 skin_color:'', //皮肤颜色 night_mode:false, //夜晚模式 skin_list:['skin-default', 'skin-blue', 'skin-green', 'skin-pink', 'skin-dark', 'skin-light'], // skin_color_active:'', font_size:14 } }, methods:{ // 显示设置弹出层 showConfig(){ if(!this.show_config){ this.show_config=true }else{ this.show_config=false this.show_config_pop=false } }, // 夜间模式 showNightMode(){ this.skin_color=false if(!this.night_mode){ this.night_mode=true }else{ this.night_mode=false } }, // 显示弹出层皮肤和字体大小 showConfigPop(){ this.show_config_pop=true }, changeSkinColor(skin){ this.night_mode=false this.skin_color=skin }, changeFontSize(isAdd){ console.log(this.font_size) // if ((this.font_size >= 30 && isAdd) || (this.font_size <= 10 && !isAdd)) { // return; // } let size = this.font_size; isAdd ? size++ : size-- this.font_size=size // this.SET_FONT_SIZE(size); }, // 显示章节 $emit触发父组件 showChapter(){ // this.show_config=false this.$emit('show-chapter'); } } } </script> <style lang="scss" scoped> // 皮肤颜色和夜晚模式 .night-color{ color: rgba(255, 255, 255, .5); background-color: #1a1a1a; } .skin-default { background-color: #c4b395; } .skin-blue { background-color: #c3d4e6; } .skin-green { background-color: #c8e8c8; } .skin-pink { background-color: #F8C9C9; } .skin-dark { background-color: #3E4349; } .skin-light { background-color: #f6f7f9; } .read-content{ padding:0 .2rem; // 设置 .bg-black{ color: #fff; background-color: #13120F; transition: transform 0.15s ease 0s; text-align: center; } .config-top{ position: fixed; width: 100%; height: .8rem; top:0; left:0; transform: translateY(-100%); } } </style>
Chapter.vue组件
<template> <section :class="['chapter',{'chapter-show':chapterShow}]"> <div class="chapter-title">返回</div> <div class="chapter-head"> <span class="chapter-total">共{{chapterName.length}}章</span> <span class="chapter-sort" @click="chapterSort()"> <span v-if="sort">倒序</span> <span v-else>正序</span> </span> </div> <div class="chapter-list-section"> <div class="chapter-top">正文卷</div> <ul class="chapter-list"> <li class="chapter-list-item" v-for=" chapter in chapterName" :key="chapter.id" @click="selectChapter(chapter.id)"> <span>{{chapter.title}}</span> <span v-if="chapter.isVip">vip</span> </li> </ul> </div> </section> </template> <script> export default { name:'chapter', props: { chapterName: Array, chapterShow:Boolean }, data(){ return{ sort:true } }, methods:{ // 排序 chapterSort(){ this.sort=!this.sort this.chapterName.reverse(); //数组reverse() }, // 选择章节 selectChapter(chapter_id){ this.$emit('select-chapter', chapter_id); } } } </script> <style lang="scss" scoped> .chapter-title{ height: .8rem; } .chapter-show{ transform: translateX(0) !important; } .chapter{ position: fixed; top:0; left:0; bottom:0; right:0; overflow: hidden; z-index: 9999; background-color: #fff; transform: translateX(-100%); transition: transform .15s; } </style>