因为项目做的是博客demo, 首页进来想给人直观的就能看到文章,看到分类。所以想的一个是可以左右滑动,切换分类,一个是页面以列表形式,直接 list 渲染。类似掘金的样式:
- 页面的左右滑动,并自带过渡效果,直接就可以使用自带的swiper组件;
- 顶部的分类导航其实也是跟着可以左右滑动,并且跟随swiper 页面同步切换,选择也是小程序组件scroll-view,设置为左右滑动;
- 上拉加载更多:小程序有自带的生命周期
onReachBottom
, 默认距离底部50px距离,想要修改可以在页面的style 中设置onReachBottomDistance
字段 - 下拉刷新:小程序页面生命周期
onPullDownRefresh
,同时要在页面的style重配置enablePullDownRefresh:true
开启下拉刷新;掘金的下拉刷新是安卓app的下拉样式,当你用uniapp开发应用,真机运行是可以看到如下结果。
小程序自带的下拉样式如下(原生导航条):
使用自定义导航条下拉样式如下:它会从最顶部开始出现下拉样式
很明显上面的结果不是我们想要的,常用的方式是自定义导航条下面的内容区使用scroll-view 组件,通过scroll-view监听到达顶部和到达底部,继而出发下拉和上拉。为了避免重复造轮子和瞻仰大佬的代码,咱们选择到插件市场逛一逛:最终我选择了如下插件
引入插件进行布局
- 下载插件,把如下两个文件夹复制到自己的项目中,我放到了(
/colorui/components/
),scroll有空数据时的图片记得一并引入。
大家可以自己运行下载的zip安装包,项目直接可以跑通。咱们这里不做演示,直接引入插件中的代码放入首页: (代码参考插件中的/pages/swipe-list/index.vue
)
以下代码在 /pages/home/home.vue 中
...
<view class="top-wrap"><tab id="category" :tab-data="categoryMenu" :cur-index="categoryCur" :size="80" :scroll="true" @change="toggleCategory"></tab></view>
// 这里 swiper使用的是animationfinish,当滑动完成后改变,也可以使用 change事件
<swiper :current="categoryCur" :duration="duration" @animationfinish="swipeChange">
<swiper-item v-for="(item, index) in categoryData" :key="index">
<scroll :requesting="item.requesting" :end="item.end" :empty-show="item.emptyShow" :list-count="item.listCount" :has-top="true" :refresh-size="80" @refresh="refresh" @more="more">
<view class="cells">
<view class="cell" v-for="(item, index) in item.listData" :key="index">
<view class="cell__hd"><image mode="aspectFill" :src="item.images" /></view>
<view class="cell__bd">
<view class="name">{{ item.title }}</view>
<view class="des">{{ item.description }}</view>
</view>
</view>
</view>
</scroll>
</swiper-item>
</swiper>
...
代码语言:javascript复制以下代码在 /pages/home/home.vue 中
// 用于分页
let pageStart = 0
let pageSize = 15
// 列表数据
let testData = [
{
title: '这个绝望的世界没有存在的价值,所剩的只有痛楚',
description: '思念、愿望什么的都是一场空,被这种虚幻的东西绊住脚,什么都做不到',
images: '/static/logo.png' // 这里换成自己本地的图片地址
}...]
// 引入组件, 修改路径
import Tab from '@/colorui/components/tab/index'
import Scroll from '@/colorui/components/scroll/index'
components:{Tab, Scroll},
// 当页面存在时,只会加载一次; onShow 是每次界面显示都会触发,包括手机的屏幕关闭和唤起
onLoad() {
// 第一次进入 加载第一页的数据
this.getList('refresh', pageStart)
},
methods: {
getList(type, currentPage = 1) {
let pageData = this.getCurrentData()
pageData.requesting = true
this.setCurrentData(pageData)
uni.showNavigationBarLoading()
setTimeout(() => {
pageData.requesting = false
uni.hideNavigationBarLoading()
if (type === 'refresh') {
pageData.listData = testData
pageData.listCount = pageData.listData.length
pageData.end = false //是否已经全部加载
pageData.page = currentPage 1
} else {
pageData.listData = pageData.listData.concat(testData)
pageData.listCount = pageData.listData.length
pageData.end = true
pageData.page = currentPage 1
}
this.setCurrentData(pageData)
}, 100)
},
// 顶部tab切换事件
toggleCategory(e) {
this.duration = 0
setTimeout(() => {
this.categoryCur = e.index
}, 0)
},
// 页面滑动切换事件
swipeChange(e) {
this.duration = 300
setTimeout(() => {
this.categoryCur = e.detail.current
this.loadData()
}, 0)
},
// 更新页面数据
setCurrentData(pageData) {
this.categoryData[this.categoryCur] = pageData
},
// 获取当前激活页面的数据
getCurrentData() {
return this.categoryData[this.categoryCur]
},
// 判断是否为加载新的页面,如果是去加载数据
loadData() {
let pageData = this.getCurrentData()
if (pageData.listData.length == 0) {
this.getList('refresh', pageStart)
}
},
// 刷新数据
refresh() {
this.getList('refresh', pageStart)
},
// 加载更多
more() {
this.getList('more', this.getCurrentData().page)
}
}
代码语言:javascript复制以下代码在 /pages/home/home.vue 中
<style lang="scss">
// 这里改成自己放置页面的位置, tab 和 swiper 组件中的 scss 变量文件引用也要修改
@import '~@/colorui/variables';
$top-height: 90rpx;
.top-wrap {
position: fixed;
left: 0;
top: 0;
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
width: 100%;
background-color: #ffffff;
z-index: 99;
}
swiper {
height: 100vh;
}
.cells {
background: #ffffff;
margin-top: 20rpx;
}
.cell {
display: flex;
padding: 20rpx;
&:not(:last-child) {
border-bottom: 1rpx solid $lineColor;
}
&__hd {
font-size: 0;
image {
width: 160rpx;
height: 160rpx;
margin-right: 20rpx;
border-radius: 12rpx;
}
}
&__bd {
flex: 1;
.name {
@include line(2);
font-size: 28rpx;
margin-bottom: 12rpx;
}
.des {
@include line(2);
color: $mainBlack2;
font-size: 24rpx;
}
}
}
</style>
页面效果如下:我们发现,引入了但是没有tab组件
查看dom结构可知:
.top-wrap
使用了fixed定位,使用原生导航条没有影响,而我们使用的自定义导航条,也是定位。所以这里需要给 tab 设置 top 值,导航条的高度。还记得我们在实现自定义导航条时在App.vue中获取系统相关信息,赋值到了 Vue.prototype.CustomBar
// 以下代码在 /pages/home/home.vue中
// 动态设置 top值
<view :style="{top: CustomBar 'px'}" class="top-wrap tui-skeleton-rect">
<tab id="category" :tab-data="categoryMenu" :cur-index="categoryCur" :size="80" :scroll="true" @change="toggleCategory"></tab>
</view>
...
data 中设置
CustomBar: this.CustomBar,
效果很帅气~~
这时大家在左右滑动的时候会发现一个问题,第一次从推荐滑动到精选集锦的时候,tab中的下边栏没有动,之后的滑动都会运动:
我们查看发现/colorui/tab/index.vue
中的 scrollByIndex 方法没有触发,在tab/index.vue
中,我们发现实际上 curIndex 值是变化的,最简单的方式就是在监听curIndex变化的时候触发 scrollByIndex 方法:
watch: {
curIndex(newVal, oldVal) {
this.tabCur = newVal
this.tabCurChange(newVal, oldVal)
// 新增
this.scrollByIndex(newVal)
},
...
bug 解决
添加云函数
这里我们创建两个云函数,一名为article存放类别下的文章,一名为articleCategory,对应我们的顶部tab(我也不确定这么分是否最好)。创建完成后记得上传。
- 初始化云数据库
// 以下代码在 db_init.json 中
"article_category": {
"data": [ // 数据
{
"name": "掘金"
},
{
"name": "HTML"
},
{
"name": "CSS"
},
{
"name": "JS"
},
{
"name": "VUE"
},
{
"name": "REACT"
},
{
"name": "LeeCode"
},
{
"name": "面试题"
}
],
"index": [{ // 索引
"IndexName": "name", // 索引名称
"MgoKeySchema": { // 索引规则
"MgoIndexKeys": [{
"Name": "name", // 索引字段
"Direction": "1" // 索引方向,1:ASC-升序,-1:DESC-降序
}],
"MgoIsUnique": false // 索引是否唯一
}
}]
}
右键初始化我们的云数据库,如下
因为云数据库中有我们的user 数据,包括初始化和注册的,所以我们这里不覆盖。这是打开我们的云端web控制台,可以看到数据初始化成功:
接下来我们初始化文章表,由于文章会跟种类对应,所以文章每一项会有个categoryId字段,所以我们先初始化的类别表,初始化article表:
代码语言:javascript复制// 以下代码在 db_init.json 中
// 文章表
"article": {
"data": [ // 数据
{
"headImg": "https://images.weserv.nl/?url=https://p1.ssl.qhimgs1.com/sdr/400__/t012defb31c27aec7eb.jpg",
"title": "这个绝望的世界没有存在的价值,所剩的只有痛楚1",
"categoryId": "5ebd3b7f33bd17004e01c686",
"description": "思念、愿望什么的都是一场空,被这种虚幻的东西绊住脚,什么都做不到",
"date": "2020-03-09"
},
{
"headImg": "https://images.weserv.nl/?url=https://p1.ssl.qhimgs1.com/sdr/400__/t012defb31c27aec7eb.jpg",
"title": "这个绝望的世界没有存在的价值,所剩的只有痛楚2",
"categoryId": "5ebd3b7f33bd17004e01c686",
"description": "思念、愿望什么的都是一场空,被这种虚幻的东西绊住脚,什么都做不到",
"date": "2020-03-08"
},
{
"headImg": "https://images.weserv.nl/?url=https://p1.ssl.qhimgs1.com/sdr/400__/t012defb31c27aec7eb.jpg",
"title": "这个绝望的世界没有存在的价值,所剩的只有痛楚css",
"categoryId": "5ebd3b7f33bd17004e01c686",
"description": "思念、愿望什么的都是一场空,被这种虚幻的东西绊住脚,什么都做不到",
"date": "2020-03-08"
}
],
"index": [{ // 索引
"IndexName": "date", // 索引名称
"MgoKeySchema": { // 索引规则
"MgoIndexKeys": [{
"Name": "date", // 索引字段
"Direction": "-1" // 索引方向,1:ASC-升序,-1:DESC-降序
}],
"MgoIsUnique": false // 索引是否唯一
}
}]
}
正常应该有PC管理平台,分配上传文章,并实现文章和类别的对应,这里就自己简化操作
这时开始写我们的页面逻辑:先把前端写死的假数据清除,包括categoryMenu 和 categoryData(记住数据格式)。
编写请求类别逻辑
代码语言:javascript复制// 以下代码在 /pages/home/home.vue 中
onLoad() {
// 我得逻辑是先请求类别,在根据第一个类别获取文章
this.getCategoryMenu()
// this.getList('refresh', pageStart)
},
...
async getCategoryMenu() {
// 以防内部执行出错
try {
// 还记得我们在user表时,创建了add 和get两个目录处理不同操作,
// 这里也用了同样思路,万一想在小程序里实现delete 和put功能也方便,不是必须的
const res = await this.$uniCloud('articleCategory', {
type: 'get'
})
this.categoryMenu = ?
this.categoryData = ?
// 获取完类别 获取类别下的文章
this.getList('refresh', pageStart)
} catch (e) {
// 在全局混入中定义了通用的报错信息
this.$toast(this.errorMsg)
}
}
...
书写articleCategory函数中逻辑:
代码语言:javascript复制// 一下代码在云函数 articleCategory中
'use strict';
const { get } = require('./get')
exports.main = async (event, context) => {
// event 就是我们传递的 变量对象
switch (event.type) {
case 'get':
return await get(event)
}
};
// get 目录
const db = uniCloud.database()
exports.get = async (data) => {
// 数据没有限制,表中有什么返回什么
const collection = db.collection('article_category')
// 查找最后一定要 get 一下
return await collection.get()
}
// 记得上传运行云函数呦
云函数部署成功后,刷新我们的页面,发现有请求,书写页面逻辑:
代码语言:javascript复制// 请求处理数据代码
async getCategoryMenu() {
try {
const res = await this.$uniCloud('articleCategory', {
type: 'get'
})
this.categoryMenu = res.result.data
this.categoryData = this.categoryMenu.map(item => {
return {
name: item.name,
requesting: false,
end: false,
emptyShow: false,
page: pageStart,
listData: []
}
})
// 请求第一类别文章
// this.getList('refresh', pageStart)
} catch (e) {
this.$toast(this.errorMsg)
}
}
这时发现页面显示不正确:
我们看一下tab/index.vue
插件代码,发现 11 行显示的是 item,而我们返回的是对象, 所以改成 item.name,这时我们的类别显示出来了。
编写请求文章逻辑
代码语言:javascript复制// home.vue中的getList方法
// 插件本身逻辑不用动,只需加入我们的请求
async getList(type, currentPage = 1) {
let pageData = this.getCurrentData()
pageData.requesting = true
this.setCurrentData(pageData)
// 自定义导航栏没有这个,需要可以自己加
// uni.showNavigationBarLoading()
// 请求数据, 第0页开始 1-10条
let res = await this.$uniCloud('article', {
// 类别
categoryId: this.categoryMenu[this.categoryCur]._id,
currentPage,// 第几页
pageSize// 每页数量
})
// 请求的数据赋值
testData = res.result.list
setTimeout(() => {
pageData.requesting = false
// uni.hideNavigationBarLoading()
if (type === 'refresh') {
pageData.listData = testData
pageData.listCount = pageData.listData.length
pageData.end = false //是否已经全部加载
pageData.page = currentPage 1
} else if (testData.length === 10) {
pageData.listData = pageData.listData.concat(testData)
pageData.listCount = pageData.listData.length
pageData.end = false
pageData.page = currentPage 1
} else if (testData.length >= 0 && testData.length < 10) {
pageData.listData = pageData.listData.concat(testData)
pageData.listCount = pageData.listData.length
pageData.end = true
// pageData.page = currentPage 1
}
this.setCurrentData(pageData)
if (pageData.listData.length === 0) {
pageData.emptyShow = true
}
}, 100)
}
代码语言:javascript复制// 以下代码在article云函数中, 相信大家一看会很清楚
'use strict';
const db = uniCloud.database()
const dbCmd = db.command
exports.main = async (event, context) => {
const collection = db.collection('article')
// 总条数
let total = await collection.where({categoryId : event.categoryId}).count()
// 获取文章列表
let start = event.currentPage * event.pageSize
let res = await collection.where({categoryId : event.categoryId}).orderBy('date','desc').skip(start).limit(event.pageSize).get();
return {
total: total.total,
list: res.data
}
};
数据出来了,三调正好和初始化的一样(没有出现图片的小伙伴,我们修改了images变量为 headImg,记得修改)
这里有个小问题
这里不是使用的border-bottom,而是根据tab 的item的宽度对应生成的,那这个初始长度哪里来的呢,查看tab/index.vue
源码中data初始赋值:lineWidth: 100, // 下划 line 宽度
,我们发现在初始化数据时并未触发动态生成下划线的方法:this.init(),因为插件是直接写死的数据,页面直接渲染,而我们是请求的数据, 所以初始时并未执行,同样一个watch即可:
以下代码在 tab/index.vue中
watch:{
...
tabData(newVal) {
this.init()
}
}
我们发现未有数据时,会有个100px长的line,所以我们直接设置初始值为0即可
文章详情
由于存储到云数据库中时,都会自动生成_id,所以从文章列表页跳转到详情页,只要带着_id字段即可,在详情页面进行请求。
代码语言:javascript复制在pages目录右键创建page-details页面,由于文章内容以markdown或者富文本形式,我们可以使用rich-text组件,但是该组件对图片的预览,链接的跳转,包括事件的实现都不好,所以我们同样在插件市场使用parse富文本解析插件,首先实现列表跳转详情页:
// home.vue 中
<view class="cells">
// navigator-hover 内置的点击样式
<view hover-class="navigator-hover" @tap='toDetail(item)' class="cell" v-for="(item, index) in item.listData" :key="index">
<view class="cell__hd"><image mode="aspectFill" :src="item.headImg" /></view>
<view class="cell__bd">
<view class="name">{{ item.title }}</view>
<view class="des">{{ item.description }}</view>
</view>
</view>
</view>
...
toDetail(item) {
this.$router('/page-details/page-details?_id=' item._id)
}
// 以下代码在 page-details.vue中
onLoad(e) {
//路由中传参,使用onLoad接收
console.log(e)
}
- 富文本解析插件
下载zip解压到我们的项目中,/colorui/components/parse
,在app.vue中引入样式@import "colorui/components/parse/parse.css";
,page-details中使用:
<view>
<Parse :content="article" @preview="preview" @navigate="navigate"></Parse>
</view>
...
import Parse from '@/colorui/components/parse/parse.vue'
data() {
return {
article: ''
}
},
components:{Parse},
...
//预览图片
preview(src, e){},
// 跳转
navigate(href, e){}
插件支持富文本格式和markdown格式,先介绍markdown使用
markdown
- 使用cnpm安装marked,在根目录下npm init -y && cnpm install marked --save
- 详情目录中引入 import marked from 'marked'
let str = `# uncertainty rn ## uncertainty rn ### uncertainty`
this.article = marked(str)
富文本
代码语言:javascript复制onLoad(e) {
this.article = `<div><h1>你好啊</h1><h2>我很好</h2></div>`
},
详情接口
创建pageDetails云函数,上传并运行;初始化 article_details 集合:
代码语言:javascript复制// 以下代码在 db_init.json
"article_details": {
"data": [
{
// 这里的_id切记要和列表中返回的对应
"id": "5ebd3c9c3c6376004c5cedbc",
"content": "# 你好测不准 hello",
"date": "2020-05-18"
}
],
"index": [{
"IndexName": "id", // 索引名称
"MgoKeySchema": { // 索引规则
"MgoIndexKeys": [{
"Name": "id", // 索引字段
"Direction": "-1" // 索引方向,1:ASC-升序,-1:DESC-降序
}],
"MgoIsUnique": true // 索引是否唯一
}
}]
}
我使用的文章详情的id值对应列表中的_id值,实现查找;只需要点击列表跳转时把_id传到详情页,详情页中实现获取云端数据
代码语言:javascript复制// 以下代码在 page-details.vue中
onLoad(e) {
this.getDetails(e)
},
...
async getDetails(e) {
let res = await this.$uniCloud('pageDetails', {
id: e._id
})
try{
this.article = marked(res.result.data[0].content)
}catch(e){
//TODO handle the exception
}
},
发现请求成功,数据也是我们刚刚上传的,富文本格式也是一样的;属性配置大家使用官网即可
如果后台返回的富文本中的媒体标签 img、video等的链接地址没有域名,只有目录如/upload/images/a.png
,大家可以在 /parse/libs/wxDiscode.js
中修改:
如果有a标签需要跳转,应为web-view组件,页面中使用原生导航条,web-view加载的第三方页面层级会最高。 App开发中使用富文本解析显示的结果可能会和小程序中不同,大家可自行尝试。 使用rich-text标签解析富文本的朋友,可能很难去改变内部的样式,还有不能给标签添加事件。如果功能简单的话大家可以使用字符串替换添加样式类,如:
后台返回数据 let str = '<div>你好测不准</div>'
拿百度富文本解析为例,一般使用者会直接把word文档拖入到富文本编辑器中,编辑器也会自动解析成dom结构,没有类名只有行内样式,样式比较固定。正因为如此如果到移动端了如果由样式修改的需要就要自己做字符串替换(后端返回的就是dom字符串)添加样式或者添加类名。
1.实现文本复制可以 str = ‘<div class="wrapper">’ str '</div>'
(给返回的dom串包裹在wrapper类中,整体设置wrapper样式修改)
.wrapper{
user-select: text;
}
2.给图片添加类名,设置居中,最大宽度100% str = str.replace(/<img/g,"<img class='my-img'")
.my-img{
display:block;
max-width:100%;
margin: 0 auto;
}
如果详情页面中有点赞,而列表页中为 onLoad 请求,那么在退出详情页返回列表页时不会在请求(如果使用onShow,会重新请求,但是列表页会有分页查询,发挥列表页时在请求会带来很多不便),这时要更新列表页的点赞数,确定点赞或取消点赞成功的话,可以使用 uni 自带的 uni.,和on(),详情页触发,列表页监听。和vue 的EventBus一样
制作侧边弹出栏
因为我得页面只做了两个切换按钮,所以设置头像,设置字段就放在侧边抽屉:
我们要在自定义导航条组件中进行小修改
代码语言:javascript复制// 以下代码在cu-custom中
<view class="action" @tap="BackPage" v-if="isBack">
<text :class="'cuIcon-' icon"></text>
<slot name="backText"></slot>
</view>
props中添加
icon: {
type: String,
default: 'back'
}
图标我们选用colorui自带的cuIcon-sort
创建组件colorui/components/drawer/drawer.vue。其实思路也比较简单,就是icon是back还是sort,如果是sort就显示侧边栏,加个简单的动画,直接上代码:
代码语言:javascript复制以下代码在 /colorui/components/drawer/drawer.vue
<template>
<view class="drawer-class drawer" :class="[visible ? 'drawer-show' : '','drawer-left']">
<view v-if="mask" class="drawer-mask" @tap="handleMaskClick" @touchmove.stop.prevent></view>
<view class="drawer-container" @touchmove.stop.prevent>
<slot></slot>
</view>
</view>
</template>
<script>
export default {
name:"Drawer",
props: {
visible: {
type: Boolean,
default: false
},
mask: {
type: Boolean,
default: true
},
maskClosable: {
type: Boolean,
default: true
}
},
methods: {
handleMaskClick() {
if (!this.maskClosable) {
return;
}
this.$emit('close', {});
}
}
}
</script>
<style>
.drawer {
visibility: hidden;
}
.drawer-show {
visibility: visible;
}
.drawer-show .drawer-mask {
display: block;
opacity: 1;
}
.drawer-show .drawer-container {
opacity: 1;
}
.drawer-show.drawer-left .drawer-container{
transform: translate3d(0, -50%, 0);
}
.drawer-mask {
opacity: 0;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 99999;
background: rgba(0, 0, 0, 0.6);
transition: all 0.3s ease-in-out;
}
.drawer-container {
position: fixed;
left: 50%;
height: 100%;
top: 0;
transform: translate3d(-50%, -50%, 0);
transform-origin: center;
transition: all 0.3s ease-in-out;
z-index: 99999;
opacity: 0;
background: #fff;
}
.drawer-left .drawer-container {
left: 0;
top: 50%;
transform: translate3d(-100%, -50%, 0);
}
</style>
在cu-custom/cu-custom.vue
中引入(布局样式大家可以根据自己的喜好书写):
// 以下代码在/colorui/components/cu-custom/cu-custom.vue中
...
<drawer mode="left" :visible="isleftDrawer" @close="closeDrawer">
<view class="d-container h-100 flex flex-direction justify-center align-center">
<view class="cu-avatar xl bg-red round cu-card shadow margin-bottom-xl">
<!-- 随机头像 http://api.btstu.cn/doc/sjtx.php-->
<image src="http://api.btstu.cn/sjtx/api.php" class="w-100 h-100"></image>
</view>
<view class="cu-list w-100 menu">
<view class="cu-item arrow" @tap='handleNav(item)' v-for="(item, index) in navList" :key='index' hover-class="hover-class">
<view class="content">
<text class="text-grey" :class="['cuIcon-' item.icon]"></text>
<text class="text-grey">{{item.navName}}</text>
</view>
</view>
</view>
</view>
</drawer>
...
import Drawer from "../drawer/drawer"
...
data() {
retrun {
isleftDrawer: false
}
},
components: {Drawer},
props: {
...
icon: {
type: String,
default: 'back'
}
}
小节
本小节是是博客demo的最后一部分,功能没多少,也算是我对使用云函数的一个小总结。大家可以自己的想法设计自己的小程序,自己书写云函数,小编也是刚入手,有写的不对的地方请大家指正;有跟着实现功能的朋友也可以自己去拓展,例如列表页实现骨架屏,大家可以去插件市场学习查看更多的功能实现,引入到自己的项目中。
·END·