在平时开发时，不管是前端、后端或者小程序端，为了节省带宽及存储空间，我们一般都会对上传的图片进行压缩。本文我们介绍一下使用uniapp开发小程序时，基于uni-file-picker组件进行图片压缩的方法。开启uni-file-picker自带的压缩配置uni-file-picker 组件通过配置 sizeType可以开启自带的压缩功能。sizeType: { type: Array, default () { return ['original', 'compressed'] } },'original'代表原始文件'compressed'代表启用压缩。使用也比较简单，配置一下就行了。<uni-file-picker return-type="object" fileMediatype="image" mode="grid" :sizeType="sizeType" :auto-upload="false" @select="selectImage" @delete="deleteImage"/>通过以上设置，便可实现对图片进行压缩，一般能够实现对半压缩的，比如10M的图片压缩成5M左右这样的。当然这个不是绝对的，只是个约莫的压缩率。如果需要测试，需要使用手机进行真机调试，才可以看出来文件压缩后的大小。如果对图片大小没有太大限制 ，直接这样压缩就可以了，但是有的项目会限制对图片的大小必须小于1M，这时候，光有这个设置，就满足不了需求了，这时候我们可以再采取一点措施。自定义图片压缩当uni-file-picker自带的压缩功能不能满足我们需要时，我们可以自己对图片进行压缩，自定义压缩图片时，我们可以指定压缩率或循环压缩到指定大小。当然，也需要注意压缩后的图片质量。一、创建公共压缩方法创建公共方法imageCompress，传入file进行压缩。// 图片压缩递归，小于1M跳出 export function imageCompress(file){ return new Promise((resolve, reject)=>{ let { size,path } = file let type = path.split(".")[1] //大于1M进行压缩， if(size< (1024*1024)){ resolve(file) return false } uni.compressImage({ src: path, quality: 80, success: res => { let newPath = res.tempFilePath+type let newName = res.tempFilePath.split("/")[res.tempFilePath.split("/").length-1]+type uni.getFileInfo({ filePath:res.tempFilePath, success:async (info)=>{ let newFile = {...file,size:info.size,path:newPath,name:newName,tempFilePath:res.tempFilePath} resolve(await imageCompress(newFile)) } }) } }) }) }二、修改uni-file-picker上传方法在uni-file-picker上传方法时，先调用公共方法imageCompress进行压缩，压缩完成后在进行上传。import { imageCompress } from "@/utils/leeframe.js" import { uploadImageCommon } from "@/common/api.js" export default { data() { return{ sizeType:['compressed'], //设置图片压缩 } }, onLoad(option) { this.workId = option.workId }, methods:{ //选择照片 selectImage(e){ this.timeSeting() if(e.tempFilePaths&&e.tempFiles){ this.file = e.tempFiles[0].file this.type = 'mentou' this.uploadImage() } }, // 删除照片 deleteImage(e){ this.mentouValue = {} }, // 上传照片 async uploadImage(){ // 压缩图片 this.file = await imageCompress(this.file) // 要传的参数 let params = { file:this.file } // 上传图片到相依的接口 uni.uploadFile({ url: uploadImageCommon, //后台上传地址 filePath: this.file.tempFilePath?this.file.tempFilePath:this.file.path, fileType: "image", formData:{...params}, name: 'file', header: { "content-type": "multipart/form-data", "Authorization": uni.getStorageSync('token') }, success: uploadFileRes => { let imageName = JSON.parse(uploadFileRes.data).data // 这里可以对返回的参数进行处理了 uni.showToast({ title: '上传成功', icon: "success" }); }, fail(err) { uni.showToast({ title: '上传失败', icon: "error" }); } }) }, } }
具体实现效果就是上面这个图片。里面的元素都是自己根据自己实际情况拼接进去的。在开发小程序时，我们经常会遇到【分享】这个功能，在实现分享功能时，我们一般会基于当前页面，生成一个海报进行分享。在实现时，基本上是通过canvas画一个图片，然后生成一个图片，具体的可以看下面的代码。视图代码 <view :style="{marginTop: '30rpx'}"> <view class="container-price"> <view class="container-price-left"> <view style="display: flex;flex-direction: column;align-items: center;margin-left: 30rpx;" @click="shareing"> <view> <image src="../../static/img/share.svg" style="height: 46rpx;width: 46rpx;"></image> </view> <view style="font-size: 16rpx;height: 30rpx;">分享</view> </view> <view style="display: flex;flex-direction: column;align-items: center;margin-left: 30rpx;" @click="subscribeMsg4Bid"> <view> <image src="../../static/img/subscribe.svg" style="height: 46rpx;width: 46rpx;"> </image> </view> <view style="font-size: 16rpx;height: 30rpx;">订阅</view> </view> </view> </view> </view> <uni-popup ref="popupPosters" type="bottom" border-radius="10px 10px 0 0"> <view class="popup-posters-wrap"> <image :src="posterImg" mode="aspectFill" :style="{height:'420px',width:'100%'}"> </image> <view class="popup-footer" :style="{marginBottom : (0-safeAreaHeight) + 'px'}"> <view style="text-align: center;font-size: 20rpx;">立即分享</view> <view style="display: flex;flex-direction: row;font-size: 20rpx;margin: 0 20rpx;"> <view style="display: flex;flex-direction: column;align-items: center;"> <u-icon size="80" color="#03de6d" name="weixin-circle-fill"></u-icon> <view>微信好友</view> </view> <view style="display: flex;flex-direction: column;align-items: center;margin-left: 20rpx;"> <view @click="saveToLocal"> <u-icon size="80" color="#18a5f0" name="photo-fill"></u-icon> </view> <view>保存海报</view> </view> </view> <view :style="{bottom:(safeAreaHeight+5)+'px',left:'50%',position:'fixed',fontSize:'24rpx',transform:'translate(-50%,-50%)',padding:'5rpx 15rpx'}" @click="this.$refs.popupPosters.close()"> <view>取消</view> </view> </view> </view> </uni-popup>js代码//生成海报--微信端 createPoster() { let _this = this const canvasId = "myCanvas" let ctx = uni.createCanvasContext(canvasId, _this) // 自定义组件中 一定要传this ，这里一开始没加，困惑很久，一定要写一下 // 填充背景 ctx.setFillStyle('#FFFFFF') ctx.fillRect(0, 0, _this.pupopWidth, _this.pupopHeight); ctx.save() //生成车辆图册 uni.getImageInfo({ src: _this.imageFileList[0], //这里的banner是展示的商品图 success(image) { let bannerW = _this.pupopWidth let bannerH = 400 * _this.pixelRatio let bannerX = 0 * _this.pixelRatio let bannerY = 20 * _this.pixelRatio // 将banner到画布上 ctx.drawImage(image.path, bannerX, bannerY, bannerW, bannerH) ctx.restore() ctx.save() //车辆描述 ctx.setFontSize(18 * _this.pixelRatio) ctx.setFillStyle("#000") let bannerTextX = 20 * _this.pixelRatio let bannerTextY = bannerY + bannerH + 20 * _this.pixelRatio + 20 let title = _this.inquiry.vehicleBrand + _this.inquiry.vehicleSeries + _this.inquiry.vehicleYear + _this.inquiry.vehicleModel if (title.length > 20) { title = title.slice(0, 20) + '...' } ctx.fillText(title, bannerTextX, bannerTextY, _this.pupopWidth); // 第二行文字 ctx.setFontSize(14 * _this.pixelRatio) ctx.setFillStyle("#cb171d") //事故类型 let titleAccidentType = _this.inquiry.accidentTypeName ctx.fillText(titleAccidentType, bannerTextX, bannerTextY + 30 * _this.pixelRatio, _this .pupopWidth); let accidentTypeWidth = ctx.measureText(titleAccidentType).width //所有人性质 let vehicleOwnerNature = _this.inquiry.vehicleOwnerNatureName ctx.fillText(vehicleOwnerNature, bannerTextX + accidentTypeWidth + 5 * _this.pixelRatio, bannerTextY + 30 * _this.pixelRatio, _this .pupopWidth); let vehicleOwnerNatureWidth = ctx.measureText(vehicleOwnerNature).width // 使用性质 let vehicleUsageNature = _this.inquiry.vehicleUsageNatureName ctx.fillText(vehicleUsageNature, bannerTextX + accidentTypeWidth + 5 * _this.pixelRatio + vehicleOwnerNatureWidth + 5 * _this.pixelRatio, bannerTextY + 30 * _this.pixelRatio, _this .pupopWidth); let vehicleUsageNatureWidth = ctx.measureText(vehicleUsageNature).width // 初登日期及地方位置 ctx.setFontSize(12 * _this.pixelRatio) ctx.setFillStyle("#b3b3b3") let vehicleRegisterDate = '初登日期 ' + _this.inquiry.vehicleRegisterDate let position = '停放地 ' + _this.inquiry.parkingPositionProvinceName + _this.inquiry .parkingPositionCityName let vehicleRegisterDateWithPosition = vehicleRegisterDate + " / " + position ctx.fillText(vehicleRegisterDateWithPosition, bannerTextX, bannerTextY + 30 * _this.pixelRatio + 20 * _this.pixelRatio, _this.pupopWidth); let vehicleRegisterDateWithPositionWidth = ctx.measureText(vehicleRegisterDateWithPosition) .width //画间隔线 ctx.moveTo(40 * _this.pixelRatio, bannerTextY + 20 * _this.pixelRatio + 20 * _this.pixelRatio + 20 * _this.pixelRatio); //设置起点状态 ctx.lineTo(_this.pupopWidth - 40 * _this.pixelRatio, bannerTextY + 20 * _this.pixelRatio + 20 * _this.pixelRatio + 20 * _this.pixelRatio); //设置末端状态 ctx.lineWidth = 1 * _this.pixelRatio; //设置线宽状态 ctx.strokeStyle = '#EEEEEE'; //设置线的颜色状态 ctx.stroke(); //二维码 uni.getImageInfo({ src: 'https://oss.guochewang.cn/banner/app/wechat_app.jpg', success(res) { // 画当前页面的二维码 const img_x = 20 * _this.pixelRatio const img_w = 60 * _this.pixelRatio const img_y = bannerTextY + 20 * _this.pixelRatio + 20 * _this.pixelRatio + 20 * _this.pixelRatio + 20 * _this.pixelRatio ctx.drawImage(res.path, img_x, img_y, img_w, img_w) //画提示文字 const tiptextX = img_x + img_w + 20 * _this.pixelRatio const tiptext1Y = img_y + 20 * _this.pixelRatio const tiptext2Y = img_y + 20 * _this.pixelRatio + 20 * _this .pixelRatio + 12 * _this.pixelRatio const tiptext1 = '国车网 ' const tiptext1_1 = '一个专注事故车的交易平台 ' const tiptext2 = '识别二维码查看车辆更多详情' ctx.setFontSize(16 * _this.pixelRatio) ctx.setFillStyle("#cb171d") ctx.fillText(tiptext1, tiptextX, tiptext1Y); ctx.stroke(); let tiptext1Width = ctx.measureText(tiptext1).width ctx.setFontSize(12 * _this.pixelRatio) ctx.setFillStyle("#333333") ctx.fillText(tiptext1_1, tiptextX + tiptext1Width, tiptext1Y); ctx.stroke(); ctx.setFontSize(12 * _this.pixelRatio) ctx.setFillStyle("#333333") ctx.fillText(tiptext2, tiptextX, tiptext2Y); ctx.stroke(); ctx.draw(false, () => { uni.canvasToTempFilePath({ width: _this.pupopWidth, height: _this.pupopHeight, destWidth: _this.pupopWidth, destHeight: _this.pupopHeight, canvasId: canvasId, fileType: 'png', quality: 1, success: function(res) { _this.posterImg = res .tempFilePath; //最终将canvas转换为图片 _this.$refs.popupPosters .open(); uni.hideLoading() _this.ifGeneratingPosters = false }, fail(error) { console.log('4', error) uni.showToast({ title: '生成海报失败，请稍后重试！' }) setTimeout(() => { uni.hideLoading() _this.ifGeneratingPosters = false }, 2000) } }, _this) }) }, fail(error) { console.log('获取二维码失败', error) uni.showToast({ title: '生成海报失败，获取二维码失败' }) setTimeout(() => { uni.hideLoading() _this.ifGeneratingPosters = false }, 2000) } }) }, fail(error) { console.log('生成商品图失败', error) uni.showToast({ title: '生成海报失败，获取商品图失败' }) setTimeout(() => { uni.hideLoading() _this.ifGeneratingPosters = false }, 2000) } }); }, //将图片保存到本地相册 saveToLocal() { //#ifdef MP-WEIXIN uni.saveImageToPhotosAlbum({ filePath: this.posterImg, success: () => { uni.showToast({ icon: 'success', title: '保存到相册成功' }) this.$refs.popupPosters.close() }, fail: (err) => { console.log("保存到相册失败", err) } }); //#endif },
微信小程序开发修改页面名称大小写遇到的坑
最近在使用UniApp开发小程序时，有一个页面名称大小写不是很规范，因此修改了一下命名，当然，只是修改了页面名称的大小写，没有修改其他的东西。修改完成后，在UniApp运行调试，发现没有任何问题，于是顺理成章的发行并上传，但是，此时问题来了，代码上传到微信小程序之后，在真机测试时，页面始终无法打开。这个时候，首先想到是不是在UniApp发行出现了问题，于是乎，在UniApp重新发行了一次，这次发行后，特意在微信开发者工具进行了测试，页面也是能够正常打开没有任何报错或警告信息，在调试通过后，再次上传代码，进行真机测试，发现页面仍然无法打开。联想到Git对文件名称大小写不敏感的问题，我觉得应该是在微信开发者工具上传代码时，同样对文件名称大小写不敏感出现了问题，导致虽然我修改了页面名称，但是在微信服务器，文件名称仍然还是小写的，但是我代码中，此时已经修改了打开的页面路径的大小写，因此导致找不到页面。其实解决方式也特别简单，重命名一下页面名称，当然，这个时候，不能只是修改页面名称大小写，重新发行、上传代码，然后真机测试，此时页面能够完美打开了。
最近在使用UniApp开发微信小程序时，使用了uView UI框架，在进行列表展示时，使用了Cell单元格组件。但是在使用的时候，感觉Cell单元格的字体较大，想调整小一点。在Vue开发中，我们知道都是用depp进行样式穿透，但是在UniApp开发小程序时，如果我们添加以下代码 ::v-deep { .u-cell { padding: 10rpx 32rpx !important; font-size: 20rpx !important; line-height: 54rpx; font-family: cursive; } }可以尝试运行一下，你会发现没有任何效果。原因就在于小程序组件样式是隔离的。如果我们想解除样式隔离，可以在父组件（基本就是我们自己的页面）与methods同级的位置，添加以下代码options: { styleIsolation: 'shared', // 解除样式隔离 },对于组件的styleIsolation有三个可选值：可选值说明isolated默认值，组件内外的样式互不影响apply-shared接收外部（包括父页面和全局）的样式，但组件内的样式不影响外部。shared接收外部样式，同时组件内的样式会共享到页面
油耗笔记OilNote集成腾讯地图实现载在图选择加油站充电站功能
油耗笔记OilNote经过两轮迭代，将地图服务由高德地图切换到百度地图，最终定格到了腾讯地图。在前面，我们已经实现了逆地址解析及周边搜索功能。接下来我们要实现打开地图，在图直接选择加油站功能。我们使用腾讯地图提供的微信小程序JavaScript SDK实现在线地图功能。一、微信小程序JavaScript SDK介绍腾讯位置服务为微信小程序提供了基础的标点能力、线和圆的绘制接口等地图组件和位置展示、地图选点等地图API位置服务能力支持，使得开发者可以自由地实现自己的微信小程序产品。 在此基础上，腾讯位置服务微信小程序JavaScript SDK是专为小程序开发者提供的LBS数据服务工具包，可以在小程序中调用腾讯位置服务的POI检索、关键词输入提示、地址解析、逆地址解析、行政区划和距离计算等数据服务，让您的小程序更强大！微信小程序JavaScript SDK开发文档二、微信小程序JavaScript SDK使用2.1、小程序域名设置在小程序管理后台 -> 开发 -> 开发管理 -> 开发设置 -> “服务器域名” 中设置request合法域名，添加https://apis.map.qq.com2.2、引入SDK下载微信小程序JavaScriptSDK，微信小程序JavaScriptSDK v1.1 JavaScriptSDK v1.2，并将解压后的.js文件引入工程中。2.3、页面引入2.3.1、在页面引入脚本文件 // 引入SDK核心类，js文件根据自己业务，位置可自行放置 var QQMapWX = require('../../common/utils/qqmap-wx-jssdk.min.js'); //引入下载好的sdk var qqmapsdk; // 实例化API核心类 qqmapsdk = new QQMapWX({ key: LocationUtils.TENCENT_MAP_KEY });2.3.2、查询加油站//页面加载 onLoad(options) { let that = this; this.latitude = uni.getStorageSync('latitude'); this.longitude = uni.getStorageSync('longitude'); this.oilType = options.oilType === '电' ? '充电站' : '加油站' let iconPath = 'https://oss.lisen.cc/oilnote/images/chargingPile.png'; if (options.oilType !== '电') { iconPath = 'https://oss.lisen.cc/oilnote/images/oilStation.png'; } // 调用接口 qqmapsdk.search({ keyword: encodeURI(that.oilType), location: { latitude: that.latitude, longitude: that.longitude }, page_size: 20, auto_extend: '1', success: function(res) { var selfLocation = { id: -1, latitude: that.latitude, longitude: that.longitude, width: 20, height: 20, iconPath: 'https://oss.lisen.cc/oilnote/images/location.png', title: "我的位置", callout: { content: '我的位置', color: '#adec9c', display: 'BYCLICK', borderRadius: 5, } }; that.makers.push(selfLocation); if (res.status === 0) { res.data.forEach((item, index) => { let obj = { id: index, latitude: item.location.lat, longitude: item.location.lng, width: 20, height: 20, iconPath: iconPath, title: item.title, callout: { content: item.title, bgColor: '#adec9c', display: 'ALWAYS', borderRadius: 5, } }; that.makers.push(obj); }) } }, fail: function(res) { console.log(res); }, }) }markers数组就是我们要标记的坐标点，数组中对象的属性id，表示标记点id，类型为Number，必填项，marker点击事件回调会返回此id，建议为每个marker设置上Number类型id，保证更新marker时有更好的性能。latitude，纬度，类型Number，必填项，浮点数，范围 -90 ~ 90longitude，经度，类型Number，必填项，浮点数，范围 -180 ~ 180title，标注点名，类型String，不是必填，点击时显示，callout存在时将被忽略iconPath，显示的图标，类型String，必填项，项目目录下的图片路径rotate，旋转角度，类型Number，不是必填，顺时针旋转的角度，范围 0 ~ 360，默认为 0alpha，标注的透明度，类型Number，不是必填，默认1，无透明，范围 0 ~ 1width，标注图标宽度，类型Number，不是必填，默认为图片实际宽度height，标注图标高度，类型Number，不是必填，默认为图片实际高度callout，自定义标记点上方的气泡窗口，类型Object，不是必填 - 可识别换行符label，为标记点旁边增加标签，类型Object，不是必填 - 可识别换行符anchor，经纬度在标注图标的锚点，默认底边中点，不是必填，{x, y}，x表示横向(0-1)，y表示竖向(0-1)。{x: .5, y: 1} 表示底边中点。marker上的气泡callout（Object类型）, marker数组上属性callout对象使用属性：content，文本，Stringcolor，文本颜色，StringfontSize，文字大小，NumberborderRadius，callout边框圆角，NumberbgColor，背景色，Stringpadding，文本边缘留白，Numberdisplay，'BYCLICK':点击显示; 'ALWAYS':常显，StringtextAlign，文本对齐方式。有效值: left, right, center，Stringmarker上的标签label（Object类型）content，文本，Stringcolor，文本颜色，StringfontSize，文字大小，Numberx，label的坐标，原点是 marker 对应的经纬度，Numbery，label的坐标，原点是 marker 对应的经纬度，NumberborderWidth，边框宽度，NumberborderColor，边框颜色，StringborderRadius，边框圆角，NumberbgColor，背景色，Stringpadding，文本边缘留白，NumbertextAlign，文本对齐方式。有效值: left, right, center，String2.3.3、界面展示获取到数据之后，就是通过marker属性进行展示 2.3.3.1、地图组件的属性：longitude（类型为Number，没有默认值，表示中心经度）latitude（类型为Number，没有默认值，表示中心纬度）scale（类型为Number，默认值为16，缩放级别取值范围为5-18）markers（类型为Array数组，类型为数组即表示地图上可以有多个，没有默认值，表示标记点）polyline（类型为Array数组，没有默认值，表示路线，数组上的所有点连成线）circles（类型Array数组，表示圆）controls（类型Array数组，表示控件）include-points（类型Array数组，表示缩放视野已包含所有给定的坐标点）enable-3D（类型为Boolean，默认值为false，表示是否显示3D搂块）show-compass（类型为Boolean，默认值为false，表示为是否显示指南针）enable-overlooking（类型为Boolean，默认值为false，表示为是否开启俯视）enable-satellite（类型为Boolean，默认值为false，表示为是否开启卫星图）enable-traffic（类型为Boolean，默认值为false，表示为是否开启实时路况）show-location（类型为Boolean，表示显示带有方向的当前定位点）polygons（类型Array，表示为多边形）2.3.3.2、组件方法@markertap-表示点击标记点时触发，e.detail={markerId}@labeltap-表示点击label时触发，e.detail = {markerId}@callouttap-表示点击标记点对应的气泡时触发，e.detail = {markerId}@controltap-表示点击控件时触发，e.detail = {controlId}@regionchange-表示视野发生变化时触发@tap-表示点击地图时触发; App-nuve、微信小程序2.9支持返回经纬度@updated-表示在地图渲染更新完成时触发
油耗笔记OilNote周边搜索能力由高德地图切换成百度地图
一、交代下背景以前油耗笔记OilNote检索周边加油站功能，都是用的高德地图，起初，也不知道高德Api限制多少访问量，反正一直也没提示过超额，但是今天，莫名的收到了两条超额的短信。一开始收到80%超额的时候也没太当回事，觉得可能这个月改版，调试用的数量比较大，但是紧接着就收到了超额100%的提示，就感觉到不太对劲了，于是登录了高德后台，查询了一下配额。结果真是乖了个乖，搜索服务每日限额100次，这简直就是坑了，100次跟没有没啥区别，高德这是直接劝退个人开发者的节奏。查了下站内信，2022年10月26日发的站内信（没短信），通知2022年10月27日调整限额，连给你调整的时间都不给。坑你没商量呀。二、百度地图为什么选择百度地图呢？介个，国内好像除了高德就是百度了，不用也没办法。因为我就用到了地点检索，百度地图目前还算比较良心的，个人开发者每日限额是5000次。是高德地图的50倍。并发限制都是一样的30QPS。百度地图创建应用的时候，记得选择微信小程序，否则逆地理编码是不能用的。三、微信公众平台配置如果使用百度地图，必须提前把百度地图Api放到域名白名单里面。微信小程序后台，依次定位到开发管理→开发设置→服务器域名，在request合法域名里面加上百度地图的网址https://api.map.baidu.com如果微信开发者工具提示域名不合法，记得刷新一下开发者工具的域名信息。刷新后记得重新编译项目。三、功能改造既然决定了用百度地图，那么剩下的就是如何对程序进行改造了。因为我的小程序是使用uniapp开发的，所以这里就介绍一下uniapp的整个的改造过程。为了不影响在线版本，暂时先不删除高德域名。3.1、manifest.json改造manifest.json用于配置地图的Key，因为我之前使用的是高德的地图，切换到百度后，需要将相关配置改成百度地图的。其实我现在只是用了微信小程序，AK我直接在代码里面写死了，这里配置的appkey我感觉没啥用，对uniapp理解不是很深入，这块可能更多的是给App用的吧。我们Maps需要勾选百度地图并取消高德地图。具体的key，可以在百度开放平台，个人创建的应用中找到。3.2、获取附近加油站功能改造其实改造也比较简单，高德跟百度Api还是比较类似的。3.2.1、原来高德获取周边加油站的代码let getLocation = function(radius = 1000, successFn) { var latitude = '' //纬度 var longitude = '' //经度 if (uni.getStorageSync('position') == '') { uni.getLocation({ geocode: true, type: 'gcj02', altitude: true, accuracy: 'best', isHighAccuracy: true, success: (res) => { console.log('位置是', res) latitude = res.latitude longitude = res.longitude uni.setStorageSync('latitude', latitude) uni.setStorageSync('longitude', longitude) uni.request({ url: 'https://restapi.amap.com/v3/geocode/regeo?key=AK' + '&location=' + longitude + ',' + latitude + '&poitype=010100&radius=' + radius + '&extensions=all&batch=false&roadlevel=0', success: function(res) { var regeocode = res.data.regeocode // 省份名称 uni.setStorageSync('province', regeocode.addressComponent.province) // 城市名称 uni.setStorageSync('city', regeocode.addressComponent.city) // 城市编号 uni.setStorageSync('citycode', regeocode.addressComponent.citycode) successFn(regeocode) }, fail(err) { console.log('获取加油站信息失败：' + JSON.stringify(err)) } }) }, fail: (err) => { console.log('获取加油站信息失败：' + JSON.stringify(err)) } }) } }3.2.2、百度地图获取逆地址编码（根据坐标获取位置）let getLocation = function(radius = 1000, successFn) { var latitude = '' //纬度 var longitude = '' //经度 uni.getLocation({ geocode: true, type: 'gcj02', altitude: true, accuracy: 'best', isHighAccuracy: true, success: (res) => { console.log('位置是', res) latitude = res.latitude longitude = res.longitude uni.request({ url: 'https://api.map.baidu.com/geoconv/v1/?' + 'ak=您的AK（类型是服务端的）' + '&coords=' + longitude + ',' + latitude + '&from=3&to=5&output=json', success: function(res) { debugger if (res.data.status === 0) { longitude = res.data.result[0].x latitude = res.data.result[0].y uni.setStorageSync('latitude', latitude) uni.setStorageSync('longitude', longitude) uni.request({ url: 'https://api.map.baidu.com/reverse_geocoding/v3/?' + 'ak=您的AK（类型是小程序的）&output=json' + '&coord_type=gcj02ll' + '&location=' + latitude + ',' + longitude + '&radius=' + radius, success: function(res) { if (res.data.status === 0) { // 省份名称 uni.setStorageSync('province', res.data .result.addressComponent .province) // 城市名称 uni.setStorageSync('city', res.data .result.addressComponent .city) // 城市编号 uni.setStorageSync('citycode', res.data .result.cityCode) successFn(res.data, res.data.result .addressComponent.province, res.data.result .addressComponent.city) } }, fail(err) { console.log('获取位置信息失败：' + JSON.stringify(err)) } }) } }, fail(err) { console.log('获取位置信息失败：' + JSON.stringify(err)) } }) }, fail: (err) => { console.log('获取位置信息失败：' + JSON.stringify(err)) } }) }需要注意，uni.getLocation获取gcj02时的坐标适用于高德等地图，但是不适用百度地图，需要调用百度地图坐标转换接口geoconv转换成百度地图的坐标，不然会有较大的误差，同时地图转换接口使用的AK需要是服务端类型的应用的AK，也就是说我们实际上使用了两种AK，一种是服务端的，一种是小程序的。3.2.3、百度地图获取附近加油站let getOilStation = function(radius = 1000, successFn) { let latitude = uni.getStorageSync('latitude') //纬度 let longitude = uni.getStorageSync('longitude') //经度 uni.request({ url: 'https://api.map.baidu.com/place/v2/search?' + 'query=充电站$加油站&ak=您的AK（类型是小程序的）&output=json' + '&scope=2' + '&coord_type=2' + '&page_size=20' + '&location=' + latitude + ',' + longitude + '&radius=' + radius, success: function(res) { successFn(res) } }) }改造完成后验证一下四、使用百度地图一些问题目前切换到百度地图之后遇到的一些问题搜索功能显示不全，我家附近其实就有一个中国石化，但是百度地图搜索不到，充电站搜索还算比较全的不能显示图片，如果photo_show开启，会提示授权失败。
解决小程序报错the api need to be declared in the requiredPrivateInfos field in app.json
一、报错信息从报错信息可以大概看到，应该是缺少授权导致的。PS:这里真的想吐槽一下微信的接口，真的是三天两头的变，发个版就得调整一次，心累。二、微信官方文档说明打开微信开发文档查看配置信息。为了开发者能够正常使用获取模糊地理位置等接口，以及后续对于代码提审环节的优化，自 **2022 年 7 月 14 日起，开发者在使用地理位置相关接口时（共计 8 个），需要提前在 app.json 中进行配置。在代码中使用的地理位置相关接口（共计 8 个），开发者均需要在 app.json 中 requiredPrivateInfos 配置项中声明，代码格式如下：地理位置接口新增与相关流程调整 | 微信开放社区三、解决方法因为我是用uni-app框架开发的，所以是在manifest.json里面配置，如果是用其他框架开发的话，一般都是在app.json里面配置，不过原理都是一样的。
微信小程序获取用户头像后上传到七牛云
一、事情起因【油耗笔记OilNote】小程序好久没有升级了，最近打算对代码进行一些优化，但是新版本突然发现无法获取到用户微信头像及微信昵称了。查阅官方文档才知道，官方有对getUserProfile接口进行调整了。自 2022 年 10 月 25 日 24 时后（以下统称 “生效期” ），用户头像昵称获取规则将进行如下调整：自生效期起，小程序 wx.getUserProfile 接口将被收回：生效期后发布的小程序新版本，通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像，昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响，但如果要进行版本更新则需要进行适配。自生效期起，插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回：生效期后发布的插件新版本，通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像，昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响，但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。「头像昵称填写能力」支持获取用户头像昵称：如业务需获取用户头像昵称，可以使用「头像昵称填写能力」（基础库 2.21.2 版本开始支持，覆盖iOS与安卓微信 8.0.16 以上版本），具体实践可见下方《最佳实践》。小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求：对于来自低版本的基础库与微信客户端的访问，小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称，插件通过 wx.getUserInfo 接口将正常返回用户头像昵称，开发者可继续使用以上能力做向下兼容。现在只要是发布的新版本，默认都需要调整，不然就显示下面灰色头像，已经发布的版本不受影响。既然官方调整了，那么我们也只有被动接受的份。二、油耗笔记的开发框架油耗笔记OilNote不是直接使用微信开发者工具开发的，而是使用UniApp开发的，后端是SpringBoot。三、改进思路3.1、获取用户头像由于官方指导意见是，使用button组件 open-type 的值设置为 chooseAvatar，当用户选择需要使用的头像之后，可以通过 bindchooseavatar 事件回调获取到头像信息的临时路径。从官方的指导我们可以看到，微信并没有给我们返回一个具体的路径，只是返回了一个临时的路径，因此我们就必须自己获取到这个临时的文件，然后存储起来。3.2、使用七牛云由于使用的腾讯云的低配服务器，带宽、存储都比较捉襟见肘，所以我打算把头像都存储到七牛云上，既能减轻带宽压力也能节省服务器空间。四、具体改进4.1、UniApp页面改进当用户通过微信登录时，此时获取用户头像信息，如果头像存在，登录之后跳转到首页，否则跳转到个人信息界面，让用户维护头像及昵称，此方法适用于新用户，同时也适用于老用户重新登录。//微信授权登录 getUserInfo(e) { let that = this; var p = this.getSetting(); p.then(function(isAuth) { console.log('是否已经授权', isAuth); if (isAuth) { console.log('用户信息，加密数据', e); //eData 包括//微信头像//微信名称 还有加密的数据. // let eData = JSON.parse(e.detail.rawData); uni.getUserProfile({ desc: 'Wexin', // 这个参数是必须的 success: function(infoRes) { //接下来就是访问接口. that.$request( 'wechat/authCode2Session?code=' + that.weChatCode, 'POST' ).then(function(res) { if (res.code == 200) { //将接口返回的数据保存在全局变量中. let userInfo = {} // 用户id userInfo.id = res.data.id userInfo.username = res.data.username userInfo.tel = res.data.tel userInfo.email = res.data.email userInfo.wechatOpenId = res.data.wechatOpenId userInfo.nickName = res.data.nickName userInfo.avatarUrl = res.data.avatarUrl ? res.data .avatarUrl : infoRes.userInfo.avatarUrl userInfo.gender = infoRes.userInfo.gender userInfo.password = '' if (!userInfo.province) { userInfo.province = uni.getStorageSync('province') } if (!userInfo.city) { userInfo.city = uni.getStorageSync('city') } uni.setStorageSync('userInfo', userInfo); if (!res.data.avatarUrl) { //没有头像时，跳转到用户信息维护界面 uni.redirectTo({ url: '/pages/profile/profile' }) } else { uni.redirectTo({ url: '/pages/index/index' }) } } }, function(err) { uni.showToast({ title: '授权登录失败！', mask: true, icon: 'none' }) } ) } }); } else { uni.showToast({ title: '授权失败，请确认授权已开启', mask: true, icon: 'none' }) } }); },4.1.1、登录界面改造4.1.2、个人信息界面改造油耗笔记之前有一个【个人信息】页面，因此我打算把修改头像的功能发放到里面。在合适的位置放入选择头像的按钮 <view class="cu-avatar xl round margin-center" :style="{backgroundImage:'url('+userInfo.avatarUrl+')'}"></view> 增加回调方法用户选择微信头像之后，会回调chooseavatar方法，因此我们增加一个chooseavatar用于用户选择头像之后上传到服务器（进一步上传到七牛）//选择头像回调 onChooseAvatar(e) { const that = this; this.$set(this.userInfo, "avatarUrl", e.detail.avatarUrl); uni.uploadFile({ url: operate.api + 'user/uploadAvatar/', //上传接口 header: { token: that.userInfo.id ? that.userInfo.id : '', }, formData: { 'userInfo': JSON.stringify(that.userInfo) }, filePath: e.detail.avatarUrl, name: 'file', success: (uploadFileRes) => { uni.hideLoading(); const back = JSON.parse(uploadFileRes.data); if (back.code == 200) { that.$set(that.userInfo, 'avatarUrl', back.data.avatarUrl) } else { uni.showToast(back.msg) } }, fail: (error) => { uni.hideLoading(); uni.showToast("图片上传失败，请联系开发！") }, complete: function() { uni.hideLoading(); } }); }4.2、后端改造我们首先在后端增加一个方法，用于接受前端传递的附件及其他参数（我这里主要传递的是用户信息，用户更新用户表，记录头像地址）。4.2.1、Controller增加接受用户上传头像的Api，具体的实现我们稍后在说。/** * 上传头像 * * @param multipartFile 文件信息 * @return 用户信息 */ @PostMapping("uploadAvatar") public AjaxResult uploadAvatar(@RequestParam("file") MultipartFile multipartFile,@RequestParam("userInfo") String oilUser) { return AjaxResult.success(oilUserService.uploadAvatar(multipartFile)); }4.2.2、增加七牛云依赖经过上面改造，我们已经可以将用户头像上传到我们的后端了，接下来的任务就是将头像上传到我们的七牛云了。现在pom.xml中增加七牛云的依赖 <dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>7.2.28</version> </dependency>4.2.3、增加七牛云配置为了方便使用，我们将七牛云的一些配置信息放入yaml文件中，方便维护。# ========================== ↓↓↓↓↓↓ 七牛云配置 ↓↓↓↓↓↓ ========================== qiniu: accessKey: XXX # Key secretKey: XXX # 密钥 bucket: XXX # 空间名称 domain: XXX # 访问域名 dir: XXX/ # 目录参数说明：accessKey：AK，在七牛云，个人中心，密钥管理中可以看到secretKey：SK，在七牛云，个人中心，密钥管理中可以看到bucket：空间名称，根据自己创建的空间填写domain：访问域名，根据控件绑定的域名实际填写dir：存储路径，因为七牛云默认是直接存储到根目录，为了方便管理，我们可以创建子目录，比如avatar，可以填写avatar/4.2.4、增加配置类为了方便使用，我们将yaml的值，映射到配置类上。/** * 七牛云实体 */ @Component @ConfigurationProperties(prefix = "qiniu") public class QiNiuConfig { /** * Key */ private static String accessKey; /** * 密钥 */ private static String secretKey; /** * 空间名称 */ private static String bucket; /** * 访问域名 */ private static String domain; /** * 目录 */ private static String dir; public static String getAccessKey() { return accessKey; } public void setAccessKey(String accessKey) { QiNiuConfig.accessKey = accessKey; } public static String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { QiNiuConfig.secretKey = secretKey; } public static String getBucket() { return bucket; } public void setBucket(String bucket) { QiNiuConfig.bucket = bucket; } public static String getDomain() { return domain; } public void setDomain(String domain) { QiNiuConfig.domain = domain; } public static String getDir() { return dir; } public void setDir(String dir) { QiNiuConfig.dir = dir; } } 4.2.5、封装公共方法为了方便调用，我们将上传、删除等方法封装到单独的服务中。接口/** * 七牛接口 */ public interface IQiNiuService { /** * 以文件的形式上传 * * @param file * @param fileName: * @return: java.lang.String */ String uploadFile(File file, String fileName) throws QiniuException; /** * 以流的形式上传 * * @param inputStream * @param fileName: * @return: java.lang.String */ String uploadFile(InputStream inputStream, String fileName) throws QiniuException; /** * 删除文件 * * @param key: * @return: java.lang.String */ String delete(String key) throws QiniuException; } 实现@Service public class QiNiuServiceImpl implements IQiNiuService, InitializingBean { // 七牛文件上传管理器 private final Configuration cfg; private final Auth auth; public QiNiuServiceImpl() { // //构造一个带指定 Region 对象的配置类 cfg = new Configuration(Region.huadong()); auth = Auth.create(QiNiuConfig.getAccessKey(), QiNiuConfig.getSecretKey()); } /** * 定义七牛云上传的相关策略 */ private StringMap putPolicy; @Override public String uploadFile(File file, String fileName) throws QiniuException { if (!StringUtils.isEmpty(QiNiuConfig.getDir())) { fileName = QiNiuConfig.getDir() + fileName; } UploadManager uploadManager = new UploadManager(cfg); Response response = uploadManager.put(file, fileName, getUploadToken()); int retry = 0; while (response.needRetry() && retry < 3) { response = uploadManager.put(file, fileName, getUploadToken()); retry++; } if (response.statusCode == 200) { return "http://" + QiNiuConfig.getDomain() + "/" + fileName; } return "上传失败!"; } @Override public String uploadFile(InputStream inputStream, String fileName) throws QiniuException { if (!StringUtils.isEmpty(QiNiuConfig.getDir())) { fileName = QiNiuConfig.getDir() + fileName; } UploadManager uploadManager = new UploadManager(cfg); Response response = uploadManager.put(inputStream, fileName, getUploadToken(), null, null); int retry = 0; while (response.needRetry() && retry < 3) { response = uploadManager.put(inputStream, fileName, getUploadToken(), null, null); retry++; } if (response.statusCode == 200) { return "http://" + QiNiuConfig.getDomain() + "/" + fileName; } return "上传失败!"; } @Override public String delete(String key) throws QiniuException { BucketManager bucketManager = new BucketManager(auth, cfg); Response response = bucketManager.delete(QiNiuConfig.getBucket(), key); int retry = 0; while (response.needRetry() && retry++ < 3) { response = bucketManager.delete(QiNiuConfig.getBucket(), key); } return response.statusCode == 200 ? "删除成功!" : "删除失败!"; } @Override public void afterPropertiesSet() throws Exception { this.putPolicy = new StringMap(); putPolicy.put("insertOnly", 0); } /** * 获取上传凭证 */ private String getUploadToken() { return this.auth.uploadToken(QiNiuConfig.getBucket(), null, 3600, putPolicy); } } 有几个需要注意的点：在构造函数中，构造Configuration时，需要指定区域，因为我是华东区域的，因此使用的是Region.huadong()，如果使用的其他区域的，需要根据自己实际区域指定。在指定策略时，因为我一个用户只允许一个头像，因此上传时，如果存在我们直接覆盖的，所以在afterPropertiesSet方法中，设置上传策略时，直接指定的putPolicy.put("insertOnly", 0);，即如果存在就覆盖，如果不想覆盖，可以设置putPolicy.put("insertOnly", 1);，但是此时需要注意，如果上传重名文件，会返回异常。4.2.6、完善后用户上传头像方法用户上传头像后，更新用户实体（但是此时不更新数据库），将更新后的实体返回到前端，点击保存时，再更新数据库。 @Override public OilUser uploadAvatar(MultipartFile multipartFile, OilUser oilUser) throws IOException { String originalFilename = multipartFile.getOriginalFilename(); if (originalFilename == null || !originalFilename.contains(".")) { throw new CustomException("文件名不正确"); } String fileName = "avatar" + oilUser.getId() + originalFilename.substring(originalFilename.lastIndexOf(".")); String avatarUrl = qiNiuService.uploadFile(multipartFile.getInputStream(), fileName); oilUser.setAvatarUrl(avatarUrl); // LambdaUpdateWrapper<OilUser> userUpdateWrapper = new LambdaUpdateWrapper<>(); // userUpdateWrapper.set(OilUser::getAvatarUrl, oilUser.getAvatarUrl()); // userUpdateWrapper.eq(OilUser::getId, oilUser.getId()); // oilUserMapper.update(null, userUpdateWrapper); return oilUser; }4.2.7、用户保存方法分改造用户保存方法主要增加userUpdateWrapper.set(OilUser::getAvatarUrl, user.getAvatarUrl());,当用户有头像时，同步更新用户的头像信息。/** * 新增或保存用户 * * @param user 用户 * @return 结果 */ public OilUser saveUser(OilUser user) { if (user == null) { throw new CustomException("用户信息不能为空"); } if (user.getId() == null) { user.setId(""); } if (checkUserNameExist(user)) { throw new CustomException("用户名已存在"); } if (StringUtils.isEmpty(user.getId())) { user.setId(UUID.randomUUID().toString()); if (!StringUtils.isEmpty(user.getPassword())) { user.setPassword(passwordEncoder.encode(user.getPassword())); } oilUserMapper.insert(user); } else { LambdaUpdateWrapper<OilUser> userUpdateWrapper = new LambdaUpdateWrapper<>(); userUpdateWrapper.set(OilUser::getUsername, user.getUsername()); userUpdateWrapper.set(OilUser::getNickName, user.getNickName()); userUpdateWrapper.set(OilUser::getTel, user.getTel()); userUpdateWrapper.set(OilUser::getEmail, user.getEmail()); if (!StringUtils.isEmpty(user.getPassword())) { userUpdateWrapper.set(OilUser::getPassword, passwordEncoder.encode(user.getPassword())); } if (!StringUtils.isEmpty(user.getProvince())) { userUpdateWrapper.set(OilUser::getProvince, user.getProvince()); } if (!StringUtils.isEmpty(user.getCity())) { userUpdateWrapper.set(OilUser::getCity, user.getCity()); } if (!StringUtils.isEmpty(user.getAvatarUrl())) { userUpdateWrapper.set(OilUser::getAvatarUrl, user.getAvatarUrl()); } userUpdateWrapper.eq(OilUser::getId, user.getId()); oilUserMapper.update(null, userUpdateWrapper); user.setPassword(""); } return user; }五、效果微信用户登录后，如果没有上传过头像，会自动跳转到【个人信息】页面在个人信息上传头像后，自动跳转到首页。六、其他注意事项七牛云域名需要配置HTTPS小程序域名白名单uploadFile合法域名需要配置后台上传附件的域名。
问题最近使用uniapp做了一个记油耗的App。大家有兴趣的可以点击https://lisen.cc/app/oil_note.html查看。在记油耗OilNote中，有一个根据用户地理位置获取附近加油站的功能，我这里使用的是高德地图的Web服务对应的周边搜索功能，搜索对应的poi。一开始，我主要是在微信小程序还有H5进行测试的，系统都能正确获取到附近的加油站信息。我以为既然是web服务，那么应该所有的平台都是一样的，事实证明，我还是太年轻了。我用两台手机测试的，一台是一加8（系统是Color OS 12)、另外一个华为Mate30 Pro（鸿蒙系统，具体版本不清楚），在一加手机，所有定位服务失效，在华为Mate30 Pro，时好时坏。解决其实现在问题解决了，我也不知道啥原因，解决方式也很简单，自己又在高德开放平台申请了Android的key，然后在HBuilder中配置上，重新用自己的证书打包就好了。1.申请Android平台的key2.配置Android平台的key修改mainfest.json文件，找到App模块配置，在Maps中，使用自己申请的Android平台的key3.使用自己的证书打包App在打包App时，使用自己的证书。注意包名要求高德开放平台填写的报名保持一致。