Skip to main content

业务

列表通用模板

小程序列表通用模板代码:首部搜索框,下方数据列表,无数据就显示暂无数据

  • 如果数据全部返回,在前端搜索过滤
  • 如果数据分页返回,上拉加载、下拉刷新,搜索、清空
  • 顶部项目选择

1. 数据全部返回,搜索过滤由前端处理

小程序 UI 组件库:Vant Weapp

index.wxml
<van-search
value="{{ searchValue }}"
placeholder="请输入项目名称"
shape="round"
bind:search="onSearch"
bind:change="onSearch"
bind:clear="onClear"
/>
<view wx:if="{{projectList.length>0}}">
<van-cell-group>
<navigator wx:for="{{projectList}}" wx:key="id" url="/pages/project/detail?proId={{item.id}}">
<van-cell title="{{item.proName}}" label="{{item.proNo}}" />
</navigator>
</van-cell-group>
</view>
<van-empty wx:else description="暂无数据" />
index.json
{
"navigationBarTitleText": "项目管理",
"usingComponents": {
"van-search": "@vant/weapp/search/index",
"van-cell": "@vant/weapp/cell/index",
"van-cell-group": "@vant/weapp/cell-group/index",
"van-empty": "@vant/weapp/empty/index"
}
}
index.js
import { getProjectList } from '../../utils/api.js';

Page({
data: {
searchValue: '',
projectList: [],
originProList: [],
loading: false
},
onLoad() {
this.searchProjectList();
},
searchProjectList() {
this.setData({ loading: true });
getProjectList().then(res => {
this.setData({ loading: false, projectList: res.data.data, originProList: res.data.data });
});
},
onSearch(e) {
this.setData({ searchValue: e.detail });
if (!e.detail) {
this.setData({ projectList: this.data.originProList });
} else {
const arr = this.data.projectList.filter(ele => ele.proName.indexOf(e.detail) !== -1);
this.setData({ projectList: arr });
}
},
onClear() {
this.setData({ projectList: this.data.originProList });
}
});

2. 分页返回数据

index.wxml 去掉了bind:change="onSearch",其余同上。index.json 如果要开启下拉刷新,需要添加:"enablePullDownRefresh": true

index.js
import { getProjectList } from '../../utils/api.js';

Page({
data: {
searchValue: '',
listData: [],
pageNum: 1,
total: 0,
loading: false
},

onLoad() {
this.getList(1);
},

/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
if (!this.data.loading && this.data.pageNum < Math.ceil(this.data.total / 10)) {
this.getList(this.data.pageNum + 1);
}
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
//启用标题栏显示加载状态
wx.showNavigationBarLoading();
// 调用相关方法,重置数据
this.setData({ listData: [], searchValue: '', pageNum: 1, total: 0 });
// 重新发起请求
this.getList(1);

setTimeout(() => {
wx.hideNavigationBarLoading(); //隐藏标题栏显示加载状态
wx.stopPullDownRefresh(); //结束刷新
}, 2000); //设置执行时间
},

getList(pageNum) {
this.setData({ loading: true });
getProjectList({ pageNum, pageSize: 10, proName: this.data.searchValue }).then(res => {
this.setData({
loading: false,
listData: this.data.listData.concat(res.data.rows),
pageNum,
total: res.data.total
});
});
},

onSearch(e) {
this.setData({ listData: [], searchValue: e.detail, pageNum: 1, total: 0 });
this.getList(1);
},

onClear() {
this.setData({ listData: [], searchValue: '', pageNum: 1, total: 0 });
this.getList(1);
}
});
  • 总页数 = Math.ceil(总条数 / 每页显示的条数)

生成随机数

文档地址

wx.getRandomValues({
length: 30, // 生成 30 个字节长度的随机数
success(res) {
that.setData({ inspectFileUuid: wx.arrayBufferToBase64(res.randomValues) });
}
});

图片上传

使用原生方法上传

使用wx.chooseMedia()选取图片,默认支持从相册选择和拍照上传,下方是单独区分的。再使用wx.uploadFile()上传到服务器, 但是该方法不支持多图片上传,可以封装成 promise 并行上传图片

upload.wxml
<view>
<view>
<van-button type="primary" bindtap="photoAlbum">相册上传</van-button>
<van-button type="info" bindtap="photograph">拍照上传</van-button>
</view>
<view>
<view class="num">
<text>图片上传</text>
<text>{{imageList.length}}/9</text>
</view>
<view>imageList: {{imageList}}</view>
<block wx:for="{{imageList}}" wx:key="*this">
<view class="q-image-wrap">
<image
class="q-image"
style="width: 300rpx; height: 300rpx"
src="{{item}}"
mode="aspectFill"
data-idx="{{index}}"
bindtap="handleImagePreview"
></image>
<view data-idx="{{index}}" bindtap="removeImage">删除</view>
</view>
</block>
</view>
<van-button type="primary" block color="#409eff" bindtap="submitForm">保存</van-button>
</view>
upload.js
Page({
data: {
imageList: []
},

// 相册上传
photoAlbum() {
let that = this;
const imgNum = this.data.imageList.length;
if (imgNum >= 9) {
wx.showToast({ title: '最多上传9张图片', icon: 'loading', duration: 2000 });
return false;
} else {
imgNum = 9 - imgNum;
}
wx.chooseMedia({
count: imgNum,
mediaType: ['image'],
sourceType: ['album'],
success(res) {
const arr = [];
res.tempFiles.forEach(e => {
arr.push(e.tempFilePath);
});
that.setData({ imageList: that.data.imageList.concat(arr) });
},
fail(res) {
console.log('接口调用失败的回调函数', res);
}
});
},
// 拍照上传
photograph() {
let that = this;
const imgNum = this.data.imageList.length;
if (imgNum >= 9) {
wx.showToast({ title: '最多上传9张图片', icon: 'loading', duration: 2000 });
return false;
} else {
imgNum = 9 - imgNum;
}
wx.chooseMedia({
count: imgNum,
mediaType: ['image'],
sourceType: ['camera'],
success(res) {
const arr = [];
res.tempFiles.forEach(e => {
arr.push(e.tempFilePath);
});
that.setData({
imageList: that.data.imageList.concat(arr)
});
},
fail(res) {
console.log('接口调用失败的回调函数', res);
}
});
},
// 图片预览
handleImagePreview(e) {
const index = e.currentTarget.dataset.idx;
const images = this.data.imageList;
wx.previewImage({
current: images[index], //当前预览的图片
urls: images //所有要预览的图片
});
},

// 删除图片
removeImage(e) {
const that = this;
const imgList = this.data.imageList;
const index = e.currentTarget.dataset.idx;
wx.showModal({
title: '提示',
content: '确定要删除此图片吗?',
success(res) {
if (res.confirm) {
imgList.splice(index, 1);
} else if (res.cancel) {
return false;
}
that.setData({ imageList: imgList });
}
});
},

// wx.uploadFile() 不支持多图片上传。可以封装成 promise
wxUploadFile(filePath) {
let that = this;
return new Promise((resolve, reject) => {
wx.uploadFile({
url: `${config.url.fileServer}/upload`,
filePath,
name: 'file',
formData: { user: 'test' }, // 要传递的参数
method: 'POST',
header: { 'Content-Type': 'multipart/form-data' },
success: resolve,
fail: reject
});
});
},

// 保存提交
submitForm(e) {
const arr = [];
//将选择的图片组成一个Promise数组,准备进行并行上传
for (let path of this.data.imageList) {
arr.push(this.wxUploadFile(path));
}

wx.showLoading({ title: '正在上传...', mask: true });

// 开始并行上传图片
Promise.all(arr)
.then(res => {
// 上传成功,获取这些图片在服务器上的地址,组成一个数组
return res.map(item => JSON.parse(item.data).url);
})
.catch(err => {
console.log('upload images error:', err);
})
.then(urls => {
// 调用保存图片的后端接口
// return saveImages({
// projectID: 1,
// images: urls
// })
})
.then(res => {
// 保存图片成功,返回上一页
const pages = getCurrentPages();
const currPage = pages[pages.length - 1];
const prevPage = pages[pages.length - 2];
wx.navigateBack();
})
.catch(err => {
console.log(err);
})
.then(() => {
wx.hideLoading();
});
}
});

使用 vant 小程序组件库

vant 地址

<van-uploader
file-list="{{ fileList }}"
use-before-read
bind:before-read="beforeRead"
bind:after-read="afterRead"
deletable="{{ true }}"
/>
  beforeRead(event) {
const { file, callback } = event.detail
callback(file.type === 'image')
},

afterRead(event) {
const that = this
const { file } = event.detail
wx.uploadFile({
url: `${config.url.apiServer}/manage/file/upload`,
filePath: file.url,
name: 'file',
formData: { businessId: that.data.guid },
header: { 'Content-Type': 'multipart/form-data' },
success(res) {
const data = JSON.parse(res.data)
const kfc = result.data.data
const { fileList = [] } = that.data
let url = kfc.serverUrl
let files = kfc.fileList
files.forEach(f => {
fileList.push({ ...file, url: url + f.fileUrl })
})
that.setData({ fileList })
},
fail(res) {
console.log('error', res)
}
})
}

文件下载

<view style="color: #1990FF" wx:for="{{fileList}}" wx:key="fileUUid" data-url="{{item.fileUrl}}" bindtap="downloadFile">
{{item.name}}
</view>
 downloadFile(e) {
const url = this.data.fileServer + e.currentTarget.dataset.url
wx.downloadFile({
url: url,
success(res) {
if (res.statusCode === 200) {
wx.openDocument({
filePath: res.tempFilePath,
showMenu: true,
success(res) {
console.log('打开文档成功')
}
})
}
}
})
}

放大缩小页面

可以利用 css 的 transform 属性,设置 scale

<button bindtap="zoomIn">放大</button>
<button bindtap="zoomOut">缩小</button>

<view class="screen" style="width: 100%; height: 100%; transform-origin: 0 0; transform: scale({{scale}});">
<view>content</view>
</view>
Page({
data: {
scale: 1
},

// 点击放大按钮
zoomIn() {
let scale = this.data.scale + 0.1;
this.setData({
scale: scale > 2 ? 2 : scale // 设置最大缩放比例
});
},

// 点击缩小按钮
zoomOut() {
let scale = this.data.scale - 0.1;
this.setData({
scale: scale < 0.1 ? 0.1 : scale // 设置最小缩放比例
});
}
});

Echarts

使用 echarts-for-weixin

<view id="container">
<ec-canvas id="mychart-dom" canvas-id="mychart-pie" ec="{{ ecOption }}"></ec-canvas>
</view>

在 index.json 中引入组件

{
"usingComponents": {
"ec-canvas": "../../components/ec-canvas/ec-canvas"
}
}

index.js

import * as echarts from '../../components/ec-canvas/echarts';

Page({
data: {
ecOption: {
lazyLoad: true // 延迟加载图表,可以在获取数据后再初始化数据
}
},

onLoad(option) {
this.getBarData();
},

getBarData() {
getStatistic().then(res => {
this.initChart(res.data);
});
},

initChart(data) {
const that = this;
const chartDom = this.selectComponent('#container');
chartDom.init((canvas, width, height, dpr) => {
const chart = echarts.init(canvas, null, { width, height, devicePixelRatio: dpr });
canvas.setChart(chart);
const option = {
// ....
};
chart.setOption(option);

// 如果需要更新图
that.gtChart = chart;

return chart;
});
},

// 更新图
toggle(e) {
this.gtChart &&
this.gtChart.setOption({
series: [
{
name: 'a',
type: 'line',
data: this.data.data1
},
{
name: 'b',
type: 'line',
data: this.data.data2
}
]
});
}
});

如果需要更新图,可以在初始化时赋值给 this,如that.gtChart = chart,这里的 gtChart 不需要在 data 对象里定义

在 tab 页签切换时,图表不显示

如果有两个页签,每个页签下面都有一个图,且图的类型一样,比如都是折线图,那么可以只设置一个图表容器,在初始时渲染,在切换时更新数据重新渲染。

图和内容重叠

如果确认配置图表没有问题,但是页面上的图表显示位置有问题。可以排查图表前面的元素,如果前面有别的元素且数据是从后台获取的,那么可以先给前面的元素设置一个占位。可能是数据还没返回,图表就已经渲染显示了,等到数据返回后,撑起了内容的高度,就会把上面的内容挤下去了,导致和图表重叠了

自定义 tabBar

文档

1、配置app.json

{
"tabBar": {
"custom": true,
"color": "#000000",
"selectedColor": "#000000",
"backgroundColor": "#000000",
"list": [
{
"pagePath": "page/home/index",
"text": "首页"
},
{
"pagePath": "page/my/index",
"text": "我的"
}
]
},
"usingComponents": {}
}

2、在根目录下创建custom-tab-bar目录,在这里编写自定义组件。

注意:在配置pagePath时,以/开始。

index.js
Component({
data: {
selected: 0,
color: '#000000',
selectedColor: '#000000',
list: [
{
pagePath: '/pages/home/index',
iconPath: '/images/icon/home.png',
selectedIconPath: '/images/icon/home-active.png',
text: '首页'
},
{
pagePath: '/pages/my/index',
iconPath: '/images/icon/my.png',
selectedIconPath: '/images/icon/my-active.png',
text: '我的'
}
]
},
methods: {
switchTab(e) {
const data = e.currentTarget.dataset;
const url = data.path;
wx.switchTab({ url });
this.setData({ selected: data.index });
}
}
});
index.json
{
"component": true
}
index.wxml
<view class="tab-bar">
<view class="tab-bar-border"></view>
<view
wx:for="{{list}}"
wx:key="index"
class="tab-bar-item"
data-path="{{item.pagePath}}"
data-index="{{index}}"
bindtap="switchTab"
>
<image src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></image>
<view style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</view>
</view>
</view>
index.wxss
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: white;
display: flex;
padding-bottom: env(safe-area-inset-bottom);
}

.tab-bar-border {
background-color: rgba(0, 0, 0, 0.33);
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
transform: scaleY(0.5);
}

.tab-bar-item {
flex: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}

.tab-bar-item image {
width: 45px;
height: 25px;
}

.tab-bar-item view {
font-size: 10px;
}

官方文档里强调:如需实现 tab 选中态,要在当前页面下,通过 getTabBar 接口获取组件实例,并调用 setData 更新选中态。

在官方示例里,在各个 tab 页面里使用了Component,如下:

Component({
pageLifetimes: {
show() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 0
});
}
}
}
});

但是在实际使用中,没有调用getTabBar方法似乎也可以更新选中态,按照下面的方式从效果上看似乎也更新了

home.js
Page({
onShow() {
this.getTabBar().setData({
selected: 0
});
}
});

自定义导航栏

1、在需要自定义导航栏的页面的index.json里配置"navigationStyle": "custom",这样只保留右上角的胶囊按钮。

{
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"usingComponents": {
"custom-nav-bar": "../../components/custom-nav-bar/index"
}
}

2、在 components 目录里创建custom-nav-bar目录,编写自定义导航栏组件

设置 swiper 间隔

给 swiper 的每一项之间设置间隔,

关键点:next-margin="-30rpx"margin-right: 30rpx;

<swiper class="wrap" display-multiple-items="2" previous-margin="30rpx" next-margin="-30rpx">
<swiper-item wx:for="{{cultureData}}" wx:key="id">
<view class="item" style="background-image: url({{item.url}});">
<navigator url="/pages/culture/index?id={{item.id}}" class="full">{{item.columnname}}</navigator>
</view>
</swiper-item>
</swiper>
.wrap {
position: relative;
width: 100%;
height: 280rpx;
}
.full {
width: 100%;
height: 100%;
}
.item {
position: relative;
height: 100%;
margin-right: 30rpx;
}

rich-text 图片宽度自适应

在富文本里,图片宽度可能会超出容器。利用正则表达式匹配到 img 标签,然后添加样式即可。

formatImg(content) {
if (!content) return '';
const regex = /<img[^>]*>/g;
content = content.replace(regex, function (match) {
return match.replace('/>', ' style="max-width:100%;height:auto;"/>');
});
return content;
}

小程序跳转到公众号

方式一:放置公众号二维码,长按识别跳转。

<image show-menu-by-longpress src="https://xxx" />

方式二:使用 official-account 公众号关注组件。

  • 需要在小程序后台关联公众号,且打开「引导关注公众号」
  • 在微信公众号管理后台设置关联小程序
<official-account></official-account>

方式三:使用 web-view 打开关联的公众号的文章。

<web-view src="https://mp.weixin.qq.com/s/-xm_UBcgyuynCS9HiKhKjA" />
  • 可以点击顶部的标题,跳转到公众号的关注页
  • 一般在文章的最底部会放一个二维码,可以长按识别跳转