We developed an open source tool based on Git for technical practice tutorial writing, all our tuq community tutorials are written with this tool, welcome Star

If you want to quickly learn how to use it, please read our tutorial documentation

This article is written by a Tuture writer who is not authorized to use the Tuture writing tool. Tuture community will seriate its uni-App and cloud function development mini-program blog series. Thanks to the author for his excellent output and making our technology world a better place 😆

Because the project is a blog demo, the home page to give people intuitive can see the article, see the classification. So the idea is that you can swipe left and right to switch categories, or you can render the page as a list, directly as a list. A similar style to the nuggets:

  1. Swiper can be used directly by swiper.
  2. In fact, the top category navigation can be swiped left or right, and synchronized with the swiper page switch, select the small program component Scroll view, set to left or right;
  3. Pull-up load more: Applets have their own life cycleonReachBottomThe default distance from the bottom is 50px. If you want to change it, you can set it in the style of the pageonReachBottomDistancefield
  4. Drop-down Refresh: Applet page life cycleonPullDownRefreshAt the same time, reconfigure the page styleEnablePullDownRefresh: true,Enable drop-down refresh; Nuggets pull-down refresh is a pull-down style of Android app. When you develop an app using UniApp, you can see the following results on a real machine.

The dropdown style of the applet is as follows (native navigation bar) :

Use the custom navigation bar drop-down style as follows: It will appear from the top of the drop-down style

Obviously, the above result is not what we want. The common way is to customize the content area under the navigation bar and use the Scrollview component. The scrollview listens to reach the top and bottom, and then starts to pull down and pull up. To avoid reinventing the wheel and looking at the big guy’s code, let’s take a look at the plugin market: I ended up with the following plugin

Introduce plug-ins for layout

  1. Download the plugin and copy the following two folders into your project./colorui/components/), scroll free data when the picture remember to introduce.

You can run your own downloaded ZIP installation package, the project can run directly. We will not do the demo here, but directly introduce the plugin code into the home page: (code reference plugin /pages/swipe-list/index.vue)

The following code is in /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 uses animationFinish. You can also use the change event when swiping completes<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>.Copy the code
The following code is in /pages/home/home.vue// Used for paging
let pageStart = 0
let pageSize = 15
// List data
let testData = [
	{
		title: 'There's nothing worth living in this hopeless world. All that's left is pain.'.description: 'Thoughts, wishes and so on are all in vain. You can't do anything if you are stuck in this illusory thing.'.images: '/static/logo.png' // Change this to your local image address}... ]// Import the component and modify the path
import Tab from '@/colorui/components/tab/index'
import Scroll from '@/colorui/components/scroll/index'


components:{Tab, Scroll},
// When the page exists, it is loaded only once; OnShow is triggered every time an interface is displayed, including the phone's screen closing and arousing
onLoad() {
        // Enter the first page to load data
	this.getList('refresh', pageStart)
},
methods: {
	getList(type, currentPage = 1) {
		let pageData = this.getCurrentData()


		pageData.requesting = true


		this.setCurrentData(pageData)


		uni.showNavigationBarLoading()


		setTimeout((a)= > {
			pageData.requesting = false


			uni.hideNavigationBarLoading()


			if (type === 'refresh') {
				pageData.listData = testData
				pageData.listCount = pageData.listData.length
				pageData.end = false // Check whether all the files are loaded
				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)},// Top TAB toggle event
	toggleCategory(e) {
		this.duration = 0


		setTimeout((a)= > {
			this.categoryCur = e.index
		}, 0)},// Page slide switch event
	swipeChange(e) {
		this.duration = 300


		setTimeout((a)= > {
			this.categoryCur = e.detail.current


			this.loadData()
		}, 0)},// Update page data
	setCurrentData(pageData) {
		this.categoryData[this.categoryCur] = pageData
	},
	// Get the data for the current active page
	getCurrentData() {
		return this.categoryData[this.categoryCur]
	},
	// Determine whether to load a new page, and if so, to load data
	loadData() {
		let pageData = this.getCurrentData()
		if (pageData.listData.length == 0) {
			this.getList('refresh', pageStart)
		}
	},
	// Refresh data
	refresh() {
		this.getList('refresh', pageStart)
	},
	// Load more
	more() {
		this.getList('more'.this.getCurrentData().page)
	}
}

Copy the code
/pages/home/home.vue <style lang=" 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>Copy the code

The page looks like this: We find that the TAB component is introduced but not present

View the DOM structure:

Top-wrap uses fixed positioning, using the native navigation bar has no impact, while the custom navigation bar we use is also positioning. So we need to set the top value of TAB, the height of the navigation bar. Remember when we implemented the custom navigation bar we got the system information in app.vue and assigned it to vue.prototype.customBar

// Dynamically set the top value in /pages/home/home.vue<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 set CustomBar: this.CustomBar,Copy the code

The effect is very handsome

The first time you slide from Recommended to Highlights, the bottom bar of the TAB does not move, and all subsequent slides will move:

/colorui/ TAB /index.vue scrollByIndex method does not trigger, TAB /index.vue scrollByIndex method does not trigger, TAB /index.vue The simplest way to do this is to fire the scrollByIndex method while listening for curIndex changes:

watch: {
	curIndex(newVal, oldVal) {
		this.tabCur = newVal
		this.tabCurChange(newVal, oldVal)
                / / new
		this.scrollByIndex(newVal)
	},
...

Copy the code

Bug to solve

Add cloud functions

Here we create two cloud functions, one called Article to hold articles under the category, and one called articleCategory, which corresponds to our top TAB (I’m not sure if this is the best separation). Upload it after you create it.

  • Initialize the cloud database
. / / the following code in db_init json in "article_category" : {" data ": [/ / data {" name" : "nuggets"}, {" name ":" HTML "}, {" name ": "CSS"}, {" name ":" JS "}, {" name ":" VUE "}, {" name ":" REACT "}, {" name ":" LeeCode "}, {" name ":" interview questions "}], "index" : [{"name" : "name", // IndexName" MgoKeySchema": {// index rule "MgoIndexKeys": [{"name" : "name", // IndexName" Direction": "1" // index direction, 1: ASC- ascending, -1: DESC- descending}], "MgoIsUnique": falseCopy the code

Right click to initialize our cloud database as shown below

Because the cloud database has our user data, including initialization and registration, we do not overwrite it here. This is to open our cloud Web console and see that the data has been successfully initialized:

Next we initialize the article table. Since articles correspond to categories, each entry in the article will have a categoryId field, so we initialize the category table and the article table:

Json // article table "article": {"data": [// data {"headImg": "https://images.weserv.nl/?url=https://p1.ssl.qhimgs1.com/sdr/400__/t012defb31c27aec7eb.jpg", "title": "There's nothing worth living in this hopeless world. All that's left is pain." My thoughts, wishes and so on are all in vain. I can do nothing if I am stuck by such an illusory thing. "https://images.weserv.nl/?url=https://p1.ssl.qhimgs1.com/sdr/400__/t012defb31c27aec7eb.jpg", "title": "There's nothing worth living in this hopeless world. All that's left is pain." My thoughts, wishes and so on are all in vain. I can do nothing if I am stuck by such an illusory thing. "https://images.weserv.nl/?url=https://p1.ssl.qhimgs1.com/sdr/400__/t012defb31c27aec7eb.jpg", "title": "This hopeless world has no value, all that's left is CSS of pain ", "categoryId":" 5EBd3b7F33b17004e01C686 ", "description": "Thoughts, desire of what is a sieve, caught up in this illusory things his, couldn't do anything", "date" : "2020-03-08"}], "index" : [{/ / index "IndexName" : [{"Name": "date", // index Name" MgoKeySchema": {// index rule "MgoIndexKeys": [{"Name": "date", // index Name" Direction": // index direction, 1: ASC- ascending, -1: DESC- descending}], "MgoIsUnique": false}}]}Copy the code

Normally there should be a PC management platform, allocate upload articles, and achieve the corresponding articles and categories, here to simplify their operations

This is the time to start writing our page logic: clean up the fake data that was written to death in the front end, including categoryMenu and categoryData (remember the data format).

Write the request category logic

// The following code is in /pages/home/home.vue
onLoad() {
        // My logic is to request the category first, and then get the article according to the first category
	this.getCategoryMenu()
	// this.getList('refresh', pageStart)},... async getCategoryMenu() {// In case of internal execution errors
	try {
                // Remember that we created the add and get directories to handle different operations in the user table.
                // The same idea is used here. If you want to implement delete and put functions in small programs, it is convenient, but not necessary
		const res = await this.$uniCloud('articleCategory', {
			type: 'get'
		})


		this.categoryMenu = ?
		this.categoryData = ?
                // Get the articles under the category
		this.getList('refresh', pageStart)
	} catch (e) {
                // A generic error message is defined in global mixin
		this.$toast(this.errorMsg)
	}
}
...

Copy the code

Write the logic in articleCategory:

// This code is in the cloud function articleCategory
'use strict';
const { get } = require('./get')
exports.main = async (event, context) => {
        // Event is the variable object we pass
	switch (event.type) {
		case 'get':
			return await get(event)
	}
};


/ / get the directory
const db = uniCloud.database()
exports.get = async (data) => {
        // There is no limit to data. What is in the table returns what
	const collection = db.collection('article_category')
        // The search must finally get
	return await collection.get()
}


// Remember to upload and run cloud functions

Copy the code

After the cloud function is successfully deployed, refresh our page and find a request, write the page logic:

// Request processing data code
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: []}})// Request the first category of articles
		// this.getList('refresh', pageStart)
	} catch (e) {
		this.$toast(this.errorMsg)
	}
}

Copy the code

The page is displayed incorrectly:

If we look at the TAB /index.vue plugin code, we see that line 11 shows item, and we’re returning objects, so change to item.name, and now our category is displayed.

Write the request article logic

// home. Vue getList method
// The plugin logic does not need to be touched, just add our request
async getList(type, currentPage = 1) {
	let pageData = this.getCurrentData()


	pageData.requesting = true


	this.setCurrentData(pageData)
	// The custom navigation bar does not have this function
	// uni.showNavigationBarLoading()


	// Request data, start on page 0
	let res = await this.$uniCloud('article', {
                / / class
		categoryId: this.categoryMenu[this.categoryCur]._id,
		currentPage,/ / what page
		pageSize// Number per page
	})
        // Requested data assignment
	testData = res.result.list


	setTimeout((a)= > {
		pageData.requesting = false


		// uni.hideNavigationBarLoading()


		if (type === 'refresh') {
			pageData.listData = testData
			pageData.listCount = pageData.listData.length
			pageData.end = false // Check whether all the files are loaded
		        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)}Copy the code
// The following code is in the article cloud function
'use strict';
const db = uniCloud.database()
const dbCmd = db.command


exports.main = async (event, context) => {
  const collection = db.collection('article')
  / / the total number of article
  let total = await collection.where({categoryId : event.categoryId}).count()


  // Get the list of articles
  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
  }
};

Copy the code

The data comes out, and the three adjustments are exactly the same as the initialization. (For those who do not have images, we changed the images variable to headImg, remember to modify it.)

There’s a slight problem

The initial width of the TAB item is generated according to the width of the TAB item. The initial width of the TAB item is generated according to the width of the TAB item. Init () is not triggered when initializing the data: this.init(), because the plugin is written directly to the dead data, the page is rendered directly, and we are the requested data, so the initial execution is not executed, the same watch can be used:

The following code is in TAB /index.vue watch:{... tabData(newVal) {this.init()
	}
}

Copy the code

We found that there was a 100px line when there was no data, so we just set the initial value to 0

The article details

When stored in the cloud database, _id will be automatically generated, so jump from the article list page to the details page, as long as the _ID field can be taken, and request in the details page.

Right-click in the Pages directory to create the Page-Details page. Since the content of the article is in markdown or rich text form, we can use the rich-text component, but this component is not good for the image preview, link jump, including the implementation of events. So we also use the Parse rich text parsing plug-in in the plug-in market, first implementing the list jump detail page:

// home. Vue <view class="cells"> // navigator hover 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)} // In page-details.vue, onLoad(e) {// In route, onLoad receives console.log(e)}Copy the code

  • Rich text parsing plug-in

Download zip into our project/colorui/components/parse, in app. Vue introduced in style @ import “colorui/components/parse/parse. CSS”; , used in 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}, ... // navigate(href, e){}Copy the code

The plugin supports rich text format and Markdown format. Markdown is used first

markdown

  • NPM init -y && CNPM install marked –save in the root directory
  • Import marked from ‘marked’
let str = `# uncertainty \r\n ## uncertainty \r\n ### uncertainty`
this.article = marked(str)

Copy the code

The rich text

onLoad(e) {
	this.article = '
      
< H1 > Hello < H2 > I'm fine
'
}, Copy the code

Details of the interface

Create pageDetails cloud function, upload and run; Initialize the article_details collection:

// The following code is db_init.json


"article_details": {
	"data": [{// The _id must be the same as that returned in the list
			"id": "5ebd3c9c3c6376004c5cedbc"."content": "# Hello is not sure hello"."date": "2020-05-18"}]."index": [{
		"IndexName": "id".// Index name
		"MgoKeySchema": { // Index rules
			  "MgoIndexKeys": [{
			      "Name": "id".// Index field
			      "Direction": "1" // Index direction, 1: ASC- ascending, -1: desC-descending}]."MgoIsUnique": true // Whether the index is unique}}}]Copy the code

The ID value of the article details I used corresponds to the _id value in the list to achieve the search; You only need to click the list to jump to upload the _ID to the detail page, and the cloud data can be obtained from the detail page

// The following code is in 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}},Copy the code

The discovery request is successful, the data is also uploaded just now, and the rich text format is the same; Attribute configuration you can use the official website

/parse/libs/ wxdiscode.js /parse/libs/ wxdiscode.js

If a TAB needs to be jumped, it should be a Web-view component. The web page uses the native navigation bar, and the third party page loaded by web-view has the highest hierarchy. App development using rich text parsing results may be different from small programs, you can try. If you parse rich text using rich-text tags, it may be difficult to change the internal styles and add events to the tags. If the function is simple, you can use string substitution to add style classes, such as:

Let STR = ‘

Hello, I don’t know.

‘Take Baidu rich text parsing as an example, general users will directly drag word documents into the rich text editor, the editor will automatically resolve into a DOM structure, there is no class name only inline style, style is relatively fixed. Because of this, if you go to the mobile end and you need to do your own string substitution (the dom string returned by the back end) add the style or add the class name.

  1. Text copy can be implemented
STR = '< divclass="wrapper">’ + str + '</div>'
Copy the code

Wrap the returned DOM string in the Wrapper class and set the wrapper style change.)

.wrapper{
	    user-select: text;
}
Copy the code

2. Add a class name to the image and center it with a maximum width of 100%

str = str.replace(/<img/g."<img class='my-img'")
Copy the code
.my-img{
	    display:block;    
	    max-width:100%;    
	    margin: 0 auto;
}
Copy the code

If the details page has a “like” request, and the list page is onLoad request, then exit the details page and return to the list page will not be requested (if using onShow, it will be requested again, but the list page will have paging query, play the list page will bring a lot of inconvenience), then update the list page “like” number. Uni can be used to confirm or unlike success.On (), the detail page triggers, and the list page listens. Same as Vue’s EventBus

Make side bounces out of bars

Since I only have two toggle buttons on my page, I put the profile picture and the Settings field in the side drawer:

// The following code is in cu-custom<view class="action" @tap="BackPage" v-if="isBack">
	<text :class="'cuIcon-' + icon"></text>
	<slot name="backText"></slot>
</view>Icon: {type: String, default: 'back'}Copy the code

For ICONS, we choose the cuicon-sort of ColorUI

Create components colorui/components/drawer/drawer. Vue. If icon is back or sort, display the sidebar, add a simple animation, and go to the code:

The following code in/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.3 s ease-in-out;
	}


	.drawer-container {
		position: fixed;
		left: 50%;
		height: 100%;
		top: 0;
		transform: translate3d(-50%, -50%, 0);
		transform-origin: center;
		transition: all 0.3 s ease-in-out;
		z-index: 99999;
		opacity: 0;
		background: #fff;
	}


	.drawer-left .drawer-container {
		left: 0;
		top: 50%;
		transform: translate3d(-100%, -50%, 0);
	}
</style>

Copy the code

In cu-custom/cu-custom.vue (layout styles can be written as you like) :

// The following code is in /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">
			<! - random avatar > 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' } }Copy the code

section

This section is the last part of the blog demo. It doesn’t have a lot of features, and it’s a little summary of how I use cloud functions. We can design their own ideas of their own small program, their own writing cloud function, xiaobian is just started, there are wrong places to write please correct; Friends who follow the implementation of the function can also expand themselves, such as the list page to achieve the skeleton screen, you can go to the plug-in market to learn to see more function implementation, introduced into their own projects. Ability is general, level is limited, thank you for reading!

Want to learn more exciting practical skills tutorial? Come and visit the Tooquine community.

If you think we did a good job, please like ❤️ ❤️