preface
Some time ago, I learned about the development of micro channel small program, said not to practice false handle, so I plan to masturbate a micro channel small program, and online e-commerce class small program is too much, so I chose the travel guide class small program to practice. This is my first time to write small procedures and the first time to write an article, please bear more deficiencies, thank you. Below I will share my problems and experience when writing small procedures, I hope to bring you help, but also welcome big guy correct. Finally, I would like to thank my teachers and classmates who helped me when I wrote the small program, as well as all the famous and unknown authors on Baidu who helped me. Finished with my nonsense, first on the project renderings.
Preparation before development
- Wechat developer tools
- VsCode
- Easy Mock
All pages of the project
Customize the top navigation bar component
Top navigation bar
Custom Components
"window": {
"navigationBarTextStyle": "black"// The navigation bar title color supports only black and white"navigationStyle": "custom"// Navigation bar style, only the following values are supported: default Default style custom Custom navigation bar, only the capsule button in the upper right corner is reserved}Copy the code
wxml
<view class='nav-wrap' style='height: {{height*2 + 20}}px; background-color:{{navbarData.backgroundColor}}; opacity:{{navbarData.opacity}}'>
<view style="width:100%; height:100%;"> <! -- City name --> <navigator URL ="/pages/destination/destination" hover-class="none">
<view class="nav-city" style='margin-top:{{height*2 + 20-36}}px; ' wx:if='{{navbarData.showMain}}'>
<text>{{navbarData.cityName}}</text>
<view class="downtips"></view>
</view>
</navigator>
<navigator url="/pages/search/search" hover-class="none"> <! -- Search box --> <view class="section" style='top:{{height*2 + 20-34}}px; ' wx:if='{{navbarData.showMain}}'> // The search box here is not an input component, just a view to click on and jump to the search page <view class='search_icon'>
<icon type='search' size='14px'></icon>
</view>
<view class='placehold'> search destination/sites/strategy < / view > < / view > < / navigator > < / view > <! <view wx:if="{{navbarData.title! = ' '}}" class='nav-title' style='line-height: {{height*2 + 44}}px; '> {{navbarData.title}} </view> <! --> <block wx:if="{{navbarData.showCapsule===1}}">
<view class='nav'>
<view class='nav_back' bindtap="_navback">
<image src='/images/back.png'></image>
</view>
<view class="line"></view>
<view class='nav_home' bindtap="_backhome">
<image src='/images/home.png'></image>
</view>
</view>
</block>
</view>
Copy the code
Elements in a component can be controlled by the data passed into the component by the current page
Js wrote two route jump functions, wechat applets official documents have a very detailed introduction, here is not much to say.
Login screen
wx.getUserInfo
<button style='background:green; color:#fff' open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo"> approve authorization </buttonCopy the code
Small program in the authorization to allow access to user information, and will pop up the location authorization box to obtain the current location of the user, to render the data on the main page. Call the interface wx.getLocation given by the small program (user authorization is required) to obtain the longitude and latitude, and then use the obtained longitude and latitude API provided by baidu Map open platform to the small program to obtain the name of the current city, and put the city name into the cache, so that the main page can get it.
## Note: Using wx.getLocation() needs to be configured in app.json
"permission": {
"scope.userLocation": {
"desc": "Little program will get your location information."}}Copy the code
Login interface JS
/ / miniprogram/pages/login/login. Js const app = getApp () Page ({/ * * * * / data Page of the initial data: {show:false, // The top navigation bar data navbarData: {showCapsule: 0, // whether to display the upper left icon 1 to display 0 to display title:'Hornet's Nest'// Title backgroundColor in the middle of the navigation bar:'#354a98', / /'#354a98'Height: app.globaldata. Height * 2 + 20,},bindGetUserInfo(res) {
let that =this
let info = res;
if (info.detail.userInfo) {
wx.login({
success: function(res) {that getPlaceData ()}}}}), function / * * * life cycle - listening page load * / onLoad:function (options) {
letthat = this; Wx.getuserinfo ({success:}) {wx.getUserinfo ({success:}) {wx.getUserinfo ({success:}) {wx.getUserinfo ({success:});function (res) {
wx.switchTab({
url: '/pages/main/index'
})
},
fail(err) {
that.setData({
show: true})})}, // Get the city name getCityName(location) {return new Promise((resolve, reject) => {
let that = this;
var e = {
coord_type: "gcj02",
output: "json",
pois: 0,
ak: ' ',// Put your AK key on the key application see baidu Map opening platform link sn above:"",
timestamp: ""
};
e.location = location;
wx.request({
url: "https://api.map.baidu.com/geocoder/v2/",
data: e,
header: {
"content-type": "application/json"
},
method: "GET",
success: function (t) {
let currentCity = t.data.result.addressComponent.city;
if (currentCity.slice(currentCity.length - 1) == "The city") {
currentCity = currentCity.slice(0, currentCity.length - 1)
}
wx.setStorageSync('currentCity', currentCity) resolve(currentCity) // Request city data from city name}})})}, // get latitude and longitudegetLocation() {
return new Promise((resolve, reject) => {
wx.getLocation({
type: 'wgs84',
success(res) {
const latitude = res.latitude
const longitude = res.longitude
let location = latitude + ', '+ longitude console.log(longitude console.log) resolve(longitude console.log)getPlaceData() {// Get geographic informationlet that = this
this.getLocation().then((val) => {
return that.getCityName(val)
}).then(()=>{
wx.switchTab({
url: '/pages/main/index'})})}})Copy the code
The main page
Write a small program I don’t know when there are two main page style, when I know has written a lot, so there is no written component, the code looks very long, this is my mistake (MangFu), hope you’re thinking what good writing small programs, must think good page structure of small programs to do otherwise would like me, It would take a lot to change.
- Common City Page
- Hot City page
When entering the homepage, the page will first obtain the city name in the cache, and then request data through the city name. Then, according to the ISHOT attribute in the data requested, if the ISHOT attribute is true, the page of the popular city will be displayed, otherwise, the page of the common city will be displayed
‘My’ page
Attractions Details Page
For one reason or another, more than half of the data in the page is not included in the Easy Mock. Hornet’s nest is known for big data and has too many TTMS.
Continent/country/city listing page
The layout of this page is divided into three parts. Absolute positioning is used for the head search box, absolute positioning is used for the list of continents on the left, and the countries on the right are the scrollview component of a wechat mini program
wxml
<! -- pages/destination/destination.wxml --> <nav-bar navbar-data='{{navbarData}}'></nav-bar>
<view class="destination" style='top: {{height}}px'> <! --> <view class="des_head">
<navigator url="/pages/search/search" hover-class="none">
<view class="des_search">
<view class="des_search_icon">
<icon type='search' size='30rpx' color="# 000000"> < / icon > < view > search destination < / view > < / navigator > < / view > <! -- left --> <view class="des_continents">
<view class="des_continent {{curIndex===index? 'add':''}}}" wx:for="{{continents}}" wx:for-item="continent" wx:key='{{index}}' data-index='{{index}}' bindtap="switch_des">
<view class='des_continent_name {{curIndex===index?" on":""}}}'>{{continent.name}}</view> </view> </view> <! -- right --> <scroll view class='des_cities' scroll-y>
<block wx:if="{{curIndex==0}}">
<view class="des_cities_content" wx:for="{{continents[curIndex].cities}}" wx:key="{{index}}" wx:for-item="des_city">
<view class="des_cities_title">{{des_city.title}}</view>
<view class="des_city" wx:for="{{des_city.city}}" wx:key="{{index}}" bindtap='goMain' data-city_name="{{item.city_name}}">
{{item.city_name}}
</view>
</view>
</block>
<block wx:else>
<view class="des_area" wx:for="{{continents[curIndex].cities}}" wx:key="{{index}}" wx:for-item="des_city" bindtap='goMain' data-city_name="{{des_city.city_name}}">
<view class="des_img">
<image src="{{des_city.img}}" />
</view>
<view class="des_city_name">{{des_city.city_name}}</view>
</view>
</block>
</scroll-view>
</view>
Copy the code
js
/ / pages/destination/destination. Js const app = getApp () Page ({/ * * * * / data Page of the initial data: {<! --> navbarData: {showCapsule: 1, // whether to display the upper left icon 1 to display 0 to display the title:'Destination switch'// Title backgroundColor in the middle of the navigation bar:'#fff'Height: app.globaldata. Height * 2 + 20, continents: [], curIndex: 0}, <! Switch_des (e) {switch_des(e) {switch_des(e) {letcurIndex = e.currentTarget.dataset.index; this.setData({ curIndex, }) }, <! - right click events of country/city, access to click on the binding on the element of the name of the country/city, in the cache, and jump to the home page - > goMain (e) {const city_name = e.c. with our fabrication: urrentTarget. Dataset. The city_name; wx.setStorageSync('currentCity', city_name)
wx.switchTab({
url: '/pages/main/index'})}, /** * lifecycle function -- listen for page load */ onLoad:function (options) {
letthat = this <! --> wx.request({url:'https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/continents',
success:(res)=>{
that.setData({
continents: res.data.continents
})
}
})
}
}
Copy the code
Search page
Implemented functions
Click toggle list
Take the home page for example
In fact, all switch list functions are similar, the implementation method is to set a custom attribute (data-*) on the clicked element as a unique index value, bind a click event with bind-tap, obtain the unique index value through the click event, and then go to the data source to find the desired content through the unique index value. Then control the content displayed on the page through data, and set a data such as McUrIndex in the data source to represent the current selected element, which is used to distinguish it from other elements and display different styles.
wxml
<view class='menu_list'> <! -- {{mcurIndex===index?"on":""}} add a class name 'on' --> <view class= if its index value is the index value of the currently selected element'list {{mcurIndex===index?" on":""}}' wx:for="{{placeData.allGuide}}" data-mindex="{{index}}" bindtap='selected_menu' wx:key="{{index}}">
{{item.name}}
</view>
</view>
Copy the code
js
selected_menu(e) {
this.setData({
mcurIndex: e.target.dataset.mindex,
size: 0,
showend: false}) <! --> this.bitiyan()} --> this.bitiyan()}Copy the code
Sliding the page changes the visibility and pull-up loading of the top navigation bar
-
Slide the page to change the visibility of the top navigation bar
Take the home page for example
scroll-view
<scroll-view class="main_scro" scroll-y bindscroll="scroll" bindscrolltolower="bindDownLoad">
</scroll-view>
Copy the code
js
scroll(e) {
let opacity = 0;
if (e.detail.scrollTop < 60) {
opacity = (e.detail.scrollTop / 100).toFixed(1);
} else {
opacity = 1;
}
this.data.navbarData.opacity = opacity;
if (e.detail.scrollTop<10){
this.setData({
shownav: false})}else{
this.setData({
shownav: true
})
}
this.setData({
navbarData: this.data.navbarData,
})
}
Copy the code
-
Pull on loading
Take the home page for example
The implementation method here is to add the BindScRolltolower attribute to the Scroll View component, which will trigger the bindScRolltolower bound event when the page hits the bottom.
<scroll-view class="main_scro" scroll-y bindscroll="scroll" bindscrolltolower="bindDownLoad">
</scroll-view>
Copy the code
bindDownLoad() {
letpart = 0; // The length of the data already displayedletall = 0; // Total data length <! -- Determine whether the current city is a hot city -->if(this.data.ishot) {//else {
if (this.data.mcurIndex === 0) {
part = this.data.cur_view.length * 2;
all = this.data.placeData.allGuide[this.data.mcurIndex].content[this.data.hlcurIndex].content.length;
} else {
part = this.data.cur_view.length;
all = this.data.placeData.allGuide[this.data.mcurIndex].content.length;
}
if (part < all) {
wx.showLoading({
title: 'Loading'
})
setTimeout(() => {
this.bitiyan(this.data.placeData)
wx.hideLoading()
}, 1000)
} else{<! --> this.setData({showend:true})}}}Copy the code
There are a few things to note about the Scroll view component
- Make sure to set the height of the scroll after setting the vertical scroll. Sometimes when you set the height to 100%, when you slide to the bottom, it will not be complete. Or if the parent element has a margin/padding set, the height of the Scrollview component should be subtracted from the margin/padding
- When setting horizontal scrolling, note that the elements to be slid in the Scrollview cannot float. Display: Flex does not work in a scroll view that wraps large boxes around elements that need to be slid; Dislay :inline-block is used for horizontal layout of elements that need sliding in the Scroll view. The big box that wraps the Scroll view has a clear width and style –> overflow:hidden; white-space:nowrap;
Collection function
I wrote the favorite function in a component, originally wanted to be the same as the top component, for multiple pages to use, but later because the page is only one useful to this component, I will not describe this component separately, and this component and the top component is basically similar.
When you click on a scenic spot, it will trigger the click event. I believe you have seen the list switch function, and already know how to use bind-tap, so I won’t repeat it here. Here is to get the custom properties on the element, through routing method to the details of the page, the details page, according to the data passed to obtain the corresponding data in data source, and then pass the data to the component, when click the details on page collection button, will trigger a binding event, and then will update collectData favorites data in the cache. The ‘My’ page displays the data in the favorites
Js details page
<! -- life cycle function, listen for page load --> onLoad:function(options) { <! -- Options contains the parameters passed -->letname = options.name; Enclosing getinfo (name)}, <! Getinfo (name){<! Set collectData to an empty array if it does not already exist.let collectData = wx.getStorageSync('collectData') | | [];if (collectData.filter(e => e.name === name).length > 0) {
this.setData({
placeData: collectData.filter(e => e.name === name)[0]
})
} else {
let placeData = wx.getStorageSync('placeData')
let view = placeData.allGuide[0].content.map(e => e.content)
let newView = []
for (leti = 0; i < view.length; i++) { newView.push(... view[i]) } this.setData({ placeData: newView.find(e => e.name === name) }) } this.setBottom(); }, <! Set the data to be passed to the bottom component -->setBottom(){
this.data.bottomData.placeData = this.data.placeData;
let bottomData = this.data.bottomData;
this.setData({
bottomData
})
}
Copy the code
Js for the bottom component
/ / components/bottom/bottom. Js const app = getApp () Component ({/ * * * a list of attributes for the Component * / properties: {bottomData: {// Data passed by the parent page, variable name self - namedtype: Object,
value: {},
observer: function(newVal, oldVal) {}}, /** * the initial data of the component */ data: {height:' '
},
attached: functionThis.setdata ({share: app.globaldata.share}) // Define the height of the navigation bar to align this.setData({height: App.globaldata.height})}, /** * list of components */ methods: {<! Click the "Favorites" button to trigger the event -->collected() {<! -- Collectors,collectors,collectors,collectors,collectors,collectorslet{isCollect,collectors} = this.data.bottomData.placeData; isCollect = ! isCollect; this.data.bottomData.placeData.isCollect = isCollect;let collectData = wx.getStorageSync('collectData') | | [];if(isCollect){
wx.showToast({
title: 'Collected successfully',
icon: 'success',
duration: 2000
})
collectors++;
collectData.push(this.data.bottomData.placeData);
}else{
wx.showToast({
title: 'Uncollected',
icon: 'success', duration: 2000 }) collectors--; collectData = collectData.filter(e => e.name ! = this.data.bottomData.placeData.name) } this.data.bottomData.placeData.collectors = collectors; <! Wx.setstoragesync (wx.setStoragesync)'collectData', collectData)
let bottomData = this.data.bottomData;
this.setData({
bottomData
})
}
}
})
Copy the code
The search function
Effect of a
The search function is realized through the bindinput attribute on the native component input, which triggers the bindinput attribute binding method when the keyboard enters, and then obtains the input value in real time, and then puts the obtained value into the request address to request data, and then puts the requested data into the data source of the page. When the requested data is not empty, the page will display all relevant data obtained, such as effect one. The bindConfirm property binding event on the input box will be triggered when the search button is pressed, and the page will display the first requested data, such as effect two.
wxml
<input style='width:500rpx' bindconfirm='confirm' confirm-type='search' focus='true' placeholder="Search destinations/attractions/Walkthroughs" bindinput='search'></input>
Copy the code
js
// pages/search/search.js const app = getApp() Page({/** ** *) 1, // Whether to display the icon in the upper left corner 1 means to display 0 means not to display the title:'Hornet's Nest'// Title backgroundColor in the middle of the navigation bar:'#ffffff', / /'#354a98'
city: ' ',
opacity: 1,
showMain: 0
},
height: app.globalData.height * 2 + 20,
result: [],
searchparams: ' ',
show: true,
searchHistory: [],
showResult: false,
showconfirm: false, placedata: [] }, <! -- Clear history -->clear() {
this.setData({
searchHistory: []
})
wx.removeStorageSync('searchHistory')}, <! Click the search button on the keyboard --> Confirm (e) {if(e.detail.value ! =' ') {
let searchHistory = wx.getStorageSync('searchHistory') | | []if (searchHistory.filter(a => a === e.detail.value).length === 0) {
searchHistory.push(e.detail.value)
wx.setStorageSync('searchHistory', searchHistory)
}
if (this.data.result.length > 0) {
let currentCity = this.data.result[0].name;
this.getCityDataByName(currentCity);
}
this.setData({
show: false,
showResult: false,
showconfirm: true})}}, <! Gotomain (e) {wx.setStoragesync (wx.setStoragesync)'currentCity', e.currentTarget.dataset.name)
wx.switchTab({
url: '/pages/main/index'})}, <! Gosearch (e) {gosearch(e) {gosearch(e) {gosearch(e) {gosearch(e) {gosearch(e) {let that = this
wx.request({
url: `https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/search?name=${e.currentTarget.dataset.name}`,
success: (res) => {
if (res.data.data.length > 0) {
that.getCityDataByName(res.data.data[0].name)
} else {
this.setData({
show: false,
showResult: false,
showconfirm: trueGetCityDataByName (cityname) {getCityDataByName(cityname) {let that = this
wx.request({
url: 'https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/china',
success: (res) => {
letplacedata = []; placedata.push(... res.data.data.china.filter(e => e.chName === cityname)) that.setData({ placedata, show:false,
showResult: false,
showconfirm: true})})}, <! -- the event triggered when the keyboard enters --> search(e) {let that = this
wx.request({
url: `https://www.easy-mock.com/mock/5ca457f04767c3737055c868/example/mafengwo/search?name=${e.detail.value}`,
success: (res) => {
if (res.data.data.length > 0) {
that.changecolor(res.data.data, e.detail.value)
} else {
that.setData({
result: [],
searchparams: ' ',
showResult: false})}})}, <! Changecolor (result, searchParams) {-- change name color --> changecolor(result, searchParams) {for (let j = 0; j < result.length; j++) {
let i = result[j].name.search(searchparams);
let left = result[j].name.slice(0, i),
mid = result[j].name.slice(i, i + searchparams.length),
right = result[j].name.slice(i + searchparams.length);
result[j].left = left;
result[j].mid = mid;
result[j].right = right;
}
this.setData({
result,
searchparams,
show: false,
showResult: true,
showconfirm: false})},_navback() {wx.navigateBack({delta: 1})}, /** *function() {<! Get the search history in the cache and put it into the data sourcelet searchHistory = wx.getStorageSync('searchHistory') || []
this.setData({
searchHistory
})
}
Copy the code
I wrote this API using Easy Mock
Easy Mock address link
Easy Mock code
{
"data": function({
_req
}) {
leti = 0, <! _data = [{name: {name: {name: {name: {name: {name: {name: {name: {name: {name: {name: {name: {name: {name:'Asia'.type: 'Destination'
},
{
name: 'Europe'.type: 'Destination'
},
{
name: 'Oceania'.type: 'Destination'
},
{
name: 'Africa'.type: 'Destination'
},
{
name: 'North America'.type: 'Destination'
},
{
name: 'South America'.type: 'Destination'
},
{
name: 'Antarctica'.type: 'Destination'}, <! --_req is the object wrapped by easyMock, _req.query(parses the query argument string and returns it as an object, or an empty object if there is no query argument numeric string); --> name = _req.query.name;if(name ! =' ') {<! -- When the input value is not null -->let result = [];
let data = []
for (letj = 0; j < result.length; j++) { <! The eval() function evaluates a string and executes the JavaScript code inside it. This is used to dynamically pass parameters to regular expressions.if (eval('/' + name + '/').test(result[j].name)) { data.push(result[j]) } <! -- break the loop when 8 matches are found -->if (data.length > 8) break;
}
return data
} else{<! -- Returns an empty array when the input value is null -->return[]}}}Copy the code
Hot City animation
Since the animation only has 6 elements, there is no need to create an array traversal, just write 6 boxes, initialize their styles, and let them go to their original positions. Wechat applet provides an API for creating animation instances, wx.createAnimation
wxml
<view class='video a' animation="{{animation1}}" data-index='0' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[0].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video b' animation="{{animation2}}" data-index='1' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[1].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video c' animation="{{animation3}}" data-index='2' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[2].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video d' animation="{{animation4}}" data-index='3' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[3].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video e' animation="{{animation5}}" data-index='4' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[4].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
<view class='video f' animation="{{animation6}}" data-index='5' bindtap="_play">
<view class='context'>
<text>{{placeData.vlog[5].title}}</text>
</view>
<view class='vdoIcon'>
<image src='/images/play.png'></image>
</view>
</view>
Copy the code
wxss
. A {opacity: 0.9; }.b{transform: translate(170rpx,-110rpx) scale(0.8); Opacity: 0.8; }.c{transform: translate(210rpx,-250rpx) scale(0.7); Opacity: 0.7; }. D {transform: translate(10rpx,-350rpx) scale(0.6); Opacity: 0.6; }. E {transform: translate(-250rpx,-290rpx) scale(0.8); Opacity: 0.5; }.f{transform: translate(-300rpx,-130rpx) scale(0.9); Opacity: 0.8; }Copy the code
js
// The path of the animation translate:function(I) {// Get the screen width for adaptationletwindowwidth = this.data.windowWidth; // Animation running status status[X-axis offset, Y-axis offset,scale scaling,opacity] is also the animation running pathletStatus = [[170-110, 0.8, 0.7], [210-250, 0.7, 0.6], [10, 350, 0.6, 0.5], [250, 300, 0.8, 0.7], [300, 130, [0, 0, 1, 0.9]];let x = 0,
y = 0,
scale = 0,
opacity = 0;
for (let j = 0; j < 6; j++) {
let animationName = 'animation'+ (j + 1); x = status[(i + j) % 6][0] / 750 * windowwidth; y = status[(i + j) % 6][1] / 750 * windowwidth; scale = status[(i + j) % 6][2]; opacity = status[(i + j) % 6][3]; this.animation.translate(x, y).scale(scale).opacity(opacity).step() this.setData({ [animationName]: This.animation.export ()// Exports the animation data to the component's animation property})}},hotCityAnimation() {
leti = 0; <! Animation = wx.createAnimation({duration: 2000, timingFunction:'ease',})let that = this
let anicontrol = this.data.anicontrol
anicontrol = setInterval(function() {
that.translate(i)
if (i == 5) {
i = -1;
}
i++;
}, 3000)
this.setData({
anicontrol
})
}
Copy the code
The important thing to note here is that since this is an animation written on the Tabbar page and setInterval is used, the registered callback will execute at the specified interval (in milliseconds), meaning that even if you jump to another page, the animation will still be running, and when you return to the home page, the animation will run in error. So in the home page onHide cycle function, listen for page hide to clear the timer, and remove the animation instance.
onHide: function() {
let anicontrol = this.data.anicontrol;
clearInterval(anicontrol)
this.setData({
animation1: ' ',
animation2: ' ',
animation3: ' ',
animation4: ' ',
animation5: ' ',
animation6: ' '})}Copy the code
About CSS
Write this small program I did not use any UI framework, which has disadvantages, also has advantages, disadvantages is the code progress is slow, the advantage is to increase their understanding of CSS a lot. Anyone who wants to use a UI framework can use WeUI. You can find detailed instructions on how to use it in the link.
conclusion
Because of time and energy, the small program only wrote a few pages and a small part of the function, in the process of writing the project also found their own many deficiencies, so suffered a lot, but also learned a lot, can be said to pain and happy. Hopefully this article will be of some help to you if you want to write a small program. GitHub source code here, need to fetch.