- 适配隐私保护协议接口
1、相关api说明 适配过程中主要是用到了以下几个接口 1. wx.onNeedPrivacyAuthorization:用于监听隐私接口需要用户授权事件,只有当隐私协议需要用户授权时才会由平台触发此事件,然后开发者需要弹窗显示隐私协议说明。如果用户拒绝授权,则隐私接口调用失败,在下次调用到隐私接口时还会继续弹。 2. wx.requirePrivacyAuthorize:用于模拟隐私接口调用,并触发隐私弹窗逻辑,也就是会触发wx.onNeedPrivacyAuthorization,但如果用户之前已经同意过隐私授权,会立即返回success回调,不会触发 wx.onNeedPrivacyAuthorization 。这个api的使用场景目前我是用在获取用户昵称时,在下一篇文章会进行说明。 3. wx.openPrivacyContract:用于打开隐私协议页面。 其他api大家可以根据具体情况选择使用。 2、弹窗思路 其实就是写一个自定义组件,然后在有调用到隐私接口的页面引入,利用自定义组件的attached方法,把各个页面的隐私弹窗组件的显示、隐藏方法保存到自定义组件的全局变量中,当用户点击隐私协议弹窗的同意、拒绝按钮时调用resolve方法,将对应的参数通知给平台。 3、注意点 隐私接口在隐私保护指引中有声明才能调用基础库版本2.32.3及以上开始支持2023.9.15号之前,在 app.json 中配置 __usePrivacyCheck__: true 后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。2023.9.15号之后,不论 app.json 中是否有配置 __usePrivacyCheck__,隐私相关功能都会启用wx.onNeedPrivacyAuthorization 是覆盖式注册监听,若重复注册监听,则只有最后一次注册会生效同意授权后如果想取消授权,在开发工具上 清缓存->清除模拟器缓存->清除授权数据,在手机上删除小程序即可隐私弹窗的z-index要设置成最大,避免被其他遮罩挡住导致无法点击,另外如果有调到官方的api例如wx.showLoading,也要注意是否设置了mask,避免在loading的时候弹出隐私弹窗,导致弹窗无法点击,而loading又要等弹窗关闭才会消失的尴尬情况4、代码如下 4.1、app.json "usingComponents": { //全局自定义组件 "privacyPopup": "/components/privacy/privacyPopup" }, //开启隐私相关功能 "__usePrivacyCheck__": true 4.2、自定义组件privacyPopup 4.2.1、privacyPopup.wxml <view class="container" wx:if="{{show}}"> <view class="cover {{showCoverAnimation?'cover-fade-in':''}}" catch:touchmove="return"></view> <view class="privacy-box {{showBoxAnimation?'slade-in':''}} {{device.isPhoneX? 'phx_68':''}}" catch:touchmove="return"> <view class="title flex-start-horizontal"> <view class="logo" wx:if="{{privacyConfig.icon}}"> <image class="icon" src="{{privacyConfig.icon}}"></image> </view> <view class="mini-name">{{privacyConfig.name || '小程序'}}</view> </view> <view class="tips"> <view class="privacy-content"> <view class="start">{{privacyConfig.content.start}}</view> <view class="link" bindtap="openPrivacyContract"> {{privacyConfig.content.mid}} </view> <view class="end">{{privacyConfig.content.end}}</view> </view> </view> <view class="buttons flex-center"> <button class="cancel reset-btn" bindtap="disagree">拒绝</button> <button class="save reset-btn" id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="agree">同意</button> </view> </view> </view> 4.2.2、privacyPopup.wxss @import "/app.wxss"; .cover{ background-color: #111; opacity: 0; position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 50000; transition: opacity .3s; } .privacy-box{ position: fixed; left: 0rpx; right: 0rpx; bottom: 0rpx; z-index: 51000; 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; } .privacy-box .title{ font-size: 32rpx; font-weight: 500; text-align: center; margin-bottom: 40rpx; color: #161616; } .privacy-box .title .icon{ width: 46rpx; height: 46rpx; margin-right: 8rpx; vertical-align: bottom; border-radius: 50%; } .privacy-box .title .mini-name{ /*margin-bottom: 2rpx;*/ } .privacy-box .tips{ margin-bottom: 20rpx; } .privacy-box .tips .privacy-content{ color: #606266; font-size: 30rpx; margin-bottom: 20rpx; line-height: 1.6; } .privacy-box .tips .privacy-content .start, .privacy-box .tips .privacy-content .link, .privacy-box .tips .privacy-content .end { display: inline; } .privacy-box .tips .privacy-content .link{ color: #1890FF; } .privacy-box .buttons{ margin-bottom: 40rpx; margin-top: 50rpx; text-align: center; font-size: 34rpx; font-weight: 550; } .privacy-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; } .privacy-box .buttons .cancel{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #07c160; background-color: #F2F2F2; } 4.2.3、privacyPopup.js //获取应用实例 const tabbar = require("../../utils/tabbar.js"); const app = getApp(); //用来保存各个页面注册的隐私协议回调事件 let privacyHooks = {}; if (wx.onNeedPrivacyAuthorization) { console.warn("当前基础库支持api wx.onNeedPrivacyAuthorization"); wx.onNeedPrivacyAuthorization(resolve => { console.warn("需要隐私协议弹窗:", resolve); const pages = getCurrentPages(); const route = pages[pages.length - 1].route; const hook = privacyHooks[route]; if (hook) { hook.resolve = resolve; hook.show(resolve); } else { console.error("当前页面没有注册隐藏协议弹窗回调:", route); } }) } else { console.warn("当前基础库不支持api wx.onNeedPrivacyAuthorization"); } Component({ data: { show: false, showCoverAnimation: false,//显示类别选择窗口动画 showBoxAnimation: false,//显示类别选择窗口动画 }, lifetimes: { //在组件实例进入页面节点树时执行 attached: function () { console.warn("privacyPopup attached"); const pages = getCurrentPages(); privacyHooks[pages[pages.length - 1].route] = { show: resolve => { this.show(resolve); }, close: () => { this.hide(); } } }, //在组件实例被从页面节点树移除时执行 detached: function () { console.warn("privacyPopup detached"); } }, methods: { /** * 同意隐私协议 * @param e */ agree(e) { Object.values(privacyHooks).forEach(hook => { hook.close(); hook.resolve && hook.resolve({ event: "agree", buttonId: "agree-btn" }); }); }, /** * 不同意隐私协议 * @param e */ disagree(e) { Object.values(privacyHooks).forEach(hook => { hook.close(); hook.resolve && hook.resolve({ event: "disagree" }); }); }, /** * 显示隐私协议授权弹窗 */ show(resolve) { app.fillConfig(this, ["privacyConfig"], data => { console.log("显示隐私协议弹窗"); const device = app.getSystemInfo(); this.setData({ show: true, device, }, () => { this.setData({ showCoverAnimation: true, showBoxAnimation: true }); //自定义隐私弹窗曝光告知平台 resolve({event: "exposureAuthorization"}); }); tabbar.hideTabByPrivacy(this); }); }, /** * 关闭隐私协议授权弹窗 */ hide() { console.log("隐藏隐私协议弹窗"); this.setData({ showCoverAnimation: false, showBoxAnimation: false }, () => { const that = this; setTimeout(function () { that.setData({ show: false }) }, 300) }) tabbar.showTabByPrivacy(this); }, /** * 打开隐私协议 */ openPrivacyContract() { wx.openPrivacyContract({ success: res => { console.log("openPrivacyContract success") }, fail: res => { console.error("openPrivacyContract fail", res) } }) } } }) 5、弹窗效果 [图片]
2023-09-13 - 适配新版获取昵称头像、隐私保护协议接口
蛮记录一下,虽然官方新版获取昵称、头像的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 - 小程序隐私保护授权弹窗组件(Taro和原生版本)
【原生】https://github.com/hsuna/miniprogram-privacy-popup 【Taro】https://github.com/hsuna/taro-privacy-popup 隐私政策弹窗封装: 项目引入privacyPopup组件,在所有使用到隐私授权的页面引入privacyPopup即可,组件内部注册了onNeedPrivacyAuthorization,且通过队列的方式统一管理隐私授权的回调,无需开发者在页面做其他特殊的处理。. 在 page.wxml 中使用组件 针对Input的封装组件 由于Input无法触发onNeedPrivacyAuthorization,使用插槽的形式,通过getPrivacySetting获取用户隐私状态,在用户未同意的情况下,通过catch:touchstart拦截用户点击交互,使用wx.requirePrivacyAuthorize触发onNeedPrivacyAuthorization事件。. 在 page.wxml 中使用组件,并包裹触点区域
2023-09-03 - uniapp开发小程序接入隐私协议弹窗
全文使用Vue2语法 1.首先第一步开启隐私协议配置 manifest.json -> mp-weixin -> 增加__usePrivacyCheck__ [图片] 2.封装全局隐私协议弹窗组件 [图片] 直接贴代码(使用了原子类样式,看含义应该能看出样式布局,弹窗样式不固定各位自由发挥,这里就不贴全局原子类样式了,具体显示如下图) [图片] <template> <view v-if="showPrivacyAuthModel" class="kiwi-modal show animate-fade-in" @touchmove.stop.prevent="stopMove"> <view class="kiwi-dialog "> <view class="kiwi-bar bg-white justify-end"> <view class="content text-bold">{{ name }}</view> </view> <view class="padding-lr-50 padding-tb-20 bg-white"> <view class="text-left padding-bottom-10 text-aj text-indent"> 在你使用【{{ name }}】 服务之前,请仔细阅读<text class="text-blue" @click="ready"> 《{{ name }}隐私保护指引》</text>。如你同意《{{ name }}隐私保护指引》,请点击“同意”开始使用【{{ name }}】。 </view> </view> <view class="kiwi-bar bg-white border-top footer padding-tb-20" :class="showCancel ? 'flex-center-between double padding-lr-20' : 'flex-center-center '"> <button v-if="showCancel" class="kiwi-btn line-grey btn cancel" @click.stop="cancel"> 拒绝 </button> <button class="kiwi-btn bg-green btn confirm" id="agree-btn" open-type="agreePrivacyAuthorization" @agreeprivacyauthorization="confirm"> 同意 </button> </view> </view> </view> </template> <script> export default { name: 'privacy-auth-model', //隐私协议弹窗组件 props: { showCancel: { type: Boolean, default: true, }, }, data() { return { showPrivacyAuthModel:false, name: '小程序名称' } }, bus:{ //下方有全局事件委托插件源码 closePrivacyModel(){//全局事件委托,为了同时关闭其他页面弹出的隐私协议弹窗,例如同时打开的几个tab页面 this.showPrivacyAuthModel = false }, }, mounted() { this.onNeedPrivacyAuthorization() }, methods: { /** * @desc: 用户点击拒绝按钮 * @Author: wkiwi * @function: onNeedPrivacyAuthorization */ handleDisAgreePrivacyAuthorization() { this.showPrivacyAuthModel = false this.resolvePrivacyAuthorization&&this.resolvePrivacyAuthorization({ event: 'disagree' }) }, /** * @desc: 用户点击同意按钮 * @Author: wkiwi * @function: handleAgreePrivacyAuthorization */ handleAgreePrivacyAuthorization() { this.showPrivacyAuthModel = false this.resolvePrivacyAuthorization&&this.resolvePrivacyAuthorization({ buttonId: 'agree-btn', event: 'agree' }) this.$bus('closePrivacyModel') //关闭其他页面授权弹窗 //下方有全局事件委托插件源码 }, /** * @desc: 监听调用隐私API回调 * @Author: wkiwi * @function: onNeedPrivacyAuthorization */ onNeedPrivacyAuthorization(){ if(!wx.onNeedPrivacyAuthorization){ return } let _this = this wx.onNeedPrivacyAuthorization(resolve=>{ console.log('onNeedPrivacyAuthorization'); _this.showPrivacyAuthModel = true _this.resolvePrivacyAuthorization = resolve }) }, cancel () { this.handleDisAgreePrivacyAuthorization() console.log('cancel'); this.$emit('cancel')//组件外部事件预留 }, stopMove () { //遮罩层拦截页面滚动事件 return false }, confirm() { this.handleAgreePrivacyAuthorization() console.log('confirm'); this.$emit('confirm')//组件外部事件预留 }, ready(){ uni.openPrivacyContract({ success: () => {}, // 打开成功 fail: () => {}, // 打开失败 }) }, }, } </script> <style scoped lang="scss"> </style> 3.全局引入隐私协议插件与Bus事件委托 [图片] [图片] bus工具插件代码 import Vue from 'vue'; const bus = new Vue(); /** * 使用方式 * Vue.use(Bus) * this.$bus('eventName', id); * * bus: { * eventName(id) { * console.log(id); * } * } */ export default { install(Vue) { Vue.prototype.$bus = (type, ...args) => { bus.$emit(type, ...args); }; Vue.mixin({ beforeCreate() { const busOptions = this.$options.bus; if (busOptions) { this.$_bus = []; const addListeners = (map) => { for (const event in map) { const handler = map[event].bind(this); bus.$on(event, handler); this.$_bus.push({ event, handler }); } }; if (Array.isArray(busOptions)) { busOptions.forEach(addListeners); } else { addListeners(busOptions); } } }, beforeDestroy() { if (this.$_bus) { for (const listener of this.$_bus) { bus.$off(listener.event, listener.handler); } } } }); Vue.config.optionMergeStrategies.bus = (parent, child, vm) => { if (Array.isArray(parent)) { if (Array.isArray(child)) { return parent.concat(child); } else { parent.push(child); return parent; } } else if (Array.isArray(child)) { child.push(parent); return child; } else if (parent && child) { return [parent, child]; } else if (parent) { return parent; } return child; }; } }; 4.页面使用隐私协议插件 [图片] [图片] 此组件可以在页面主动触发显示隐私协议弹窗,也可以由组件内部的onNeedPrivacyAuthorization被动触发显示,多个tab页面引入显示时,若该页面引入了该组件,该页面调用隐私相关接口,将被动触发组件的显示,用户同意隐私协议时可同步关闭所有打开中的隐私协议弹窗组件。
2023-08-31 - 微信小程序已注册但未认证,想要注销小程序可以参考这篇
微信小程序已申请但未认证,想要注销小程序的可以参考这篇 1、首先在这先扫码查询出你需要注销的小程序原始ID 可以关注公众平台安全助手查询 [图片] 查询地址:https://mp.weixin.qq.com/acct/findacct?action=scan [图片] 2、然后下一步,选择页面上的点击此处 [图片] 3、在打开的页面上传所需要的材料 上传材料链接:https://mp.weixin.qq.com/webpoc/accountBack?token=&lang=zh_CN [图片] 只能帮你到这了能不能注销成功,就看个人运气了,毕竟大厂脑洞也大
2023-07-19 - 基于微信小程序云开发实现考研题库小程序项目(一篇到底)
今天手把手的带大家实现一款答题类的题库小程序,如果着急的话,可以直接去看文末源码下载与项目部署。 考研题库小程序云开发实战,完整版提供给大家学习。题库小程序,基于云开发的微信答题小程序,软件架构是微信原生小程序+云开发。 一、项目预览1、页面结构首页答题页结果页我的页排行榜页答题历史页登录页使用指引页2、功能结构实现页面间跳转功能微信授权登录获取微信头像和昵称等按科目分类题库随机抽题算法支持单选、判断、多选实现用云开发实现查询题库功能实现动态题目数据绑定答题交互逻辑切换下一题答题进度显示提交答卷保存到云数据库集合系统自动判分答题结果页从云数据库查询答题成绩实现转发分享答题成绩功能查询历史成绩取最佳成绩进行排名推荐分享在线客服意见反馈3、小程序端效果预览:[图片] [图片] 1)首页①使用了swiper组件实现轮播图效果,里面使用了image标签展示图片; ②按科目分类的题库; [图片] 2)排名页①对应四个科目,按照得分由高到底进行排名; ②如果同一个人答题多次,取个人最佳成绩进行排名; [图片] 3)我的页①展示个人信息,头像、昵称; ②功能按钮区域,考试记录、推荐给好友、联系客服、意见反馈、使用指引; [图片] 4)答题页①展示当前答题者的信息,头像、昵称; ②答题总数,以及当前答题进度; ③题型、题目、选项、切换下一题按钮; [图片] 5)结果页①展示考生信息,头像、昵称; ②科目、题目总数、得分、答对题数、答错题数、正确率; ③再打一次、返回首页、分享成绩给好友; [图片] 6)答题记录页答题科目、答题时间; [图片] 7)登录页可以进行微信授权登录获取头像、昵称,也可以自定义填写头像、昵称; [图片] 8)使用指引页自定义使用指引或说明; [图片] 4、CMS后台题库管理(新增、查看、搜索、编辑、删除、导入、导出)-管理员权限 1)题库列表[图片] 2)条件筛选搜索[图片] 3)关键词搜索[图片] 4)新增题目[图片] 5)编辑题目[图片] 5、数据库云开发数据库,题目数据表、答题记录数据表、答题成绩数据表; [图片] 二、学习资料1、搭建教程详细讲解手把手搭建教程,我已在前段时间免费分享给大家,请大家移步我的主页翻翻,查阅历史文章吧; 2、配套源码目前源码和配套资源文件,如果有需要的同学可以来公~众~号【木番薯科技】; 3、问题解答(●’◡’●)另外,我提供配套解答服务。你在学习过程中有任何开发问题,或者工作中遇到任何前端问题,都可以来公~众~号【木番薯科技】。 目前可以解答如下问题: 小程序方面的问题;云开发方面的问题;html+css+JavaScript方面等的问题;前端开发的问题;面试找工作方面的问题等。 三、项目创建1、环境准备1.1、 注册小程序账号 到微信公众平台进行注册微信小程序账号。建议使用全新的邮箱,没有注册过其他小程序或者公众号的。 [图片] 1.2 、获取APPID 由于后期调⽤微信⼩程序的接⼝等功能,需要索取开发者的⼩程序中的 APPID ,所以在注册成功后, 可登录,然后获取APPID。 点击开发管理,选择开发设置下面的appid: [图片] 1.3、 下载开发工具 选择开发工具进行下载,并安装: [图片] 2、初始化项目2.1、 打开微信开发者⼯具 注意:第⼀次登录的时候,需要扫码登录。 2.2、 新建⼩程序项⽬ 点击小程序选择添加 [图片] 2.3、 填写项目信息 注意:后端服务选择“微信云开发” [图片] 四、项目结构以及详解1、项目目录结构下面让我们来看看新建小程序应用,初始化后的考研刷题小程序项目目录结构吧。 [图片] 2、小程序配置文件一个小程序应用程序会包括最基本的两种配置文件。一种是全局的 app.json ,另一种是页面的page.json。 2.1、全局配置app.json 小程序根目录下的app.json文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。 以考研刷题小程序为栗子看看,以下是一个包含了部分常用配置选项的app.json: { "pages": [ "pages/index/index", "pages/home/home", "pages/test/test", "pages/result/result", "pages/history/history", "pages/rank/rank", "pages/guide/guide", "pages/my/my" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "考研刷题小博士", "navigationBarTextStyle": "black" }, "tabBar": { "color": "#aaa", "selectedColor": "#ffa517", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/home/home", "iconPath": "/image/sy2.png", "selectedIconPath": "/image/sy2-a.png", "text": "题库" }, { "pagePath": "pages/rank/rank", "iconPath": "/image/zxly2.png", "selectedIconPath": "/image/zxly2-a.png", "text": "排名" }, { "pagePath": "pages/my/my", "iconPath": "/image/wd2.png", "selectedIconPath": "/image/wd2-a.png", "text": "我的" } ] }, "sitemapLocation": "sitemap.json" } pages:页面路径列表;window:用于设置小程序的状态栏、导航条、标题、窗口背景色;tabBar:底部 tab 栏的表现;sitemapLocation:指明 sitemap.json 的位置;注意:这里只解读我这个考研刷题小程序项目里面使用到的配置项,更多配置项自行去技术官网查看。 2.1.1、pages 文件名不需要写文件后缀,框架会自动去寻找对应位置的 .json, .js, .wxml, .wxss 四个文件进行处理。未指定 entryPagePath 时,数组的第一项代表小程序的初始页面(首页)。 "pages": [ "pages/index/index", "pages/home/home", "pages/test/test", "pages/result/result", "pages/history/history", "pages/rank/rank", "pages/guide/guide", "pages/my/my" ] 2.1.2、window backgroundColor:窗口的背景色;backgroundTextStyle: 下拉 loading 的样式,仅支持 dark / light;navigationBarBackgroundColor:导航栏背景颜色;navigationBarTitleText:导航栏标题文字内容;navigationBarTextStyle:导航栏标题颜色,仅支持 black / white;2.1.3、tabbar 如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。 其中 list 接受一个数组,只能配置最少 2 个、最多 5 个 tab。tab 按数组的顺序排序,每个项都是一个对象。而在考研刷题小程序项目里面,我配置了三个,分别是题库、排名、我的。 "tabBar": { "color": "#aaa", "selectedColor": "#ffa517", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/home/home", "iconPath": "/image/sy2.png", "selectedIconPath": "/image/sy2-a.png", "text": "题库" }, { "pagePath": "pages/rank/rank", "iconPath": "/image/zxly2.png", "selectedIconPath": "/image/zxly2-a.png", "text": "排名" }, { "pagePath": "pages/my/my", "iconPath": "/image/wd2.png", "selectedIconPath": "/image/wd2-a.png", "text": "我的" } ] } [图片] 2.2、页面配置 每一个小程序页面也可以使用同名.json文件来对本页面的窗口表现进行配置,页面中配置项会覆盖app.json的window中相同的配置项。 注意:这里只解读我这个考研刷题小程序项目里面使用到的配置项,更多配置项自行去技术官网查看。 例如,首页的配置index.json: { "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black", "navigationBarTitleText": "考研刷题小博士", "backgroundColor": "#eeeeee", "backgroundTextStyle": "light" } backgroundColor:窗口的背景色;backgroundTextStyle: 下拉 loading 的样式,仅支持 dark / light;navigationBarBackgroundColor:导航栏背景颜色;navigationBarTitleText:导航栏标题文字内容;navigationBarTextStyle:导航栏标题颜色,仅支持 black / white; 五、所用技术栈及知识讲解1、模板语法1.1、view标签和text标签 我们现在做的是微信小程序,所以,要使用它的语法。WXML(WeiXin Markup Language)是微信小程序框架设计的一套标签语言。 注意!不要被官方的很官方的概念唬住了,其实就是简单的标签。不信,咱们举个栗子类比看看。相信大多数小伙伴,即使没有做过开发的都听过,html的div和span。 div => view span => text view其实相当于div,都是块级元素,也就是会换行的; text相当于span,都是行内元素,也就是不会换行的。 动手试试吧,在index.wxml直接使用这两个标签就明白了。 <view> 马原 </view> <view> 毛中特 </view> <view> <text> 思修 </text> <text> 近代史 </text> </view> [图片] 什么?还有点迷糊?这样看呢! [图片] [图片] [图片] [图片] 保姆级演示,明白了“view是块级元素,会换行的;text是行内元素,不会换行的”这句话了没? 或许我以后不会这么讲了,因为实在是太太太基础了。简单的事情,只说一遍。 不明白不要紧,就像公式,你不用知道是怎么来的,你只要记住公式是怎么用的就行了。 毕竟咱们做的是应用层面的开发嘛,它提供什么API,知道怎么用的,然后直接去使用就行了。如果能举一反三,就更好了。 1.2、数据绑定 在js的data中定义变量: data:{ title:"考研题库小程序", num:50, isLogin:true, user:{ nickName:"姑苏洛言", age:20 }, isChecked:true } 在 wxml中 直接使用: <!-- 字符串类型 --> <view> {{title}} </view> <!-- 数字类型 --> <view> {{num}} </view> <!-- 布尔类型 --> <view> {{isLogin?'你好,xx会员':'请授权登录'}} </view> <view wx:if="{{isLogin}}">{{user.nickName}}</view> <view wx:else="{{isLogin}}"> 请先登录 </view> <!-- 使用bool类型充当属性check 字符串和花括号之间不要存在空格,否则会导致识别识别 --> <checkbox checked="{{isChecked}}"></checkbox> <!-- 对象类型 --> <view> 昵称:{{user.nickName}},年龄{{user.age}} </view> 再了解一下,其他一些基本运算。 <!-- 算数运算 --> <view>{{a + b}}</view> <!-- 三元运算 --> <view hidden="{{flag ? true : false}}"> Hidden </view> <!-- 逻辑判断 --> <view wx:if="{{length > 5}}"> 今日刷题挑战成功 </view> <view wx:else>今日刷题挑战失败</view> 1.3、列表渲染 列表渲染,其实说白了就是我们说的数组循环。 列表渲染,关键字: wx:forwx:for-itemwx:for-indexwx:keywx:for="{{list1}}" wx:for-item="循环项的名字" => wx:for-item="item" wx:for-index="索引项的名字" => wx:for-index="index" "循环项的名字" 默认 = “item” "索引项的名字" 默认 = “index” 值有两种: 1)当对数组对象做循环的时候, list1:[{id:'1', name: '马原'},{id:'2', name: '毛中特'},{id:'3', name: '思修'},{id:'4', name: '近代史'}] wx:key="id" => item.id 2) 当数组是普通数组, list1: ['马原', '毛中特', '思修', '近代史'] wx:key="*this" index.js Page({ data: { // 普通数组 list1: ['马原', '毛中特', '思修', '近代史'], // 对象数组的循环 list2: [ {id:'1', name: '马原'}, {id:'2', name: '毛中特'}, {id:'3', name: '思修'}, {id:'4', name: '近代史'} ] } }) index.wxml <view> <view>普通数组</view> <view wx:for="{{list1}}" wx:key="*this"> {{index}} --- {{item}} </view> <view>======================</view> </view> <view> <view>对象数组</view> <view wx:for="{{list2}}" wx:key="id"> {{index}}:{{item.name}}--{{item.id}} </view> </view> 1.4、条件渲染 1)wx:if if 、else、 else if 对应微信小程序分别为 wx:if、 wx:else、 wx:elif <view wx:if="{{length > 5}}"> 1 </view> <view wx:elif="{{length > 2}}"> 2 </view> <view wx:else> 3 </view> 2)hidden 在标签上直接加属性hidden 3)什么场景下使用哪一个? 当标签不是频繁的切换显示,优先使用wx:if 直接把标签从页面结构给移除掉; 当表示频繁的切换显示,优先使用hiddem 通过添加样式的方式来切换显示。 1.5、事件的绑定 1)绑定事件通过 bind+事件名; 2)定义事件的回调需要放在 js文件和data同层级。 .wxml <view bindtap="handleTap"> {{num}} </view> .js Page({ data: { num: 0 }, // 声明了点击事件的回调 handleTap() { let num = this.data.num; num++; this.setData({ num }) } }) 2、样式WXSSWXSS( WeiXin Style Sheets )是⼀套样式语言,用于描述 WXML 的组件样式。 与 CSS 相比, WXSS 扩展的特性有: 响应式长度单位 rpx样式导入 2.1、app.wxss app.wxss是默认的全局样式 。把全局的样式的代码都写到这里: page{ background-color: aqua; } page 标签是页面最外层的标签。 2.2、其他样式 1)在wxss中 不支持 通配符 `*` 当我们要实现以下功能的时候, *{ margin: 0; padding: 0; box-sizing: border-box; } 只能够单个标签一个一个的定义了!!! page,view,text{ margin: 0; padding: 0; box-sizing: border-box; } 2.3、小程序中的单位 rpx 功能和以前移动端的web中的`rem` 类似 ,`rpx`(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为`750rpx` 。 1)不管屏幕多宽 , 都是 `750rpx`; 2)不管手机屏幕多宽 都是 100% ; 2.4、样式导入 @import "../../styles/common.wxss"; 通过@Import 引入,用的是相对路径。 3、生命周期只是一个事件而已!! 只是会在特定的时候,会自动触发 。 分为两种: 1. 应用的生命周期 `app.js`; 2. 页面的生命周期 ; 一个微信小程序项目其实一个`应用`,一个应用里面可以拥有多个 `页面`。 3.1、应用的生命周期 看看考研刷题小程序项目中的app.js App({ // 小程序在启动的时候 触发 onLaunch: function (options) { // 当应用开始启动的时候,可以获取用户的一些信息 console.log("onLaunch"); }, // 小程序被展示 的时候触发 // 反复被触发 onShow: function (options) { console.log("onShow"); }, // 小程序被隐藏的时候触发 onHide: function () { console.log("onHide"); }, // 当应用出错了时候会 触发 // 在这里 捕获错误信息 // 把错误信息收集 - 发送ajax异步请求 发送到后台中 onError: function (msg) { // msg :错误信息 console.log("onError"); console.log(msg); }, // 当页面找不到了 就会触发了 onPageNotFound: function (options) { }, // 应用的全局数据 globalData: { title:"生命周期" } }); 3.2、页面的生命周期 看看考研刷题小程序项目中的首页index.js Page({ data: { }, // 页面开始加载的时候触发 // 发送异步请求 获取数据来渲染页面 onLoad: function(options) { console.log("onLoad"); }, // 页面标签都渲染完毕 才触发 onReady: function() { console.log("onReady"); }, // 页面被 显示 页面切换-页面路由 onShow: function() { console.log("onShow"); }, // 页面被隐藏 onHide: function() { console.log("onHide"); }, // 页面被 卸载的时候 - 当页面切换的时候 使用了不同的 open-type 会关闭当前页面的 onUnload: function() { // 可以关闭一些定时任务 console.log("onUnload"); }, // 当页面 下拉刷新的时候触发 // 在全局配置或者 页面配置中 手动开启 下拉刷新!!! onPullDownRefresh: function() { console.log("onPullDownRefresh"); }, // 上拉页面 上拉加载下一页数据 onReachBottom: function() { console.log("onReachBottom"); }, // 当页面被转发的时候 onShareAppMessage: function() { console.log("onShareAppMessage"); }, // 页面被滚动的时候 onPageScroll: function() { console.log("onPageScroll"); }, // 当点击tabbar的时候触发 onTabItemTap:function(item) { } }); 4、原生组件在考研刷题小程序中,常用的原生组件: 4.1、view 视图容器,也就是块级元素。 4.2、text 文本标签,也就是行内元素。 1)text 组件内只支持 text 嵌套。 2)设置user-select属性,长按文本可选、复制。 3)可以对空格、回车进行解析显示。 4.3、image 1)默认的宽度和默认的高度 320 * 240。 2)内置懒加载 lazy-load。 3)mode 渲染模式: scaleToFill: 默认值。把图片内容,拉伸到相框的大小 。widthFix: 把图片变成了和以前web中的图片的渲染模式一样。web图片,当宽度改变的时候,高度会等比例的跟着改变。移动端开发 img width:100%。aspectFit: 等比例拉伸图片-内容,可能会导致,image相框留出空白。aspectFill: 等比例拉伸图片-内容,图片的内容会被截取(图片内容会撑满相框)。 4.4、button 按钮 [图片] [图片] 4.5、radio 单选项目,在考研刷题小程序项目中用于单项选择题。 [图片] 4.6、checkbox 多选项目,属性和单选radio大概一致。在考研刷题小程序项目中用于多项选择题。 六、题库放到云开发数据库1、开通服务,创建表首先需要开通云开发服务,然后创建环境,接着创建集合,以创建题库集合为例。我创建一个题库集合,然后录入题目,都是在云开发控制台可以操作的。 [图片] 2、创建或导入题目给题库表添加记录,也就是录入题目。有两种模式,默认模式和JSON模式。我这里使用默认模式录题。 [图片] [图片] 3、配置权限、env题目都录入或者导入之后,还要配置数据库权限,这样小程序前端就可以直接调用了。在app.js填写环境env。 [图片] 七、前端使用SDK调用云数据库1、实现从题库中随机抽取题目// 获取题库-函数定义 getQuestionList() { // 数据库集合的聚合操作实例 activityQuestion .aggregate() .match({ //类似于where,对记录进行筛选 true: _.exists(true) }) .sample({ size: 20 }) .end() .then(res => { // 在控制台打印数据 console.log(res.list) let data = res.list || []; // 将数据从逻辑层发送到视图层,通俗的说,也就是更新数据到页面展示 this.setData({ questionList:data }); }) } 2、将用户的答题成绩保存到数据库// 提交答卷 addExamRecord(){ wx.showLoading({ title: '提交答卷中' }); let examResult = { wrongList: this.data.wrongList, wrong: this.data.wrong, wrongListSort: this.data.wrongListSort, chooseValue: this.data.chooseValue, totalScore: this.data.totalScore }; activityRecord.add({ data: { ...examResult, createDate: db.serverDate() } }).then(res => { // 跳转到答题结果页,查看成绩 wx.redirectTo({ url: '../results/results' }); wx.hideLoading(); }) } 3、按答题成绩totalScore字段进行降序排序(totalScore越大越靠前)getRankList() { // 数据库集合的聚合操作实例 activityScore .where({ _openid: _.exists(true) }) .orderBy('totalScore', 'desc') .get() .then(res => { // 获取集合数据,或获取根据查询条件筛选后的集合数据。 console.log('[云数据库] [排行榜] 查询成功') console.log(res.data) let data = res.data || []; // 将数据从逻辑层发送到视图层,通俗的说,也就是更新数据到页面展示 this.setData({ rankList:data }); }) } [图片] 八、搭建CMS1、入口在答题小程序的云开发控制台,点击「更多」-「内容管理」; [图片] 2、开通勾选同意协议后,点击确定; [图片] 3、填写账号密码填写管理员账号及密码用于登录内容管理服务平台,点击「确定」完成开通。 [图片] 4、点击访问地址开通完成后,内容管理当前页面可看到内容管理的入口链接和相关信息。点击访问地址,即可在弹出的浏览器窗口中进行内容管理的相关配置。 [图片] 5、登录CMS后台云开发控制台-更多-内容管理页面中,点击「访问地址」即可进入知识竞赛小程序的内容管理界面。 打开内容管理( CMS )后,需使用账密登录,账号密码为开通时设置的管理员账号和密码。 6、创建项目首先,我们需要点击新建项目下方的创建新项目按钮,创建一个名为知识竞答小程序,Id 为zsjdxcx的项目。 [图片] 创建完项目后,点击项目卡片,进入项目的管理页面,我们会看到项目的欢迎页面。 [图片] 7、创建或导入内容模型我们需要建立一个内容模型,描述题目所具有的属性,如:题干,选项,正确答案,分值等。我到时会提供一个数据模型给你,一键导入即可。 8、新建内容在创建完题库的内容模型后,我们可以点击“内容集合”下的题库菜单,开始管理数据库中的题目数据。我们可以点击新建按钮,添加一个新的题目。 [图片] 如下所示,填完表单后,点击创建后,即可生成一条新的题目数据。 [图片] 9、管理内容在题库内容列表页,我们可以对已有的题库内容进行管理,如编辑、删除题目,导入新的题目数据等。 [图片] [图片] 更多资料、资源、资讯可关注【木番薯科技】
2023-03-21 - 关于处理小程序弹窗滚动穿透的心得
在开发小程序时,经常会使用到弹窗(遮罩层+弹窗内容),这时滑动弹窗页面,底层的页面会发生滚动。 个人处理方法(微信开发工具不报错): 逻辑: 1、在遮罩层最外层元素标签中绑定touchmove事件:@touchmove=“xxxx”; 和 catchtouchmove 属性(catchtouchmove是捕获touchmove事件的) 2、在methods里定义该方法,方法返回true :return true 代码: <template> <view catchtouchmove @touchmove="mask"> 遮罩层内容 </view> </template> <script> data() { }, methods:{ mask(){ return true }, } </script>
2023-03-29 - 小程序进行合理分包加载的一些策略
如果对分包不了解,请先阅读分包加载 | 微信开放文档,至少需要阅读使用分包、独立分包、分包预下载三节,此处仅是一种实践,更灵活的用法以官方和具体场景决定。如有错误,恳请指正。 何时需要分包分包的目的在于加快用户下载小程序包的速度,改善用户体验,此处建议小程序所有页面在20个及以下,可以不用分包,超过则进行分包,并将用户高频访问的页面放在主包配置中; 特殊情况如页面少,但某些页面很重,也需要进行分包,具体结合具体页面数、页面数据大小、用户高频访问的页面等实际因素确定如何分包。 推荐的主包和分包的一种文件组织的目录结构如下: ├── sub-pages │ ├── my │ │ ├── setting │ │ └── address │ ├── home │ │ └── goods-list │ ├── activity │ │ └── activity-detail │ └── contact-info │ └── contact-qr-code ├── pages │ ├──base │ │ ├── login │ │ └── 404-page │ ├── home │ └── my └── utils 上述解释为: pages中base为基础页面,包含登录、注册、更新用户头像、昵称、404等基础页面;其他目录为tab页面,如下仅有home和my页面;sub-pages目录存放分包,一级目录为tab对应的模块或者其他独立的模块,此处为my、home、activitys;此处goods-list因页面跳转关系,需要关联activity-detail和my下所有页面,所以在分包的基础上进行关联预下载。contanct-info为联系信息,仅通过第三方小程序跳转,作为独立分包的示例;对应的app.json配置如下 { "pages":[ "pages/home/home", "pages/my/my", "pages/base/login/login", "pages/base/404-page/404-page", ], "subpackages": [ { "root": "sub-pages/my", "pages": [ "address/address", "setting/setting" ] }, { "root": "sub-pages/home", "pages": [ "goods-list/goods-list" ] }, { "root": "sub-pages/activity", "name": "activity-sub", "pages": [ "activity-detail/activity-detail" ] }, { "root": "sub-pages/contact-info", "pages": [ "contact-qr-code/contact-qr-code", ], "independent": true } ], "preloadRule": { "sub-pages/home/goods-list/goods-list": { "network": "all", "packages": ["activity-sub","sub-pages/my"] } } } 如有错误,恳请指正。
2023-07-17