image
*Uniapp 的 <image> 与传统 web 开发中的 <img> 相比多了一个 mode 属性,用来设置图片的裁剪、缩放模式。
一般只需要使用 widthFix、aspectFill 这两个属性即可应对绝大多数情况。
即只需设置宽度自动撑起高度的图片用 widthFix ;需要固定尺寸设置宽高,并保持图片不被拉伸的图片用 aspectFill。
例如:所有 icon、文章详情里、产品详情里的详情图一般会用 widthFix,用户头像、缩略图一般会用 aspectFill。*
滚动穿透
弹窗遮罩显示时,底层页面仍可滚动。给遮罩最外层 view 增加事件 @touchmove.stop.prevent
<view class="pop-box" @touchmove.stop.prevent></view>
分享
小程序可以通过右上角胶囊按钮或者页面中 <button open-type='share'> 按钮来触发 分享功能
<template>
<!-- 假如这是一个社区列表页面 -->
<view v-for="(item, index) in list">
<button open-type="share" :data-item="item">分享</button>
</view>
</template>
<script>
export default{
data(){
return{
//列表数据
list:[
{
id:1,
title:"标题",
img:'***.jpg'
}
]
}
},
onShareAppMessage(){
// 点击页面中分享按钮,这样可以分别设置分享内容
if (res.from === 'button') {
let item=res.target.dataset.item
return {
title: item.title,
path: `/pages/detail?id=${item.id}`,
imageUrl:item.img
}
}
//点击右上角胶囊按钮分享的设置
return {
title: '**社区',
path: '/pages/community/'
}
}
}
</script>
return 的 Object 中 imageUrl 必须为宽高比例 5:4 的图片,并且图片大小尽量小于 20K。imageUrl 可不填,会自动截取当前页面画面。
另外 button 有默认样式,需要清除一下
<template>
<button class="btn-share" open-type="share"></button>
</template>
<style lang="scss">
.btn-share {
padding: 0;
margin: 0;
border: 0;
&::after {
padding: 0;
margin: 0;
border: 0;
}
/*以上代码完成了样式清除,接下来写新的css样式*/
}
</style>
底部安全区
OS 全面屏设备的屏幕底部有黑色横条显示,会对 UI 造成遮挡,影响事件点击和视觉效果。Android 没有横条,不受影响
使用 css 样式 constant(safe-area-inset-bottom) env(safe-area-inset-bottom) 来处理,兼容 iOS11.2+
需注意该方法小程序模拟器不支持,真机正常。
<template>
<view class="bottomBar"></view>
</template>
<style>
.bottomBar {
关于使用constant(safe-area-inset-bottom)、env(safe-area-inset-bottom)
会返回底部安全区的高度
两个方法都写,会自动选择能够生效的来使用
可以使用calc方法来计算,根据实际情况灵活使用
padding-bottom: calc(0rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(0rpx + env(safe-area-inset-bottom));
}
如果使用 nvue,则不支持以上方案。可使用 HTML5+规范 的方法来处理
<template>
<view :style="{ paddingBottom: `${holderHeight}px` }"></view>
</template>
<script>
export default{
data(){
return{
* 先判断是否iOS,再判断是否刘海屏即全面屏。34即为底部安全高度
* 返回系统平台名称,plus.navigator.hasNotchInScreen()返回是否是刘海屏。
* 一定要先判断是否iOS,因为该问题仅iOS需处理,而且Android返回是否刘海屏标准不一,会有显示问题。
holderHeight: == 'iOS' ? (plus.navigator.hasNotchInScreen() ? 34 : 0) : 0)
}
对于老机型不支持用于设定安全区域与边界的距离的constant(safe-area-inset-bottom) env(safe-area-inset-bottom) 可用以下兼容
not 表示不支持括号内的属性
@supports not(constant(safe-area-inset-bottom)){
page{
padding-bottom: 150rpx;
}
}
摇树优化
H5 打包时去除未引用的组件、API。
摇树优化(treeShaking)
//manifest.json
"h5" : {
"optimization":{
"treeShaking":{
"enable":true //启用摇树优化
}
}
}
H5 唤起 App
两种实现方式:
- URL Sheme
优点:配置简单
缺点:会弹窗询问“是否打开***”,未安装时网页没有回调,而且会弹窗“打不开网页,因为网址无效”;微信微博 QQ 等应用中被禁用,用户体验一般。 - Universal Link
优点:没有额外弹窗,体验更优。
缺点:配置门槛更高,需要一个不同于 H5 域名的 https 域名(跨域才出发 UL);iOS9 以上有效,iOS9 一下还是要用 URL Sheme 来解决;未安装 App 时会跳转到 404 需要单独处理。
分包
主包大小不能超过2m 除页面可以分包配置,静态文件、js 也可以配置分包。可以进一步优化落地页加载速度。
在 manifest.json 中对应平台下配置 “optimization”:{“subPackages”:true} 来开启分包优化。开启后分包目录下可以放置 static 内容。
manifest.json源码
{
...,
"mp-weixin" : {//这里以微信为例,如有其他平台需要分别添加
...,
"optimization" : {
"subPackages" : true
}
}
}
*分包预加载 (https://uniapp.dcloud.net.cn/collocation/pages.html#subpackages)*
多行文字需要限制行数溢出隐藏时,Nvue 和非 Nvue 写法不同
Nvue 写法
.text {
lines: 2; //行数
text-overflow: ellipsis;
word-wrap: break-word;
}
非 Nvue 写法
.text {
display: -webkit-box;
-webkit-line-clamp: 2; //行数
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
uniapp 的自定义导航栏插件
**uni-nav-bar 自定义导航栏 **
<uni-nav-bar v-if="navBarShowTag">
<view class="tabs-box">
<view class="one-nav" :class="currentSwiperIndex === 0 ? 'nav-actived' : '' " @tap="swiperChange(0)">推 荐</view>
<view class="one-nav" :class="currentSwiperIndex === 1 ? 'nav-actived' : '' " @tap="swiperChange(1)">资讯</view>
</view>
</uni-nav-bar>
uni加载loadding状态控制
uni.showLoading({
title: '加载中',
})
uni.hideLoading();
navigator 页面跳转
类似于a标签 但只能跳转本地页面
<navigator open-type="navigate" :url=" '/pages/webview/webview?url='+item.link">
<image class="banner-swiper-img" :src="item.image" mode="aspectFill" />
</navigator>
路由跳转
uni.navigateTo({
url: '/fpage/pages/login/login'
})
获取当前设备的宽度
let device = uni.getSystemInfoSync()
let widths = device.windowWidth
uni.navigateBack返回到上一个页面,页面没有刷新,
可这样设置
uni.navigateBack({
delta:1,
success: function() {
let page = getCurrentPages().pop(); //跳转页面成功之后
if (!page) return;
page.onLoad(); //如果页面存在,则重新刷新页面
}
})
当前页面向返回的页面传值
当前页面设置
let pages = getCurrentPages();
let prevPage = pages[pages.length - 2];
prevPage.$vm.currentIndex = this.currentIndex;
uni.navigateBack({})
返回页面接收
onShow() {
let pages = getCurrentPages();
let index = pages[pages.length - 1].$vm.currentIndex
if (index) {
console.log('getFilterGoods',this.getFilterGoods);
this.outboundGoodsAppointmentList.splice(index,1,this.getFilterGoods)
}
},
上拉加载,下拉刷新
// 上拉加载
onReachBottom() {
if(this.goodList.length>=this.total)return false;
this.initData({customerId:this.customerId,projectId:this.projectId,current:this.pageNum,pageSize:this.pageSize})
},
// 下拉刷新
onPullDownRefresh(){
this.goodList = []
this.pageNum = 1
this.initData({customerId:this.customerId,projectId:this.projectId,current:this.pageNum,pageSize:this.pageSize})
},
initData(param){
if(this.loadingType !==0) return false;
this.loadingType = 1
uni.showNavigationBarLoading()
getStockGroupByBatchNumberPage({...param}).then(res=>{
this.goodList = this.goodList.concat(res.arr)
this.total = res.total
this.loadingType = 0
this.pageNum ++
uni.hideNavigationBarLoading()
})
},
获取用户手机号
*小程序通过点击 button 获取 code 来跟后端换取手机号。在开发者工具中无法获取到 code。真机预览中可以获取到。*
<template>
<button open-type="getPhoneNumber" @getphonenumber="getphonenumber">
获取用户手机号
</button>
</template>
getphonenumber(e) {
let code = e.detail.code; //开发工具中无法获取到该code,用真机预览进行测试
//开发阶段可以设置剪贴板将code复制。便于跟后端对接调试。调试完成后记得删除。
uni.setClipboardData({
data: e.detail.code
});
}
input 设置输入类型
uniapp自带的input自身是不带输入类型设置的,除非使用uni-easyinput,但是因为uni-forms的样式与原本页面需求相差太大,只能手写
<view class="form_item">
<text class="label beforeIcon"> 件重 </text>
<input
class="inputStyle"
name="packageUnit"
type="number"
@input="getWeight(item, index, 'packageUnit', $event)"
v-model="item.packageUnit"
placeholder="请输入"
/>
<text class="weightWord">kg</text>
</view>
getWeight(item,index,objType,e){
let value = e.target.value.replace(/^0|[^\d]|[.]/g, '')
this.$nextTick(() => {
this.$set(this.inboundGoodsAppointmentList[index],objType,value)
})
if(value){
if(+item.packageUnit&&+item.quantity){
this.$set(this.inboundGoodsAppointmentList[index],'weight',Math.round(item.packageUnit*item.quantity))
}
}
},
//小红点的样式
.beforeIcon{
&::before {
content: "*";
color: #ff4949;
}
}
出来的效果一部分是这样的
picker 从底部弹出的选择框
<picker
:range="goodsOptions" //选择数据的数组
@change="changeGoods($event, index)"
range-key="label" //渲染当前对象数据的什么属性
>
<view class="form_item">
<text class="label beforeIcon"> 商品 </text>
<view
class="inputStyle"
:class="{ placeholderStyle: item.goodsName === '' }"
>
{{ item.goodsName || "请选择" }}
</view>
<uni-icons type="forward" size="16"></uni-icons>
</view>
</picker>
动态设置类名
:class="{ placeholderStyle: contractTypeName === '' }"
当uniapp需要设置多个底部tab栏
当我在别的页面需要设置另外的tab栏时,可和在配置文件的区分开来。
单个修改的时候,uniapp,支持单个设置,
然后多个的时候需要自己设置底部tab,代码
<template>
<view :style="{ 'padding-bottom': paddingBottomHeight + 'rpx' }">
<view>
<Order v-show="current == 0" />
<Record v-show="current == 1" />
<Search v-show="current == 2" />
<Bill v-show="current == 3" />
</view>
<cover-view
class="tabbar"
>
<cover-view
class="tabbar-item"
v-for="(item, index) in tabNav"
:key="index"
@click="tabbarChange(item)"
>
<uni-icons
:type="item.iconPath"
size="16"
v-if="current == index"
color="#0099fe"
></uni-icons>
<uni-icons :type="item.iconPath" size="16" v-else></uni-icons>
<cover-view
class="item-text"
:class="{ tabbarActive: current == index }"
style="color: #a3a3a3; font-size: 20rpx"
v-if="item.text"
>{{ item.text }}</cover-view
>
</cover-view>
</cover-view>
</view>
</template>
<script>
import Bill from "../bill/index"
import Order from "../order/index"
import Record from "../stock/record"
import Search from "../stock/search"
export default {
components: {
Bill,
Order,
Record,
Search
},
data() {
return {
current:0,
tabNav: [
{
pagePath: "/pages/order/index",
iconPath: "settings-filled",
text: "预约管理",
index:0,
},
{
pagePath: "/pages/stock/record/index",
iconPath: "wallet",
text: "出入库记录",
index:1,
},
{
pagePath: "/pages/stock/search/index",
iconPath: "home",
text: "库存查询",
index:2,
},
{
pagePath: "/pages/bill/index",
iconPath: "list",
text: "对账单",
index:3
},
],
paddingBottomHeight: 0, //iPhonex以上手机底部适配高度
}
},
created () {
uni.setNavigationBarTitle({
title: this.tabNav[this.current].text
});
uni.getSystemInfo({
success: (res) => {
let model = ['X', 'XR', 'XS', '11', '12', '13', '14', '15'];
for (let i=0;i<=model.length;i++) {
if(res.model.indexOf(model[i]) != -1 && res.model.indexOf('iPhone') != -1) {
this.paddingBottomHeight = 70
return false
}else {
this.paddingBottomHeight = 100
// this.$emit('tabbarHeight',100)
}
}
}
})
},
methods: {
//跳转切换tab
tabbarChange(item) {
this.current = item.index
uni.setNavigationBarTitle({
title: item.text
});
}
}
}
</script>
<style lang="scss" scoped>
.tabbarActive{
color: #0099fe !important;
}
.tabbar{
position: fixed;
bottom: 0;
left: 0;
display: flex;
justify-content: space-around;
align-items: center;
width: 100%;
height: 100rpx;
background-color: #ffffff;
border-top: 1px solid #e5eaea;
}
.tabbar-item{
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// height: 100rpx;
}
.item-img{
margin-bottom: 4rpx;
width: 46rpx;
height: 46rpx;
}
.item-name{
font-size: 26rpx !important;
color: #A3A3A3 !important;
}
.wmsContent{
padding-bottom:100rpx;
}
</style>
列表页面
*下拉刷新,上滑加载,没有更多内容的提示,关键字搜索,标签栏匹配,空白页面,列表项组件*
<!-- 页面代码 -->
<scroll-view
:scroll-y="true"
style="height: 100vh"
:refresher-enabled="true"
:refresher-triggered="list.flag"
@refresherrefresh="refresh"
@scrolltolower="getAnswer"
:enable-back-to-top="true"
:scroll-into-view="list.scrollId"
:scroll-with-animation="true"
>
<view style="position: relative" class="bg-white">
<view v-for="(item,index) in list.data" :key="index" >
<answer-item
:data="item"
></answer-item>
</view>
</view>
<view
class="cu-load bg-gray text-main loading"
style="height: 40px"
v-if="!list.nomore"
></view>
</scroll-view>
// js代码
export default {
components: {
AnswerItem,
},
data() {
return {
list: {
data: [],
flag: false, //是否展示刷新
limit: 10,
total: 0,
nomore: false, //是否显示到底
empty: false, //是否显示为空,
error: false, //是否请求错误,
}
};
},
async onLoad(e) {
this.getQuestion()
await this.getAnswer()
async getAnswer() {
},
methods: {
async getAnswer() {
const listHandle = require("../../utils/js/listHandle")
const id = this.list.data.length ? this.list.data[this.list.data.length - 1].id : ''
const res = await this.http.get('/applet/answerList', {
qId: this.question.id,
limit: this.list.limit,
userId: this.store.user ? this.store.user.id : '',
id
})
if (res.code === 200) {
const list = listHandle.add(res.data, this.list.data, this.list.limit)
Object.assign(this.list, list)
return res.data
} else {
this.list.error = true
this.message.toast('请求失败')
return false
}
},
async refresh() {
// 刷新方法
const listHandle = require("../../utils/js/listHandle")
this.list.flag = true
this.list.nomore = false
this.list.empty = false
this.list.error = false
this.list.loadNum = 0
const res = await this.http.get('/applet/answerList', {
qId: this.question.id,
limit: this.list.limit,
userId: this.store.user ? this.store.user.id : '',
id: ''
})
this.list.flag = false
if (res.code === 200) {
const list = listHandle.update(res.data, this.list.limit)
Object.assign(this.list, list)
} else {
this.list.err = true
this.message.toast('请求失败')
}
},
}
}
listHandle.js 列表项处理的方法
const listHandle = {
add(resData, listData, limit) {
var ret = {
nomore: false,
empty: false,
data: []
}
if (resData) {
if (resData.length < limit) {
// 获取数据条数小于页码数,显示已到底
ret.nomore = true
}
ret.data = listData.concat(resData)
} else {
ret.data = listData
ret.nomore = true
if (!listData.length) {
// 请求已无返回数据且当前列表无数据,显示为空
ret.empty = true
}
}
return ret
},
update(resData, limit) {
var ret = {
nomore: false,
empty: false,
data: []
}
if (resData) {
if (resData.length < limit) {
// 获取数据条数小于页码数,显示已到底
ret.nomore = true
}
ret.data = resData
} else {
ret.nomore = true
// 请求已无返回数据且,显示为空
ret.empty = true
}
return ret
}
}
module.exports = listHandle
在手机端上滑加载的时候,如果你是按照时间的倒序来排序,传统web底部分页器那样传一个页码数和单页条数给后端查询的话。如果在你上滑的过程中有用户发布新的内容,那么你的列表中就会有 重复的项。
举个例子:你最初取了十条数据,当你上滑到底部加载时传了一个 page=2 和 limit=10给后端,意思是将所有数据十条作为一页,我要拿第二页的内容。但是这时候有用户添加了一条新的数据,你第一页的最后一条就被挤到第二页去了,此时你拿到的数据第一条会和上一次拿到的最后一条一模一样!
想要解决这个问题也很简单,我们在向后端传递数据的时候不要按页码数去传值。我们将当前数据的最后一条的id传给后端,让后端取 id比这个值更小的十条,此时不论有多少用户插入新的数据都不会对你的结果产生影响。当然要实现这种功能的前提是你的数据表id是递增的,如果不是你也可以用数据的创建时间的那个字段来传递。
对应这个功能的是下面这行代码。
const id = this.list.data.length ? this.list.data[this.list.data.length - 1].id : ''
登录页面
在小程序中,登录往往不像web端需要输入账号密码或者手机验证码登录,而是使用各个平台的快速鉴权功能,例如微信小程序就是可以通过向微信官方的api发送请求来获取用户信息
一个按钮来获取用户信息
<template>
<button class="cu-btn bg-blue shadow-blur round lg" @click="login">
立即登录
</button>
</template>
<script>
export default {
methods: {
async login() {
uni.showLoading({
mask: true,
title: '登录中'
})
const res = await uni.getUserProfile({
desc: '用于存储用户数据'
})
uni.hideLoading()
if (res[0]) {
this.message.toast('获取失败', 'text')
return
}
this.getUser(res[1].userInfo)
},
async getUser(userInfo) {
uni.showLoading({
mask: true,
title: '登录中'
})
const loginRes = await uni.login()
if (loginRes[0]) {
uni.hideLoading()
this.message.toast('获取失败')
return
}
const res = await this.http.post('/applet/weChatLogin', {
code: loginRes[1].code
}, 'form')
uni.hideLoading()
if (!res.data) {
this.router.push('/pages/form/userForm', {
userInfo,
handle: 'add'
})
return
}
this.store.setData('user', res.data)
},}
跨页面方法调用
*我要实现一个搜索功能,当我点进搜索详情输入关键词后,我要返回列表页面并触发一次搜索方法*
searchFunc(){
let pages = getCurrentPages()
let page = pages[pages.length - 2]
page.$vm.searchText = this.search.text
page.$vm.refresh()
this.router.back()
}
getCurrentPages() 是一个全局函数,用于获取当前页面栈的实例,以数组形式按栈的顺序给出。我们当前页面就是数组最后一项,那么上一个页面就是pages[pages.length - 2]。当然我们还要记得加上$vm属性,因为在uniapp中我们的数据和方法是挂载这个实例上的。我们可以通过这个示例访问到对应页面的所有数据和方法
证书文件
准备苹果开发账号
ios 证书、描述文件 申请方法(https://ask.dcloud.net.cn/article/152)
证书和描述文件分为开发(Development)和发布(Distribution)两种,Distribution 用来打正式包,Development 用来打自定义基座包。
ios 测试手机需要在苹果开发后台添加手机登录的 Apple 账号,且仅限邮箱方式注册的账号,否则无法添加。
苹果登录
APP
苹果登录需要使用自定义基座打包才能获得 Apple 的登录信息进行测试
iOS 自定义基座打包需要用开发(Development)版的证书和描述文件
iOS自定义基座包,如果开发者重新续费要生成一个本地证书,再打包
踩坑
scroll-view无法下拉刷新
*scroll-view标签上有一个scroll-into-view属性,这个属性值可以传入一个id,当我们更改这个值时我们就会滚动到指定id的容器位置。
但是如果我们的scroll-view被封装在组件中使用时,scroll-view无法下拉刷新
应该是scroll-view元素的下拉位置在元素渲染时就确定了,而我们赋予scroll-view的高度更改了下拉位置,导致下拉的时候没法拉到位
在scroll-view的高度变量确定后再渲染scroll-view元素,具体的做法如下代码*
<scroll-view
scroll-y="true"
:style="'height:'+height"
v-if="height"
:refresher-enabled='true'
:refresher-triggered='list.flag'
@refresherrefresh='refresh'
@scrolltolower='loadMore'>
</scroll-view>
scroll-view sticky样式问题
scroll-view 是小程序常常会用到的一个标签,在滚动窗口内我们可能会有一个顶部标签栏,如果我们不想通过计算高度去固定在顶部的话我们可以使用 position:sticky 加一个边界条件例如top:0 属性实现一个粘性布局,在容器滚动的时候,如果我们的顶部标签栏触碰到了顶部就不会再滚动了,而是固定在顶部。
但是在小程序中如果你在scroll-view元素中直接为子元素使用sticky属性,你给予sticky的元素在到达父元素的底部时会失效,
解决方法
在scroll-view元素中,再增加一层view元素,然后在再将使用了sticky属性的子元素放入view中,就可以实现粘贴在某个位置的效果了
ios固定输入框字体移动bug
在一个固定于页面中间的滚动容器内放了一个表单,在安卓端测试功能完好,在IOS端有一个bug。当输入框在输入后没有点击其他位置使输入框失焦的话,如果滚动窗口内部的字体也会跟着滚动下面使解决方法
使用这个方法更改后,不仅布局的样式不会改变,而且字体随着固定的滚动窗口一起滚动的bug也会解决
更改后的输入框
<textarea fixed="true" auto-height="true" ></textarea>
真机时间错误BUG
在小程序中使用new Date().toLocaleDateString() api获取时间的时候,在开发工具中显示为当前时间,而在真机中显示为其他地区的时间
toLocaleDateString()方法依赖于底层操作系统在格式化日期上。 例如,在美国,月份出现在日期(06/22/2018)之前,而在印度,日期出现在月份(22/06/2018)之前。
解决方案
使用new Date()构造函数来获取年月日后拼接
如果没有输入任何参数,则Date的构造器会依据系统设置的当前时间来创建一个Date对象。
Date和toLocaleDateString()的区别在于一个是获取系统当前设置的时间,一个则是底层操作系统来格式化时间
//具体代码如下
let date = new Date()
date = date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate()
date = date.split('/')
if (date[1] < 10) {
date[1] = '0' + date[1]
}
if (date[2] < 10) {
date[2] = '0' + date[2]
}
date = date.join('-')
textarea回显异常
问题描述:
textarea回显在popup弹框内,在小程序状态,会渲染少字的情况
问题分析
小程序和App的vue页面,主体是webview渲染的,这个环境下,部分的UI元素,使用的是原生控件,此种混合渲染,虽然提升性能了,但是会带来问题
- 前端组件无法覆盖原生控件的层级问题
- 原生控件无法嵌套特殊前端组件(如scroll-view)
- 原生控件UI无法灵活自定义
原生组件是:
- map
- video
- camera
- canvas
- input
- textarea
- live-player
- live-pusher
- cover-view
- cover-image
- ad
uniapp原生组件说明H5或者App的nvue页面就不存在混合渲染,因为都是前端渲染或原生渲染,不存在层级关系
解决方案:以text替换textarea
<u-popup
v-model="show"
mode="center"
width="500"
:closeable="true"
:mask-close-able="true"
>
<view class="titles-wrap">
<view class="titles">使用规则</view>
</view>
<scroll-view scroll-y="true" style="height: 540rpx;">
<view class="titlesw-wrap" v-if="showText">
<!-- <u-input v-model="item.useInstructions" type="textarea" :maxlength="-1" :height="100" :auto-height="true" /> -->
<text class="cardTips">{{ forMatStr(item.useInstructions) }}</text>
<!-- <textarea class="titlesw" v-model="item.useInstructions" :maxlength="-1" autoHeight></textarea> -->
</view>
</scroll-view>
</u-popup>
forMatStr (str) {
if (!str) return ''
let a = str.replace(/\↵/g, '\n')
return a
},