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:
- Swiper can be used directly by swiper.
- 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;
- Pull-up load more: Applets have their own life cycle
onReachBottom
The default distance from the bottom is 50px. If you want to change it, you can set it in the style of the pageonReachBottomDistance
field - Drop-down Refresh: Applet page life cycle
onPullDownRefresh
At 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
- 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.
- 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 ❤️ ❤️