个人案例
- 微信小游戏制作工具接入原生模版广告
嗨!大家好,我是小蚂蚁。 微信小游戏制作工具目前已支持所有类型(单个格子,单行格子,格子矩阵)原生广告的添加,这篇教程会详细的介绍原生广告的接入方法。 创建原生模版广告创建广告位时选择【原生模版广告】。 [图片] 原生模版广告分为两种类型,横幅和格子,横幅就是之前的Banner广告,现在已经合并到原生模版广告中了,这里我们重点了解格子广告。 [图片] 格子广告根据显示的格子数量分为三种。 单格子 [图片] 多格子(一行格子) [图片] 矩阵格子 [图片] 每种类型的格子广告都可以设置属性,例如广告的尺寸(大一点儿还是小一点儿),广告显示颜色(深色还是浅色)等,在右侧的属性栏设置不同的属性,中间的显示区会直接显示调整属性后的效果。 创建完广告之后,可以得到广告位ID,例如下方我创建了四个原生模版广告。 [图片] 广告的属性可以在后台随时进行修改,点击对应广告后面的【编辑】按钮即可。例如我原本创建了背景色为浅色的广告,后来在游戏中发现深色跟游戏更搭配,此时,可以去后台找到该广告,直接编辑其背景色属性为深色。修改属性后需要等待 10 分钟后才会生效。 在游戏中接入广告在微信小游戏制作工具中,接入原生广告需要用到【小游戏】类别中的原生广告这块积木。 [图片] 该积木一共有三个参数,前两个参数用于设置广告在屏幕上的显示位置,最后一个参数用于填广告位ID。 [图片] 接下来重点说一下原生广告的坐标位置设置。 原生广告有着固定的尺寸,每种类型广告的尺寸不同,具体尺寸可查看官方文档:小程序原生模版广告文档(下方二维码直达) [图片] [图片] 如何查看和计算对应广告的尺寸?举个简单的例子,如上图是文档中的单格子模版,单格子模版有两种样式,常规样式和卡片样式,两种样式的广告尺寸不同。除了样式之外,我们在后台创建广告时,还可以在属性中设置广告尺寸的大小,一共有三个选项,分别为 80%,90%,100%。假设我们创建了卡片样式的单格子广告,并将其尺寸设置为 80%。那最终这个广告的尺寸为卡片样式的广告尺寸 68 x 106,然后乘以 80%,最终的尺寸为 54.4x84.8。 通过以上的计算我们能够确定的知道要显示的原生广告的尺寸,接下来我们要做就是如何在一个精确的位置上显示已知尺寸的广告。 我们都知道不同的手机屏幕尺寸是不同的,假设我们希望在屏幕的最下方显示一行格子广告,该如何做呢?设置一个固定数值作为广告显示的坐标?这不太可行,因为不同的屏幕长度不同,设置一个固定的数值,广告在 A 屏幕上显示在最下方,但是在更长的 B 屏幕上就显示不到最下方,这样就有可能遮挡到游戏区域。 那该如何做呢?我们已经知道了单行格子广告的具体的尺寸数值,如果能知道当前屏幕的具体高度,就可以利用屏幕的高度以及单行格子广告的高度,计算出广告的具体显示位置了,并且能够确保在所有尺寸的屏幕上广告都显示在最下方。 [图片] 广告的高度 h 是已知的(可以在文档中查到),现在只需要知道当前设备的屏幕高度 H 即可。 在微信小游戏制作工具中提供了获取场景的宽度和高度的积木,这块积木位于【侦测】类别中。利用这块积木可以获得当前设备屏幕(也就是场景)的宽度和高度。 [图片] 需要的注意的是积木获取到的场景的宽度和高度,并不是最终游戏显示区域的宽度和高度。 [图片] 游戏显示区域的宽度和高度,与场景的宽度和高度存在如上的换算关系。因为原生广告会显示在游戏区域内,所以需要获取的屏幕高度 H 指的是游戏显示区域的高度,即场景高度/2。 最后,我们还需要知道一个关键的信息,所有的原生模版广告的位置都是以左上角为基准点的。 [图片] 如图,不论哪种类型的原生广告,我们所设置的位置指的都是广告左上角的点的位置。 知道了所有必需的数据之后,接下来我们就可以去设置不同类型的广告位置了。 下图是我在真机上测试显示的不同类型的原生广告。 [图片] 举例说明具体广告位置的计算方法。 1.右侧的单格子广告,单格子广告为卡片样式,尺寸设置为 80%。 [图片] 根据文档可以计算出最终广告的尺寸为:54.4 x 84.8。 [图片] 已知场景宽度和广告的宽度,很容易计算出广告的X坐标位置 = 场景宽度/4 - 54.4(注意这里用的是场景宽度除以 4,因为要换算成游戏显示区域的宽度)。 2.下方单行格子广告,水平布局,尺寸设置为 90%。 [图片] 根据文档可以算出,最终显示的广告尺寸为 324x95.4。 [图片] 已知广告的宽度,高度和场景高度,可计算出广告的 X 坐标 = -162,广告的 Y 坐标 = -场景高度/4 + 95.4。 其它广告位置的显示计算方法都类似,学会方法后可自行计算。如果想不明白的话,就动手画个图,图像往往更直观,更容易理解。 最后,你可能发现了,在最终设备上显示的截图中,有些广告的位置并不理想,比如说右侧的单格子右边跑到屏幕外面去了,矩阵广告的左侧紧贴屏幕左侧,而没有显示屏幕中间,对于这些问题可以通过设置数值来进行一些微调。 下方是我使用的广告位置数值,仅供参考,单格子广告与屏幕两侧留出一点距离,下方的单行格子稍微往上提升一点,避免遮挡到下方的文字,矩阵格子显示在屏幕的中央。 [图片] 注意事项最后,有几点注意事项: 1.一个广告ID只能请求到一条广告,如果你想在游戏中同时显示两个单格子广告,需要创建两个原生广告,得到两个广告ID。对于多格子也是同样的,如果你想在屏幕的上方和下方同时显示多格子广告,需要创建两个原生广告。 2.原生广告会每隔一段时间自动刷新,无需手动反复调用积木。 3.在同一位置显示多个原生广告时,广告会重叠。原生广告显示后不会自动关闭,只能使用积木关闭广告,我们需要在游戏中设置好广告的显示和关闭时机,例如在跳转场景的时候,关闭原生广告,尽量避免广告重叠显示。 4.原生广告需要上传游戏后使用体验版在真机上进行测试,无法使用预览版测试。 希望这篇文章对你有用! 欢迎关注我的微信公众号【小蚂蚁教你做游戏】,可领取原创的游戏开发学习路线和教程资料合集。 [图片]
10-25 - 省钱有道之 云开发环境共享小结
#前言 最近为了节省一点小程序的运营成本,一些没啥流量的小程序如果每个月也要19块略微有些肉疼(主要还是穷),研究了一下云环境共享,在这里简单做一下总结。 [图片] 这里有官方的小程序环境共享文档需提前了解一下,具体共享步骤按官方文档操作即可。 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/introduce.html #注意点 共享环境有几个注意点大致如下: 1、必须是相同主体 2、开通了云开发环境的小程序可以共享给同主体的小程序、公众号,被共享方无需开通云开发环境 3、一个云开发环境最多可以共享给10个小程序/公众号 4、共享后双发均可主动解除 5、按官方文档要求,资源方需有云函数cloudbase_auth,测试时发现没有这个云函数其实也能正常运行,可能我验证的场景还不够多 6、云能力初始化的方式不同,资源方按传统的云环境初始化方式即可,也就是 wx.cloud.init({ env: env.activeEnv, traceUser: true }); 而调用方的初始化方式有所不同 const cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); 后续调用资源方的云函数就用这个cloud就行了:cloud.callFunction({...}); 7、调用方有操作到云存储文件的api也需要用6步骤中的cloud 8、云存储fileId需要用cloud.getTempFileURL转换成临时/永久链接,否则在调用方无法展示 9、一些api的云调用方式也有变化,需指明具体的appid。比如A小程序授权给了B小程序,想给B小程序推送客服消息需要写成 await cloud.openapi({appid:B小程序appid}).customerServiceMessage.send({...}); 10、获取调用方的appid/openid/unionid也有所不同 // 跨账号调用时,由此拿到来源方小程序/公众号 AppID console.log(wxContext.FROM_APPID) // 跨账号调用时,由此拿到来源方小程序/公众号的用户 OpenID console.log(wxContext.FROM_OPENID) // 跨账号调用、且满足 unionid 获取条件时,由此拿到同主体下的用户 UnionID console.log(wxContext.FROM_UNIONID) #适配 基于以上注意点,开始进行适配,由于我是一套代码部署N个小程序,然后一个云环境共享给其他小程序,希望通过配置决定哪个小程序作为资源方,哪些作为调用方 首先是云开发环境的初始化: 1、env.js 环境配置: //云开发环境 const cloudBase = { //使用共享云环境资源,资源方=false,调用方=true useShareResource: false, //资源方AppID resourceAppid: "wx9d2xxxxxxxx0088", //资源方环境ID resourceEnv: "prod-9gxqvi3qb3c257ef", //云环境ID prod: "prod-9gxqvi3qb3c257ef" } 2、api.js 操作模块 const env = require('../env.js'); let cloud; /** * 初始化云能力 * @returns {Promise} */ const wxCloudInit = async function () { const {cloudBase} = env; if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else if (cloudBase.useShareResource) { const {resourceAppid, resourceEnv} = cloudBase; // 声明新的 cloud 实例 cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); console.log("初始化云能力完毕:", initRes, "资源方appid:", resourceAppid, "资源方环境ID:", resourceEnv); } else { wx.cloud.init({ env: env.activeEnv, traceUser: true }); console.log("初始化云能力完毕,当前环境:", env.activeEnv); cloud = wx.cloud; } this.cloud = cloud; } /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; 3、在app.js中初始化云环境,后续有用到wx.cloud的都需要改成api.cloud const api = require('utils/api.js'); App({ onLaunch: async function (options) { await api.wxCloudInit(); } }); 其次是资源方的获取用户信息调整 每次都要判断wxContext.FROM_OPENID是否为空,不为空则是调用方的用户信息,为空则是资源方的用户信息,略微繁琐,干脆封装了一个npm包wx-server-inherit-sdk,改造了一下getWxContext函数,源码如下,引入这个包后也就可以不用引入官方的wx-server-sdk const cloud = require('wx-server-sdk'); // 保存原始getWXContext方法到另一个变量 const originalGetWXContext = cloud.getWXContext; cloud.getWXContext = function () { //调用原始getWXContext方法 const wxContext = originalGetWXContext.call(this); const {FROM_APPID, FROM_OPENID} = wxContext; //云开发环境共享时获取到的APPID会替换成源方APPID if (FROM_APPID) { Object.assign(wxContext, {APPID: FROM_APPID}); } //云开发环境共享时获取到的OPENID会替换成源方OPENID if (FROM_OPENID) { Object.assign(wxContext, {OPENID: FROM_OPENID}); } return wxContext; } module.exports = cloud; 到此也就大功告成。为了省钱也是够折腾的[哭笑]
2023-08-28 - 微信小程序-封装请求API——promise方式
目录 第一步:在app.js同级目录下,创建一个文件夹 第二步:封装wx.request方法成promise对象 第三步:页面中引用封装的请求API 1.设置基础请求路径 2.解构传入的参数 3.根据不同的url接口添加不同的header 4.添加请求发起时页面loading效果 完整的封装的API 的 js文件 微信小程序原生的请求API就是wx.request [代码]wx.request({ url: 'example.php', //仅为示例,并非真实的接口地址 data: { x: '', y: '' }, header: { 'content-type': 'application/json' // 默认值 }, success (res) { console.log(res.data) } }) [代码] 有时候不能很好的适配我们的开发需求,比如我们要加一些基础url路径、请求前后的loading效果、不同接口名称下的header。而且,在success回调方法里写请求成功后的操作,看起来代码不太清晰。接下来讲一下封装的逻辑,完整代码放在最后。 第一步:在app.js同级目录下,创建一个文件夹 [图片] 在utils文件夹里新建一个service.js文件,用来放封装的wx.request方法 第二步:封装wx.request方法成promise对象 使用promise对象能很好的解决回调地狱,在.then(res=>{}).catch(err=>{})中能很清晰地看出代码的逻辑 [代码]export const 返回出去的方法名 = (parmas) => { // 返回一个promise对象 return new Promise((resolve, reject) => { wx.request({ url: parmas.url, //仅为示例,并非真实的接口地址 data:parmas.data, header: { 'content-type': 'application/json' // 默认值 }, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) } [代码] 注意:success: (result) => {} 使用箭头函数,防止出现this指向错误 这样,就算是封装了一个最简单、最基础(简陋)的请求API了,在需要使用这个方法的页面的js文件中,引入它 第三步:页面中引用封装的请求API [代码]/** * 小程序中要引用方法,哪个页面要用,就在哪个页面引入 */ import { cjRequest } from "../../utils/service"; Page({ /** * 页面的初始数据 */ data: { .......... .......... getGoodsList () { //因为返回的是promise对象,所以通过.then来获取resolve出来的请求成功的返回数据 cjRequest({ url: "https://xxxtest/goods/search", data: this.QueryParams }) .then((res) => { console.log(res); }) } [代码] 现在,来详细扩展一下封装的请求API 1.设置基础请求路径 [代码]// 基础url const baseUrl = "https://xxxtest" [代码] 这样,就可以简化调用这个方法时url的参数内容了,也方便统一修改开发环境地址、生产环境地址 [代码]// url: "/goods/search" ==》 url中不用再写前面的一长串了 cjRequest({ url: "/goods/search", data: this.QueryParams }).then((res) => { console.log(res); }) [代码] 2.解构传入的参数 我们可以通过ES6中的扩展运算符,将传入到封装方法里的参数直接全部解构出来,不用再一个个获取赋值给对应的键值对了 …parmas 直接解构出传入的参数 [代码]export const 返回出去的方法名 = (parmas) => { //设置基础请求头 const baseUrl = "https://xxxtest" // 返回一个promise对象 return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其解构出的,传入的url:xxx参数 url: baseUrl + parmas.url, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) } [代码] 3.根据不同的url接口添加不同的header header中的Authorization字段一般存储token,用来做身份验证,但有些请求不需要携带token,所以这个封装的API中需要能根据传入的url来判断什么时候该给请求添加上token,什么时候不用可以添加。 [代码]``` export const 返回出去的方法名 = (parmas) => { /** * 根据不同的url接口,来设置不同的header请求头 ** 判断 url中是否带有 /my/ 请求的是私有的路径 带上header token ** { ...parmas.header } ==> 先解构出传进来的header对象,然后再往这个对象里面添加 Authorization字段数据,这样即使有传入header的其他字段也能保留下来 * 如果传入的parmas参数中没有header,那myHeader就是个空的对象 {} 因为啥都没有 */ let myHeader = { ...parmas.header }; //通过includes方法查找字符串中是否包含指定内容,进而判断是否要添加token if (parmas.url.includes("/neddToken/")) { // 往myHeader这个对象里插入键值对 带上Storage中存储的token myHeader["Authorization"] = wx.getStorageSync("token"); } //设置基础请求头 const baseUrl = "https://xxxtest" // 返回一个promise对象 return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其解构出的,传入的url:xxx参数 url: baseUrl + parmas.url, /** * !可以设置上默认的content-type,然后再扩展出传入的myHeader,如果传入的myHeader 为空,那header就还是默认的content-type一个键值对 * !{ 'content-type': 'application/json', ...myHeader } ==》 扩展出myHeader这 个对象中的键值对; */ header: { 'content-type': 'application/json', ...myHeader }, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) [代码] } [代码][代码] 此时,在使用这个封装的API的时候,就可以对header进行设置了,例如: [代码] ``` cjRequest({ url: '/neddToken/home/swiperdata', // 使用的时候也可以传入一些header的字段 header: { 'content-type': 'application/json', 'Date': 'Tue, 15 Nov 2021 08:12:31 GMT' }, method: 'GET', }).then((result) => { console.log(result) }) [代码] [代码][代码] 此时,因为请求url中含有【neddToken】,就会在header中插入token [图片] 此时,这个请求的header中就会添加上token(也就是Authorization这个字段了) [图片] 4.添加请求发起时页面loading效果 当页面在加载数据的时候,最好要有一个loading的提示,同时有遮罩,防止用户乱点 所以,就需要在封装的API中加入微信小程序的wx.showLoading遮罩层了 [代码] // 显示加载中loading效果 wx.showLoading({ title: "加载中", mask: true //开启蒙版遮罩 }); ... ... // 关闭正在等待loading效果 wx.hideLoading(); [代码] 如果直接在封装的API的开始加上loading,在请求结束加上隐藏loading效果,那乍看一下,好像没错,但是细想一下,如果一个页面同时触发了多个请求呢?比如打开一个页面,同时加载多个模块,需要从不同的接口请求数据,那就会使用多次这个封装的API。 此时,就会出现,第一个请求结束,直接关闭了loading效果,而后面几个请求就没有loading效果的遮罩了。 所以,需要在封装的js文件中设置一个全局变量,每次调用这个封装的文件时,就对这个变量++,每次请求结束,返回数据出去的时候,就对这个变量–,最后判断一下这个变量是否为0(也就是所有请求的结束了),在决定是否关闭loading效果 [图片] [图片] 完整的封装的API 的 js文件 [代码]// 同时发送异步代码的次数 let ajaxTimes = 0; export const cjRequest = (parmas) => { // 当有地方调用请求方法的时候,就增加全局变量,用于判断有几个请求了 ajaxTimes++; // 显示加载中loading效果 wx.showLoading({ title: "加载中", mask: true //开启蒙版遮罩 }); /** * 根据不同的url接口,来设置不同的header请求头 ** 判断 url中是否带有 /my/ 请求的是私有的路径 带上header token ** { ...parmas.header } ==> 先解构出传进来的header对象,然后再往这个对象里面添加Authorization字段数据,这样即使有传入header的其他字段也能保留下来 *? 如果传入的parmas参数中没有header,那myHeader就是个空的对象 {} 因为啥都没有 */ let myHeader = { ...parmas.header }; if (parmas.url.includes("/neddToken/")) { // 往myHeader这个对象里插入键值对 带上Storage中存储的token myHeader["Authorization"] = wx.getStorageSync("token"); } // 基础url const baseUrl = "https://api-hmugo-web.itheima.net/api/public/v1" return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其传入的url:xxx参数 url: baseUrl + parmas.url, /** * !可以设置上默认的content-type,然后再扩展出传入的myHeader,如果传入的myHeader为空,那header就还是默认的content-type一个键值对 * !{ 'content-type': 'application/json', ...myHeader } ==》 扩展出myHeader这个对象中的键值对; */ header: { 'content-type': 'application/json', ...myHeader }, success: (result) => { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, // 不管请求成功还是失败,都会触发 complete: () => { /** * !loading效果同时被多个请求触发是可以显示一个的,但是关闭loading一旦被第一个请求完成后关闭,后面的请求触发的loading效果就没了 * !所以,需要通过全局设置一个变量,来监听同时触发了几个请求,当最后一个请求完成后,再关闭loading * ?每次结束请求后,就减少全局变量,当为0时,就表示这是最后一个请求了 */ ajaxTimes--; // 此时就可以关闭loading效果了 if (ajaxTimes === 0) { // 关闭正在等待loading效果 wx.hideLoading(); } } }); }) } [代码] 如果需要按照原生微信请求的格式来封装(调用时要传各种方法、参数),请参考: 微信小程序-封装请求API——原生方式_五速无头怪的博客-CSDN博客[图片]https://blog.csdn.net/black_cat7/article/details/120697607 参考:黑马微信商场小程序 黑马程序员微信小程序开发前端教程_零基础玩转微信小程序_哔哩哔哩_bilibili[图片]https://www.bilibili.com/video/BV1nE41117BQ?p=3
2023-10-18 - 请问如果实现不使用默认资源进度条,全部资源加载后启动游戏?
你好,在使用微信小游戏制作工具的过程中,客户提出一个需求,就是自定义加载界面。 看了下文档和公众号,有一个B站的UP主视频,是关于切换场景时候加载的,逻辑大概看明白了,通过开启(资源管理)插件,借助(预加载XX场景,XX场景加载进度,XX场景预加载完成)来实现加载的逻辑,但是这个是在某场景中,加载其他场景时候用到的,和需求不符 。 我们需要的是一个所有资源加载完毕后才显示游戏,且这个界面是用户自定义。 在作品设置 - 使用默认加载进度条 取消了勾选,并设置第二个选项 资源加载方式 全部加载后启动,点击保存,使用默认资源进度条 会自动勾选;尝试使用切换场景加载方式,不使用默认资源进度条,点击保存,同样 使用默认资源进度条 会自动勾选。 请问有全部资源加载,且使用自定义加载界面的例子吗? 还是说设置一个场景,使用自定义加载的图片,在这个场景内逐预加载后续所有场景,来实现全部资源加载的目的,但是这个和作品设置内的默认加载进度条有什么关系呢? 不太明白,自定义加载界面,设置的顺序和操作步骤,希望官方有时间给看看,谢谢~
2023-03-11 - 关于微信小游戏可视化开发工具的常见问题20211210
常见问题 为了让大家更快更好的熟悉工具,每周会把大家的问题进行汇总并给出一个解决方案,方便大家参考! 1. 怎么删掉函数 在资源管理模块,右键就能看到删除按钮 [图片] 2.怎样创建容器 按住ctrl,在图层板块点击想要组合成容器的精灵,右键就能看到合成按钮。 [图片] 或者使用快捷键:ctrl+g 3.列表中如何获得全部9 所在的位置 这个积木获取到的是9第一次出现的位置 [图片] 目前工具没有提供相关积木,可以通过循环找出全部的位置 [图片] 结果: [图片] 4.表格里的数据如果动态变化成了 0,“”,NULL之后就不会在改变了,这种情况怎么办? 比如我动态改变数据的第一个位置的数据,并且这个位置还是数字类型的0, [图片] [图片] 这是属于工具的BUG,一般情况下不会出现。如果你的项目中出现了这种情况,可以这样设置: [图片] 让0变成字符串类型,就不会出现不能修改的情况了
2021-12-10