- 我小程序中通过webview打开微信公众号的页面,不能打开?
同一家公司的小程序(体验版),打开该公司的微信公众号网页,提示:不支持打开非业务域名http://....,请重新配置,请问应该查哪个地方? 登录到发布小程序的地方,看到如下菜单,怎么没有找到 业务域名的配置的地方啊 设置基本设置 第三方设置 关联设置 关注公众号 违规记录
2019-04-10 - 小程序白屏,但是调试模式正常,如何解决?
app_id:wxc61f378eca335be6 三方平台app_id:wxbade6e72297f50f8 [图片][图片][图片][图片]
2023-12-14 - 比较小程序基础库版本号的正确方法
目前我们发现不少开发者不太了解小程序的基础库版本号规则,在这里介绍一下。 小程序基础库版本号使用 semver 规范,格式为 Major.Minor.Patch,Major、Minor、Patch 均为整数,1.9.901、2.44.322、10.32.44 都是符合 semver 风格的版本号。 通常我们月度发布版本会把 Minor 提升一位,例如从 1.9.x 升级到 1.10.x,如果是修正版本,会把 Patch 提升一位,例如 1.10.0 升级到 1.10.1。目前不少开发者使用了错误的版本号比较方法,例如直接用字符串比较,parseInt 比较等,往后当基础库版本号提升上去后,会引发一些逻辑错误。 在这里建议大家使用以下正确方法: 1. 登录 mp 后台(https://mp.weixin.qq.com),点击左侧导航菜单中的「设置」,设置基础库最低版本号,无需编写任何代码即可限制最低运行版本; 2. 使用以下推荐的代码进行判断: [代码]function[代码] [代码]compareVersion(v1, v2) {[代码][代码] [代码][代码]v1 = v1.split([代码][代码]'.'[代码][代码])[代码][代码] [代码][代码]v2 = v2.split([代码][代码]'.'[代码][代码])[代码][代码] [代码][代码]var[代码] [代码]len = Math.max(v1.length, v2.length)[代码][代码] [代码][代码]while[代码] [代码](v1.length < len) {[代码][代码] [代码][代码]v1.push([代码][代码]'0'[代码][代码])[代码][代码] [代码][代码]}[代码][代码] [代码][代码]while[代码] [代码](v2.length < len) {[代码][代码] [代码][代码]v2.push([代码][代码]'0'[代码][代码])[代码][代码] [代码][代码]}[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]i = 0; i < len; i++) {[代码][代码] [代码][代码]var[代码] [代码]num1 = parseInt(v1[i])[代码][代码] [代码][代码]var[代码] [代码]num2 = parseInt(v2[i])[代码][代码] [代码][代码]if[代码] [代码](num1 > num2) {[代码][代码] [代码][代码]return[代码] [代码]1[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](num1 < num2) {[代码][代码] [代码][代码]return[代码] [代码]-1[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]return[代码] [代码]0[代码][代码]}[代码][代码]compareVersion([代码][代码]'1.11.0'[代码][代码], [代码][代码]'1.9.9'[代码][代码]) [代码][代码]// => 1 // 1 表示 1.11.0 比 1.9.9 要新[代码][代码]compareVersion([代码][代码]'1.11.0'[代码][代码], [代码][代码]'1.11.0'[代码][代码]) [代码][代码]// => 0 // 0 表示 1.11.0 和 1.11.0 是同一个版本[代码][代码]compareVersion([代码][代码]'1.11.0'[代码][代码], [代码][代码]'1.99.0'[代码][代码]) [代码][代码]// => -1 // -1 表示 1.11.0 比 1.99.0 要老[代码] 后续版本我们会在基础库中增加版本比较方法,到时候建议大家直接使用该方法。
2018-03-23 - 微信小程序对接企业微信客服
考虑到用户会在企业的小程序里联系客服,为此支持在小程序里接入微信客服。微信小程序打开微信客服的功能已向非个人的全体小程序开放,小程序开发者在小程序管理后台处绑定同主体的微信客服(企业ID)后即可调用小程序相关接口,接入微信客服。 接入方式:https://developer.work.weixin.qq.com/document/path/94739、https://kf.weixin.qq.com/api/doc/path/94772 在微信客服管理后台获取对外的企业ID和客服链接。在小程序管理后台的【功能】【客服】【微信客服】处,填写同一主体的微信客服对应的企业ID,完成小程序和微信客服的绑定。调用「小程序打开微信客服」接口,完成接入。 注:仅可正常接入已在小程序管理后台绑定的企业ID下的微信客服 小程序管理后台-关联企业微信客服注意:企业ID必须跟该小程序的企业主体一致; 在小程序中的接入流程:https://work.weixin.qq.com/nl/act/p/a733314375294bdd 详情如下: [图片] 登录企业微信管理后台-开启微信客服功能参考:https://baijiahao.baidu.com/s?id=1735577920604728565&wfr=spider&for=pc、 https://blog.csdn.net/weixin_42065713/article/details/126137884?from_wecom=1 (1)登录【企业微信管理后台】选择【应用管理】【微信客服】,开启【微信客服】旁边的按钮; 注意:若需要后台对接客服信息的话需要开启“通过API管理微信客服”的,若不需要则不开启。 [图片] (2)然后在【客服账号】一栏点击【创建账号】来指定接待人员;创建客服账号时,企业管理员可以选择展示的视频号,设置接待人员、接待规则、接待上限、接待时间、智能回复、超时结束聊天等内容。 [图片] [图片] (3)选择【接入场景】 在这里我们选择【在微信内其他场景接入】,进入页面后点击【去接入】。企业管理员可以选择需要配置的客服账号,复制客服链接后可以配置到以下场景:在网页接入、在公众号菜单接入、在小程序接入、在搜一搜品牌官方区接入、点击微信支付凭证接入。接入后,客户点击客服入口即可发起咨询。 [图片] [图片] (4)可以在【服务工具】找到相应的配置设置客服的自动回复 [图片] 在小程序中添加打开微信客服的点击事件,调用「wx.openCustomerServiceChat」接口完成接入。 export default { methods: { // 跳转微信客服 jumpToWeChatCustomerService() { openWeChatCustomerService("https://work.weixin.qq.com/xxxxx", "wwed1ca4d3597eXXXX"); }, // 打开微信客服 openWeChatCustomerService ( weiXinCustomerServiceUrl = "", corpId = "", showMessageCard = false, sendMessageTitle = "", sendMessagePath = "", sendMessageImg = "" ) { if (!weiXinCustomerServiceUrl || !corpId) return Toast("请配置好客服链接或者企业ID"); // eslint-disable-next-line no-undef wx.openCustomerServiceChat({ // 客服信息 extInfo: { url: weiXinCustomerServiceUrl, // 客服链接 https://work.weixin.qq.com/xxxxxxxx }, corpId, // 企业ID wwed1ca4d3597eXXXX showMessageCard, // 是否发送小程序气泡消息 sendMessageTitle, // 气泡消息标题 sendMessagePath, // 气泡消息小程序路径(一定要在小程序路径后面加上“.html”,如:pages/index/index.html) sendMessageImg, // 气泡消息图片 success(res) { console.log("success", JSON.stringify(res)); }, fail(err) { console.log("fail", JSON.stringify(err)); // eslint-disable-next-line no-undef return wx.showToast({ title: err.errMsg, icon: "none" }); }, }); }, }, } 常见错误:(1)"fail" "{"errCode":1,"errMsg":"openCustomerServiceChat:fail invalid param: url"}" [图片] 原因是:属性extInfo拼错了。 解决方法:将extInfo属性名写对即可。 (2)sendMessagePath属性设置的小程序绝对路径后,在微信客服消息的气泡消息点击打开会提示“页面不存在”。 在小程序内正常访问路径如:“pages/index/index”是可以访问成功的,但如果在sendMessagePath属性设置该路径的话,在微信客服消息的气泡消息点击打开会提示“页面不存在”。 [图片] [图片] [图片] 解决方法:在小程序文件路径后面加上“.html”即可,如“pages/index/index.html”或者“/pages/index/index.html”都可 [图片]
2023-09-05 - 适配新版获取昵称头像、隐私保护协议接口
蛮记录一下,虽然官方新版获取昵称、头像的api上线已久 目前的做法是封装了一个auth.js用来专门获取用户信息,想要实现的效果是 1、若用户没有授权过昵称、头像,则弹出授权弹窗 2、若用户有授权过,则直接返回昵称头像等其他用户信息 3、允许更新用户信息,也就是不管之前有没有授权过昵称头像,都会再次弹出授权弹窗 4、支持旧版api,主要是考虑到部分用户的微信客户端可能长期未更新,不支持新版api 另外也适配了最近的隐私接口功能。因为获取昵称组件是一个input,不会触发wx.onNeedPrivacyAuthorization,所以这里是给input加了一个catch:touchstart="requirePrivacyAuthorize",在requirePrivacyAuthorize里面调用wx.requirePrivacyAuthorize,从而触发wx.onNeedPrivacyAuthorization。具体代码可以看下improveUserProfile.wxml、improveUserProfile.js 主要代码如下,核心方法 authProcess: auth.js const api = require('/api.js'); const file = require("/file.js"); /** * 保存或者更新用户信息到云端 * @param userInfo * @returns {Promise<unknown>} * @private */ const _saveOrUpdateUserInfo = function (userInfo) { return new Promise((resolve, reject) => { api.router.callCloudUserCenterFunction("UserInfoHandler/saveOrUpdateUserInfo", userInfo, res => { console.log("保存用户信息到云端完毕=>", JSON.stringify(res)); resolve(res); }, res => { console.error("保存用户信息到云端失败=>", res); reject(); } ); }); }; /** * 发起后端请求前先获取用户信息 * 使用新版获取用户信息接口 * @param app * @param basePage * @param update 等于true时表示更新用户信息 * @returns {Promise<any>} */ const authProcess = function (app, basePage, update) { return new Promise(function (resolve, reject) { if (update) { console.log("更新用户信息,重新授权"); _getUserProfile(app, basePage).then(res => resolve(res)).catch(res => reject(res)); } else if (!app.globalData.userInfo.authEver) { console.log("没有用户授权信息,开始授权"); _getUserProfile(app, basePage).then(res => resolve(res)).catch(res => reject(res)); } else { console.log("用户有授权过,无需重新授权"); resolve(app.globalData.userInfo); } }); }; /** * 使用新版接口获取用户信息 * 兼容新版头像昵称填写能力 * @param app * @param basePage * @private */ const _getUserProfile = function (app, basePage) { return new Promise(function (resolve, reject) { const supportChooseAvatar = wx.canIUse('button.open-type.chooseAvatar'); if (supportChooseAvatar) { console.warn("支持头像昵称填写能力"); const improveUserProfile = basePage.improveUserProfile || basePage.selectComponent('#improveUserProfile'); const toast = basePage.toast || basePage.selectComponent('#toast'); if (!improveUserProfile) { console.error("完善用户资料组件缺失,无法继续"); reject("完善用户资料组件缺失"); } else if (!toast) { console.error("toast组件缺失,无法继续"); reject("toast组件缺失"); } else { improveUserProfile.show(({nickName, avatarUrl}) => { const {openid, avatarFileId: oldAvatarFileId} = app.globalData.userInfo; const targetPath = _getHeaderPath(openid); console.log("上传获取到的临时头像:", avatarUrl); toast.showLoadingToast({text: "开始上传头像"}); file.uploadCloudFile(targetPath, avatarUrl).then(fileId => { file.getTempFileURL(fileId).then(fileUrl => { toast.hideLoadingToast(); _getUserProfileCallback(app, basePage, { userInfo: { nickName, avatarUrl: fileUrl, avatarFileId: fileId } }).then(userInfo => resolve(userInfo)); if (oldAvatarFileId) { console.log("删除旧的云储存头像文件", oldAvatarFileId); file.deleteCloudFile([oldAvatarFileId]).then(); } }).catch(res => { toast.hideLoadingToast(); toast.showToast({text: "保存头像链接失败"}); reject("保存头像链接失败"); }) }).catch(res => { toast.hideLoadingToast(); toast.showToast({text: "头像上传失败"}); reject("头像上传失败"); }); file.onUploadProgressUpdate(progress => { toast.showLoadingToast({text: "上传了" + progress + "%"}); }); }, res => { console.log("用户拒绝授权信息:", res); reject(res); }); //reject("需要使用新api授权用户信息"); } } else { console.warn("不支持头像昵称填写能力"); api.getUserProfile( res => { _getUserProfileCallback(app, basePage, res).then(userInfo => resolve(userInfo)); }, res => { console.error("获取用户信息失败:", res); reject(res); } ); //reject("需要使用旧api授权用户信息"); } }); }; /** * 获取到用户信息的后续逻辑 * @param app * @param basePage * @param result * @returns {Promise<unknown>} * @private */ const _getUserProfileCallback = (app, basePage, result) => { return new Promise(function (resolve, reject) { console.log("获取用户信息结果:", JSON.stringify(result)); let {userInfo} = result; //authEver=true时后端做更新操作,false时新增操作 const {authEver} = app.globalData.userInfo || {}; userInfo = Object.assign(app.globalData.userInfo || {}, userInfo, {ready: true, authEver: true}); basePage.setData({ userInfo, }, () => { _saveOrUpdateUserInfo(Object.assign({}, userInfo, {authEver})).then(res => { console.log("用户信息保存完毕,更新后的用户信息:", res); const result = res.result; if (result.success) { userInfo = Object.assign(app.globalData.userInfo || {}, userInfo); basePage.setData({ userInfo, }); } resolve(userInfo); }).catch(res => reject()); }); }); } /** * 获取头像地址 * 不同项目可以自行调整 * @param openid * @returns {string} * @private */ const _getHeaderPath = (openid) => { return "images/" + openid + "/avatarUrl_" + new Date().getTime() + ".webp"; } module.exports = { setUserInfo2Data, authProcess }; 昵称、头像授权弹窗自定义组件 improveUserProfile.wxml <view class="container" wx:if="{{show}}"> <view class="cover {{showCoverAnimation?'cover-fade-in':''}}" catch:touchmove="return"></view> <view class="improve-box {{showBoxAnimation?'slade-in':''}} {{device.isPhoneX? 'phx_68':''}}" catch:touchmove="return"> <view class="title flex-start-horizontal"> <view class="logo"> <image class="icon" src="xxxx"></image> </view> <view class="mini-name">恋爱小清单 申请</view> </view> <view class="tips"> <view class="tip">获取你的昵称、头像</view> <view class="use-for">用于个人中心、共享清单、恋人圈以及分享时展示</view> </view> <form catchsubmit="saveUserInfo"> <view class="info-item flex-center"> <view class="label">头像</view> <view class="input-content flex-full more"> <button class="reset-btn flex-column-left" style="height:100%" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar"> <view class="choose-avatar before" wx:if="{{!avatarUrl}}"> <image class="icon" src="/images/me/choose-avatar.png"></image> </view> <image class="choose-avatar after" src="{{avatarUrl}}" wx:else></image> </button> </view> </view> <view class="info-item flex-center no-border"> <view class="label">昵称</view> <input class="input-content flex-full more" model:value="{{nickName}}" type="nickname" placeholder="输入昵称" placeholder-class="edit-placeholder" focus="{{nickNameFocus}}" always-embed="{{false}}" adjust-position="{{true}}" hold-keyboard="{{false}}" name="nickName" catch:touchstart="requirePrivacyAuthorize"></input> </view> <view class="buttons flex-center"> <button class="cancel reset-btn" form-type="reset" bindtap="close">拒绝</button> <button class="save reset-btn" form-type="submit">保存</button> </view> <!--隐藏参数区域开始--> <input disabled style="display:none;" name="avatarUrl" value="{{avatarUrl}}"></input> <!--隐藏参数区域结束--> </form> </view> </view> <!--toast--> <toast id="toast"></toast> improveUserProfile.wxss @import "/app.wxss"; .cover{ background-color: #111; opacity: 0; position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 5000; transition: opacity .3s; } .improve-box{ position: fixed; left: 0rpx; right: 0rpx; bottom: 0rpx; z-index: 5100; background-color: #fff; box-sizing: border-box; padding-top: 60rpx; padding-left: 50rpx; padding-right: 50rpx; border-radius: 30rpx 30rpx 0 0; transform: translateY(100%); transition: transform .3s, bottom .3s; } .cover-fade-in{ opacity: 0.7; } .slade-in{ transform: translateY(0); bottom: 0rpx; } .improve-box .title{ font-size: 32rpx; font-weight: 500; text-align: center; margin-bottom: 40rpx; color: #161616; } .improve-box .title .icon{ width: 46rpx; height: 46rpx; margin-right: 8rpx; vertical-align: bottom; border-radius: 50%; } .improve-box .title .mini-name{ /*margin-bottom: 2rpx;*/ } .improve-box .tips{ margin-bottom: 20rpx; } .improve-box .tips .tip{ color: #121212; font-size: 34rpx; font-weight: 500; margin-bottom: 12rpx; } .improve-box .tips .use-for{ color: #606266; font-size: 28rpx; margin-bottom: 20rpx; } .improve-box .info-item{ /*margin-bottom: 30rpx;*/ border-bottom: 1rpx solid #f0f0f0; } .improve-box .label{ margin-right: 40rpx; font-size: 32rpx; color: #404040; padding: 30rpx 0 30rpx; } .improve-box .choose-avatar.after{ width: 48rpx; height: 48rpx; display: block; border-radius: 10rpx; } .improve-box .choose-avatar.before{ background-color: #f0f0f0; width: 48rpx; height: 48rpx; display: block; border-radius: 10rpx; padding: 8rpx; box-sizing: border-box; } .improve-box .choose-avatar.before .icon{ width: 100%; height: 100%; display: block; } .improve-box .input-content{ width: 100%; font-size: 32rpx; min-height: 80rpx; height: 80rpx; line-height: 80rpx; border-radius: 10rpx; padding-left: 20rpx; box-sizing: border-box; /*border-bottom: 1rpx solid #dcdee1;*/ position: relative; padding-right: 30rpx; } .edit-placeholder{ color: #c7c7cc; font-size: 32rpx; } .improve-box .buttons{ margin-bottom: 40rpx; margin-top: 50rpx; text-align: center; font-size: 34rpx; font-weight: 550; } .improve-box .buttons .save{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #fff; /*background:#fff linear-gradient(90deg,rgba(246, 120, 121,.9) 10%, rgb(246, 120, 121));*/ background-color: #07c160; margin-left: -50rpx; } .improve-box .buttons .cancel{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #07c160; background-color: #F2F2F2; } .more:after{ content: " "; transform: rotate(45deg); height: 14rpx; width: 14rpx; border-width: 2rpx 2rpx 0 0; border-color: #b2b2b2; border-style: solid; position: absolute; margin-top: -8rpx; right: 4rpx; top: 50%; transition: transform .3s; } improveUserProfile.js //获取应用实例 const app = getApp(); const api = require('../../utils/api.js'); const tabbar = require('../../utils/tabbar.js'); Component({ /** * 组件的属性列表 */ properties: {}, /** * 组件的初始数据 */ data: { show: false, showCoverAnimation: false,//显示类别选择窗口动画 showBoxAnimation: false,//显示类别选择窗口动画 avatarUrl: "", nickNameFocus: false, }, attached() { this.toast = this.selectComponent('#toast'); }, observers: { 'show': function (show) { if (!show) { console.log("获取用户昵称头像弹窗关闭,昵称输入框状态复位"); this.setData({ nickNameFocus: false }) } } }, /** * 组件的方法列表 */ methods: { show(successCallback, failCallback) { const device = app.getSystemInfo(); this.setData({ show: true, device, successCallback, failCallback }, () => { this.setData({ showCoverAnimation: true, showBoxAnimation: true }); }) tabbar.hideTab(this); }, hide() { this.setData({ showCoverAnimation: false, showBoxAnimation: false }, () => { const that = this; setTimeout(function () { that.setData({ show: false }) tabbar.showTab(that); }, 300) }) }, close() { const {failCallback} = this.data; typeof failCallback == 'function' && failCallback("用户拒绝授权"); this.hide(); }, onChooseAvatar(e) { console.log("选择头像:", e); const {avatarUrl} = e.detail; this.setData({ avatarUrl }) }, /** * 模拟隐私接口调用,并触发隐私弹窗逻辑 */ requirePrivacyAuthorize() { if (wx.requirePrivacyAuthorize) { console.warn("当前基础库支持api wx.requirePrivacyAuthorize"); wx.requirePrivacyAuthorize({ success: res => { console.log('用户同意了隐私协议 或 无需用户同意隐私协议'); //用户同意隐私协议后给昵称输入框聚焦 this.setData({ nickNameFocus: true }) }, fail: res => { console.log('用户拒绝了隐私协议'); } }) } else { console.warn("当前基础库不支持api wx.requirePrivacyAuthorize"); this.setData({ nickNameFocus: true }) } }, saveUserInfo(e) { console.log("保存用户资料:", e); const {nickName, avatarUrl} = e.detail.value; if (!avatarUrl) { this.toast.showToast({text: "请选择头像"}); return; } if (!nickName) { this.toast.showToast({text: "请输入昵称"}); return; } const {successCallback} = this.data; typeof successCallback == 'function' && successCallback({nickName, avatarUrl}); this.hide(); } } }); 弹窗效果 [图片]
2023-08-30