- 【记录】小程序地理位置获取:百度地图逆地理编码
近期学习小程序考虑到很多类型需要用到获取用户地理位置相关信息部分,在这里记录一下。 小程序开发文档有获取地理位置的api [图片] 只是这里获取到的是用户所在地区的经纬度,还需要再做进一步的改善,这里我使用的是百度地图开放平台的api 贴上链接地址:https://lbsyun.baidu.com 要使用百度地图的api需要经过以下流程: [图片] 获得ak才能使用百度地图api的相关服务,然后可以根据自己需要新建一个应用(可以选择小程序或web端),最终得到如下图所示: [图片] [图片] 只不过现在百度地图api文档有所变化,查找不到请求地址(估计版本有所变化),参考了一下csdn博客 https://blog.csdn.net/qq_30109365/article/details/98082346?ops_request_misc=&request_id=&biz_id=102&utm_term=%E7%99%BE%E5%BA%A6%E5%9C%B0%E5%9B%BEapi%E9%80%86%E5%9C%B0%E7%90%86%E7%BC%96%E7%A0%81&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-2-98082346.nonecase&spm=1018.2226.3001.4187 附上小程序获取经纬度以及百度地图获取城市地址的代码 我在这里封装了一个函数,页面设计了一个按钮添加点击事件,点击即可获取地址信息 [图片] 随后可在控制台查看得到的结果: [图片] 还只是个初学小白,如果相关错误请多多指正😉
2021-11-23 - 如何使用微信小程序·云开发的Node.js云函数生成Word文档(2021-10-15更新)
编者按 近期一个云开发项目有生成Word文档的需求,经过搜索,发现并没有小程序·云开发有关生成word文档的案例,因为本人还是本科生且非科班出身,一路摸着石头过河,遇到了不少困难,期间还试图向社区的大佬们求助;花了两天时间才搞定这一百行代码,现在分享给大家。 代码有些糙,希望大佬们不要嫌弃。 一、安装云函数依赖officegen、fs 工欲善其事必先利其器,我们知道云函数代码运行在云端Node.js环境中,因此,理论上来说,Node.js能做的事情,小程序·云开发的云函数基本上也能做到。officegen是Github上一款生成微软Office文档的工具,包括.docx、.xlsx、.pptx三种文件,由于我只用了.docx,本文将以Word文件为例。 https://github.com/Ziv-Barber/officegen [图片] 1. 首先我们在微信开发者工具中 新建一个云函数 => 右键云函数名 => 在终端中打开 [图片] 2. npm安装依赖officegen和fs,为了方便本地调试云函数,我们这里也安装wx-server-sdk。 [图片] 代码如下,请逐个安装,如果安装有问题,可以自行搜索“npm”或“npm taobao 镜像” ;这里不再赘述。 npm i officegen npm i fs npm i wx-server-sdk 3. 在云函数index.js开头写下以下代码,引用我们刚刚安装的包。 const cloud = require('wx-server-sdk') const officegen = require('officegen'); const fs = require('fs'); const docx = officegen('docx'); 二、创建Word文档的内容 文档地址: https://github.com/Ziv-Barber/officegen/blob/master/manual/docx/README.md 1. 首先我们根据文档定义(Ctrl CV)两个函数 //文档生成完成后调用,后来其实发现没啥用 // Officegen calling this function after finishing to generate the docx document: docx.on('finalize', async function (written) { console.log('Finish to create a Microsoft Word document.') }) //生成文档出现问题时调用 // Officegen calling this function to report errors: docx.on('error', function (err) { console.log(err) }) 2. 创建段落API: docx.createP(options) //声明一个创建段落的变量p0bj let pObj = docx.createP(options) //创建一个段落并插入文本 pObj = docx.createP({ align: 'center' //文字对齐方式,center、justify、right;默认为left indentLeft = 1440; // 段落缩进 Indent left 1 inch indentFirstLine = 440; // 首行缩进 }) pObj.addText('你要插入的文字,这里可以时变量', { bold: true, //是否加粗,默认false font_face: 'KaiTi', //字体,这里以“楷体为例”,如果填写了打开文档的电脑没有安装的字体名称,将使用默认字体。能不能用中文,我没试过。 font_size: 19, //字号 color: '595959' //文字颜色 }); 上述例子外,还可以添加下划线、设置斜体、超链接、分页等;还可以编辑页眉和页脚、插入图片等。详见后续代码示例或officegen文档。 3. 插入图片 这里以插入小程序码为例,直接上代码。 要注意的是officegen似乎不支持以buffer形式插入图片,因此要先将图片保存。 //首先定义一个用于保存小程序码图片的函数 //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } //要获取小程序码,首先要修改云函数config.json文件中的云调用权限 { "permissions": { "openapi": [ "wxacode.getUnlimited" ] } } //在云函数main中获取小程序码 //https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.get.html const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', //小程序页面地址,必须是线上版本中存在的页面的完整地址 scene: '', //小程序码参数 width: 240, //小程序码的宽度(是个正方形) }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // 这里的fileData是Buffer类型,关于路径会在第三部分生成Word文件中解释。 //将图片插入到文档中 pObj = docx.createP() //创建段落 pObj.options.indentFirstLine = 440; //首行缩进 pObj.addImage('/tmp/qr.jpg', { //图片文件路径 cx: 140, //长度 cy: 140 //宽度 }); 三、生成Word文件 文档内容完成后,就可以生成文档了。officegen似乎只能生成文件,没有文件buffer的接口,而要上传到小程序·云开发的云存储中,只能使用Buffer或fs.ReadStream,怎么办呢?先把文件保存下来再读取呗。 首先提一下云函数运行环境 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/mechanism.html 云函数运行在云端 Linux 环境中,一个云函数在处理并发请求的时候会创建多个云函数实例,每个云函数实例之间相互隔离,没有公用的内存或硬盘空间。云函数实例的创建、管理、销毁等操作由平台自动完成。每个云函数实例都在 [代码]/tmp[代码] 目录下提供了一块 [代码]512MB[代码] 的临时磁盘空间用于处理单次云函数执行过程中的临时文件读写需求,需特别注意的是,这块临时磁盘空间在函数执行完毕后可能被销毁,不应依赖和假设在磁盘空间存储的临时文件会一直存在。如果需要持久化的存储,请使用云存储功能。因此,我们将文件保存在/tmp路径下,文件名随便起,这里我取为exampl.docx。生成文档的代码如下: // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/example.docx') // Async call to generate the output file: docx.generate(out) 理论上来说,我们文档生成完毕后,通过fs.ReadFileStream读取文件调用cloud.uploadFile()即可上传到云存储 const fileStream = fs.createReadStream('/tmp/example.docx') return await cloud.uploadFile({ cloudPath: '/tmp/example.docx', fileContent: fileStream, }) 而在测试过程中我发现,云端测试时,云函数调用超时。而后使用本地调试查看问题出在何处。 云函数本地调试的方法不再赘述,看这里即可。https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/local-debug.html 通过本地调试,发现cloud.uplodaFile()的网络请求始终时挂起(pending)状态,没有传输数据。 [图片] 经过一天的调试,通过监听文件,发现officegen生成文件完成,执行了我们开头复制粘贴的生成文档后执行的docx.on("finalize",)函数,打印文档生成成功的日志后,仍有文件变动,也就是说,文件并没有生成完毕。这就导致了后续步骤的失败。 当时调试的界面我没有保存,就贴一下fs监听文件的代码吧。 let watcherObj = '/tmp/example.docx' //eventType 可以是 'rename' 或 'change'; 当改名或出现或消失的时候触发rename; recursive:是否监听到内层子目录,默认false; try { let myWatcher = fs.watch(watcherObj,{encoding:'utf8',recursive:true},(event,filename) => { if(event == 'change'){ console.log("触发change事件") } console.log(event) //encoding:文件名编码格式,buffer、默认:utf8等;filename有可能为空 if(filename){ console.log('filename: ' + filename) } }) //change 事件会触发多次 myWatcher.on('change',function(err,filename){ console.log(filename + '发生变化'); }); //50秒后 关闭监视 setTimeout(function(){ myWatcher.close() },5000); } catch (error) { console.log('文件不存在!!') } 为解决这一问题,我最先想到了await,结果发现await对officegen生成文档的接口并不起作用;最终我用了最原始的笨办法:用setTimeout等一会儿再读取文件,大佬们有更好的解决方案还请赐教。 return new Promise((resolve, reject) => { setTimeout(async function () { let data = fs.readFileSync('/tmp/example.docx'); let bufferData = new Buffer.from(data, 'base64'); console.log(bufferData); setTimeout(async function () { resolve(await cloud.uploadFile({ cloudPath: varpath, fileContent: bufferData, })); }, 1000); //等文件再读1秒 }, 6300); //等文件再写一会儿。根据自己的需求调试后确定等待时长,要预留出一定时间确保文档完全生成完毕。 }) //最终返回内容为文件云存储中的CloudID。 四、完整核心代码 const cloud = require('wx-server-sdk') const officegen = require('officegen'); const fs = require('fs'); const docx = officegen('docx'); cloud.init({ env: '这里填入你的云环境' }) // Officegen calling this function after finishing to generate the docx document: docx.on('finalize', async function (written) { console.log('Finish to create a Microsoft Word document.') }) // Officegen calling this function to report errors: docx.on('error', function (err) { console.log(err) }) //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } // 云函数入口函数 exports.main = async (event, context) => { var time = new Date() var filePath = 'exportVoluntaryData' var fileName = "zyzm" + Date.parse(new Date()) + '.docx' var varpath = filePath + '/' + fileName //get QRcode const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', scene: item._id, width: 240, }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // Add a Footer: var footer = docx.getFooter().createP(); footer.addText('XXXX证明_' + item._id, { font_size: 10 }); footer = docx.getFooter().createP(); footer.addText(time.toString(), { font_size: 10 }); //下方开始文档每一页的循环 for (var i in item.volunteerInfo) { //标题 let pObj = docx.createP({ align: 'center' }) pObj.addText('XXX证明', { bold: true,XXX font_face: 'KaiTi', font_size: 19, color: '595959' }); //此处省略了一些正文内容 pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('微信扫描下方小程序码,可核验此证明。', { font_face: 'FangSong', font_size: 12, color: '595959', italic: true, }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addImage('/tmp/qr.jpg', { cx: 140, cy: 140 }); pObj = docx.createP() pObj = docx.createP({ align: 'right' }) pObj.addText('落款', { font_face: 'FangSong', font_size: 15, color: '595959' }); if (i != ((item.volunteerInfo).length - 1)){ docx.putPageBreak() //分页 } } // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/example.docx') // Async call to generate the output file: docx.generate(out) return new Promise((resolve, reject) => { setTimeout(async function () { let data = fs.readFileSync('/tmp/example.docx'); let bufferData = new Buffer.from(data, 'base64'); console.log(bufferData); setTimeout(async function () { resolve(await cloud.uploadFile({ cloudPath: varpath, fileContent: bufferData, })); }, 1000); }, 6300); }) } 本人非计算机相关专业本科生,且本文大部分内容为手打,难免会有差错和疏漏,还请各位指教。 希望本文对你有所帮助。 Soochow University. HaoChen. 2020年2月 ======= 2021-10-15更新 ======= 经过一段时间的使用,上述内容主要存在两点问题:(1)难以判断文件何时生成完毕;(2)连续调用生成文档时,若上一个云函数实例未被销毁,会出现文件内容重复和错乱的问题。 前一段时间进行了更新,因为工作学习忙碌,此次暂不做详解,代码如下。 入口文件index.js// 云函数入口文件 delete require.cache[require.resolve('officegen')]; const cloud = require('wx-server-sdk') var office = require('office.js'); //https://github.com/Ziv-Barber/officegen/blob/master/manual/docx/README.md cloud.init({ env: 'sudaxmt1900' }) const db = cloud.database() const _ = db.command // 云函数入口函数 exports.main = async (event, context) => { return await office.genWord(event); } office.jsconst cloud = require('wx-server-sdk') const fs = require('fs'); function delDir(path) { console.log("delete Dir") let files = []; if (fs.existsSync(path)) { files = fs.readdirSync(path); files.forEach((file, index) => { let curPath = path + "/" + file; if (fs.statSync(curPath).isDirectory()) { delDir(curPath); //递归删除文件夹 } else { fs.unlinkSync(curPath); //删除文件 } }); // fs.rmdirSync(path); // 删除文件夹自身 } } readDocx_fs = function (path) { return new Promise((resolve, reject) => { fs.readFile(path,(err,data)=>{ resolve(data); reject(err); }) }) } //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } exports.genWord = async (event) => { let officegen = require('officegen'); let fs = require('fs'); let docx = officegen('docx'); //ini delDir('/tmp') var item = event.item var filePath = 'exportVoluntaryData' var fileName = "21zyzm" + Date.parse(new Date()) + '.docx' var varpath = filePath + '/' + fileName //=========以下建构文档内容========== //get QRcode const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', scene: item.id, width: 140, }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // 这里的fileData是Buffer类型 timeBottom = time.getFullYear() + '年' + (time.getMonth() + 1) + '月' + time.getDate() + '日' for (var i in item.volunteerInfo) { let pObj = docx.createP({ align: 'center' }) pObj = docx.createP({ align: 'center' }) pObj.addText('志愿服务时间证明', { bold: true, font_face: 'KaiTi', font_size: 19, color: '595959' }); pObj = docx.createP() pObj = docx.createP({ align: 'justify' }) pObj.options.indentFirstLine = 440; if (item.volunteerInfo[i].academy && item.volunteerInfo[i].major && item.volunteerInfo[i].grade) { var txt = item.volunteerInfo[i].academy + ' ' + item.volunteerInfo[i].major + '专业 ' + item.volunteerInfo[i].grade + ' ' + item.volunteerInfo[i].name + ' 同学(学号 ' + item.volunteerInfo[i].idnum + '),于 ' + date + '参加 ' + item.title + ' 工作,志愿服务时间达到 ' + item.hours + ' 小时。' } else { var txt = item.volunteerInfo[i].name + ' 同学(学号 ' + item.volunteerInfo[i].idnum + '),于 ' + date + '参加 ' + item.title + ' 工作,志愿服务时间达到 ' + item.hours + ' 小时。' } pObj.addText(txt, { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('特此证明。', { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('证明人:' + event.tea_info.name + ' ' + event.tea_info.phone, { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('微信扫描下方小程序码,可核验此证明。核验信息与此证明一致时,此证明不加盖公章仍然有效;若不一致,则以加盖公章的证明为准。', { font_face: 'FangSong', font_size: 12, color: '595959', italic: true, }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addImage('/tmp/qr.jpg', { cx: 140, cy: 140 }); pObj = docx.createP() pObj = docx.createP() pObj = docx.createP({ align: 'right' }) pObj.addText('XXXXX', { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP({ align: 'right' }) pObj.addText(timeBottom, { font_face: 'FangSong', font_size: 15, color: '595959' }); // Add a Footer: pObj = docx.createP() pObj = docx.createP() pObj = docx.createP() pObj.addText('XXXXX证明_' + item._id, { font_face: 'FangSong', font_size: 10, color: '808080' }); pObj = docx.createP() pObj.addText(time.toString(), { font_face: 'FangSong', font_size: 10, color: '808080' }); if (i != ((item.volunteerInfo).length - 1)) { docx.putPageBreak() } } //=======================建构文档内容结束========================= // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/' + fileName) return new Promise((resolve, reject) => { docx.generate(out); out.on('close', async function(){ console.log("文件已被关闭,总共写入字节", out.bytesWritten) // console.log('写入的文件路径是'+ out.path); var fileBuf = await readDocx_fs(out.path); var upd = await cloud.uploadFile({ cloudPath: varpath, fileContent: fileBuf, }); console.log(docx) resolve({ event, upd, size: Math.floor(100*out.bytesWritten/1024)/100 + "KB" }) }); out.on('error', (err) => { console.error(err); reject({ errMsg: err }) }); }) }
2021-10-15 - 云开发实战:小程序导出Ecxcl表格功能
需求 作为信息收集者需要把用户填写的内容,用Ecxcl表格的方式导出。可以支持在线查看或者复制文件下载链接。 [图片] 查看 excle 表格可以直接在手机上查看表格 复制下载地址可以通过链接在电脑上下载 实现 以收集姓名信息为例: 云函数代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() //操作excel用的类库 const xlsx = require('node-xlsx'); // 云函数入口函数 exports.main = async (event, context) => { try { let { dataList, excelName } = event // 1. 定义excel表格名 let dataCVS = 'test.xlsx' // 2. 定义存储数据的 let alldata = []; // 3. 定义表头 let row = ['姓名']; alldata.push(row); // 4. 循环取出姓名数据 for (let key in dataList) { let arr = []; arr.push(applyList[key].name); alldata.push(arr) } //5. 把数据保存到excel里 var buffer = await xlsx.build([{ name: "mySheetName", data: alldata }]); //6. 把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: excelName + ".xlsx", fileContent: buffer, }) } catch (e) { console.error(e) return e } } [代码] 注意上传部署云函数的时候需要先安装[代码]node-xlsx[代码] 安装步骤: 打开命令行 [图片] 输入安装命令:[代码]npm install node-xlsx --save[代码] 小程序调用代码 通用调用云函数生成表格代码部分 [代码]createExcel(type) { // 提示加载中 wx.showLoading({ title: '正在加载中...', }) console.log('请求获取') let data = { dataList: 姓名数据集合, excelName: 文件名 } // 生成excel并存储 wx.cloud.callFunction({ name: 'excel', data: data }).then(res => { // 获取存储文件ID this.getFileUrl(res.result.fileID,type) }) } [代码] 根据不同的type来实现不同的功能 2.1 type为0时查看excel 2.2 type为1时复制地址 [代码] getFileUrl(fileID, type) { let that = this; wx.cloud.getTempFileURL({ fileList: [fileID], success: res => { // get temp file URL that.setData({ fileUrl: res.fileList[0].tempFileURL }) // 下载文件并且打开文档 if (type == 0) { wx.downloadFile({ url: res.fileList[0].tempFileURL, success: function (res) { const filePath = res.tempFilePath wx.openDocument({ filePath: filePath }) } }) } else if (type == 1) { // 复制地址 wx.setClipboardData({ data: res.fileList[0].tempFileURL }) } } }) }, [代码] 扩展阅读 以上涉及到的API文档地址: node-xlsx wx.setClipboardData wx.uploadFile wx.downloadFile wx.openDocument
2021-02-25 - 开源小程序-高仿微软待办To-Do源码分析
项目简介 此项目基于小程序云开发,你不需要自己搭建服务器环境。产品原型参考自Microsoft To-Do(微软待办) 项目界面 待办小程序:包含了待办首页、待办列表、待办详情、重要事项、待办列表,包含了待办小程序的所有基础功能。 待办首页: [图片] 点击右上角菜单可以呼出重要事项和代办列表入口: [图片] 待办详情 [图片] 重要事项 [图片] 待办列表 [图片] 代码分析 前端代码亮点 可以看到以上三个页面:待办首页、重要待办、待办列表都是很多布局都是重复的,所以在这里作者采用了把相同布局封装成自定义组件的方式。 待办首页: [代码]<!--index.wxml--> <view class="page"> <view class="page-top"> <view class="top-left"> <view class="day-text">我的一天</view> <view class="day-date"> {{currentDate}} </view> </view> <view class="top-right"> <view bindtap="openMenuPopup"> <van-icon name="wap-nav" /> </view> </view> </view> <view class="page-content"> <todo-list loading="{{loading}}" empty-text="您今天还没有任务~" todo-list="{{todoList}}"></todo-list> </view> <!-- todo input --> <view class="todo-input-wrapper fixed-bottom"> <todo-input bind:success="onAddTodoSuccess"></todo-input> </view> <!-- 右侧菜单 --> <van-popup show="{{ showMenuPopup }}" position="right" custom-style="height: 100%;" bind:close="closeMenuPopup"> <side-menu bind:getuserinfo="onGetUserInfo" bind:click-menu-list-item="clickMenuItem" user-info="{{userInfo}}" subscript="{{menuSubscript}}" /> </van-popup> <!-- 在页面内添加对应的节点 --> <van-notify id="van-notify" /> </view> [代码] 重要事项: [代码]<!--miniprogram/pages/important/important.wxml--> <view class="page important-wrapper"> <view class="page-content"> <todo-list loading="{{loading}}" todo-list="{{todoList}}"></todo-list> </view> <!-- todo input --> <view class="todo-input-wrapper fixed-bottom"> <todo-input bind:success="onAddTodoSuccess" page-type="{{1}}"></todo-input> </view> </view> [代码] 待办列表 [代码]<!--miniprogram/pages/important/important.wxml--> <view class="page important-wrapper"> <view class="page-content"> <todo-list loading="{{loading}}" todo-list="{{todoList}}"></todo-list> </view> <!-- todo input --> <view class="todo-input-wrapper fixed-bottom"> <todo-input bind:success="onAddTodoSuccess" page-type="{{2}}"></todo-input> </view> </view> [代码] 自定义组件的好处有以下两点,在看看封装的自定义组件。 可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用; 可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。 todolist wxml [代码]<!--components/todolist/todolist.wxml--> <view class="todo-lsit-wrapper"> <view wx:if="{{loading}}"> <van-skeleton row="6" /> </view> <view wx:else> <view wx:if="{{todoList.length > 0}}" class="todo-list-content"> <todo-item bind:checkboxchange="checkboxChange" bind:clicktodoitem="clickTodoItemHandle" bind:clicktodoright="clickTodoItemRight" todo="{{item}}" data-todo="{{item}}" wx:for="{{todoList}}" wx:key="_id" /> </view> <view wx:else class="no-data"> <image src="../../images/no-data.png"></image> <view style="margin-top:20rpx;"> {{emptyText}} </view> </view> </view> </view> [代码] wxss [代码]/* components/todolist/todolist.wxss */ .todo-lsit-wrapper{ padding:0 20rpx; margin-top: 20rpx; } .no-data{ color: #fff; text-align: center; font-size: 12px; } [代码] js [代码]// components/todolist/todolist.js Component({ /** * 组件的属性列表 */ properties: { todoList: { type: Array, value: [] }, loading: { type: Boolean, value: true }, emptyText: { type: String, value: '数据是空的~' } }, /** * 组件的初始数据 */ data: { }, /** * 组件的方法列表 */ methods: { checkboxChange(event) { console.log('ev', event) }, clickTodoItemHandle(event) { console.log(event) }, clickTodoItemRight(event) { console.log(event) } } }) [代码] json [代码]{ "component": true, "usingComponents": { "todo-item": "../todo-item/TodoItem" } } [代码] 在list里面包含了item组件,接下来我们来看到item的具体代码。 wxml [代码]<!--components/todo-item/TodoItem.wxml--> <view class="todo-item-wrapper"> <view class="todo-item-content"> <view class="todo-check"> <van-checkbox bind:change="onChange" value="{{ todo.done }}"></van-checkbox> </view> <navigator hover-class="none" url="/pages/todo-detail/todo-detail?id={{todo._id}}"> <view class="todo-body"> <view class="todo-title van-ellipsis" style="{{todo.done ? 'color:#a0a0a0;text-decoration:line-through;' :''}}">{{todo.description}}</view> <!-- <view class="todo-des van-ellipsis">日期:{{todo.create_date_format}}</view> --> <view class="todo-des van-ellipsis"> <view wx:if="{{todo.isShowMyday && !todo.fromIndex}}" style="display:flex;aligin-item:center; margin-right:10px;"> <view>我的一天</view> </view> <view wx:if="{{todo.isShowDueDate}}" style="color:red; display:flex;aligin-item:center;"> <view>{{todo.due_date_format}}</view> </view> </view> </view> </navigator> <view class="todo-operate" bindtap="onClickTodoItemRight"> <van-icon wx:if="{{!todo.isImportant}}" name="star-o" /> <van-icon wx:else color="{{todo.isImportant ? '#A9C3F8' :''}}" name="star" /> </view> </view> </view> [代码] wxss [代码]/* components/todo-item/TodoItem.wxss */ .van-ellipsis { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .van-multi-ellipsis--l2 { -webkit-line-clamp: 2; } .van-multi-ellipsis--l2, .van-multi-ellipsis--l3 { display: -webkit-box; overflow: hidden; text-overflow: ellipsis; -webkit-box-orient: vertical; } .van-multi-ellipsis--l3 { -webkit-line-clamp: 3; } .todo-item-content { background-color: #fff; border-radius: 15rpx; display: flex; align-items: center; justify-content: space-between; padding: 0 20rpx; height: 100rpx; /* border-bottom: 1px solid #dedede; */ margin-bottom: 4px; } .todo-body { width: 580rpx; padding-left: 20rpx; font-size: 14px; min-height: 30rpx; } .todo-body .todo-des { font-size: 12px; display: flex; } .todo-des { color: #a0a0a0; } [代码] js [代码]// components/todo-item/TodoItem.js Component({ /** * 组件的属性列表 */ properties: { todo: { type: Object, value: {} } }, /** * 组件的初始数据 */ data: { checked: false }, /** * 组件的方法列表 */ methods: { onChange(event) { let done = event.detail let todoId = this.data.todo._id const db = wx.cloud.database() db.collection('todos').doc(todoId).update({ data: { done: done, complete_date: done ? new Date() : null }, success: function(res) { console.log(res) } }) this.setData({ todo: { ...this.data.todo, done, complete_date: done ? new Date() : null } }); this.triggerEvent('checkboxchange', event.detail) }, onClickTodoItem() { this.triggerEvent('clicktodoitem') wx.navigateTo({ url: '/pages/todo-detail/todo-detail?'+this.data.todo._id, }) }, onClickTodoItemRight() { let todoId = this.data.todo._id const db = wx.cloud.database() let isImportant = !this.data.todo.isImportant db.collection('todos').doc(todoId).update({ data: { isImportant } }) this.setData({ todo: { ...this.data.todo, isImportant } }); this.triggerEvent('clicktodoright') }, remove(){ wx.showToast({ title: 'hi', }) } } }) [代码] 在这里作者把数据库操作和跳转操作都疯转到了组件中去,这样的好处就是比较省事加上业务高度一致所以可以这样做,不过如果想更加灵活可以把这些与业务耦合的内容放在相关的业务页面去编写会更好,然后再去封装业务代码。 然后再看下todo-input wxml [代码]<!--components/todo-input/TodoInput.wxml--> <view class="todo-input-wrapper"> <view class="add-icon"> <van-icon name="plus" /> </view> <view class="input-component-wrapper"> <input bindinput="todoInputHandle" bindconfirm="todoInputConfirmHandle" class="input-component" value="{{todoValue}}" placeholder-style="color:#fff;" placeholder="请输入代办事项" /> </view> <view bindtap="todoInputConfirmHandle" class="enter-icon"> <van-icon name="upgrade" /> </view> </view> [代码] wxss [代码]/* components/todo-input/TodoInput.wxss */ .todo-input-wrapper { display: flex; align-items: center; height: 90rpx; width: 100%; justify-content: space-between; background-color: rgba(100, 96, 96,.6); color: #fff; border-radius: 10rpx; font-size: 16px; } .add-icon, .enter-icon { width: 80rpx; display: flex; height: 90rpx; align-items: center; justify-content: center; } .input-component-wrapper { width: 540rpx; padding-left: 20rpx; } .input-component { width: 100%; color: #fff; height: 90rpx; } [代码] js [代码]// components/todo-input/TodoInput.js import { addTodoItem } from '../../utils/todoDbHelper.js' import Notify from '../../miniprogram_npm/@vant/weapp/notify/notify'; Component({ /** * 组件的属性列表 */ properties: { pageType: { type: Number, value: 0 } // 0我的一天 ,1重要 ,2代办列表 }, /** * 组件的初始数据 */ data: { todoValue: '' }, /** * 组件的方法列表 */ methods: { todoInputHandle(e) { this.data.todoValue = e.detail.value }, todoInputConfirmHandle(e) { let that = this let todoValue = this.data.todoValue let pageType = this.data.pageType console.log('pageType', pageType) if (!todoValue) { Notify({ type: 'warning', message: '请输入代办事项!' }) return } let addParams = { description: todoValue, } if (pageType === 0) { addParams.isMyday = true addParams.addMydayDate = new Date() } if (pageType === 1) { addParams.isImportant = true } console.log('addParams', addParams) addTodoItem(addParams).then(res => { // res 是一个对象,其中有 _id 字段标记刚创建的记录的 id console.log('插入成功', res) that.triggerEvent('success', res) that.setData({ todoValue: '' }) }) } } }) [代码] 在这里作者用到了一个操作工具类 todoDbHelper [代码]const db = wx.cloud.database() const dbCollection = db.collection('todos') /** * 通过id查询单个todo详情 */ export const queryTodoDetailById = (id) => { return dbCollection.where({ _id: id }).get() } /** * 添加todoItem 三个地方,我的一天,重要,代办列表 */ export const addTodoItem = (params) => { const defaultParams = { // description: description, // 描述,标题 create_date: new Date(), // 创建时间 isMyday: false, addMydayDate: null, // 添加到我的一天的时间 due_date: null, // 结束时间 complete_date: null, // 完成时间 done: false, // 是否完成 isImportant: false, // 是否重要 remark: '', // 备注 type: 0, remind: false, // 是否提醒 remind_date: null // 提醒时间 } return dbCollection.add({ data: { ...defaultParams, ...params } }) } /** * 更新todoItem */ export const updateTodoItem = (id, params) => { return dbCollection.doc(id).update({ data: { ...params } }) } /** * 删除todoItem */ export const removeTodoItem = (id) => { return dbCollection.doc(id).remove() } [代码] 这种封装的方式可以学习,所有数据库操作封装到一个工具类中去执行。但是在这里要说一下,我觉得可以封装的更彻底一点就是所有数据库才做都可以写在这里面,比如:item组件中的update操作。 云开发代码 整体来说,云开发代码比较简单是基础的增删查改,因为业务相对简单,除了上面提到过的在小程序调用的add、update、remove、get之外还有两个云函数。 查询数量:待办数量、重要待办数量 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 云函数入口函数 exports.main = async(event, context) => { const wxContext = cloud.getWXContext() // 先取出集合记录总数 const countResult = await db.collection('todos').where({ _openid: wxContext.OPENID }).count() const isImportantResult = await db.collection('todos').where({ isImportant: true, _openid: wxContext.OPENID }).count() const isImportantCount = isImportantResult.total const count = countResult.total return { count, isImportantCount } } [代码] 分页查询todo列表和按日期条件查询 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const moment = require('moment') cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command const MAX_LIMIT = 100 // 云函数入口函数 exports.main = async(event, context) => { const wxContext = cloud.getWXContext() // 先取出集合记录总数 const countResult = await db.collection('todos').count() const isImportantResult = await db.collection('todos').where({ isImportant: true }).count() const isImportantCount = isImportantResult.total const count = countResult.total const queryCount = event.count ? event.count : 10 // 查询参数 const dbParams = event.dbParams ? event.dbParams : {} // openid dbParams._openid = wxContext.OPENID // 我的一天条件/当天 if (dbParams.isMyday) { let curDate = moment().format('YYYY-MM-DD'); let nextDate = moment().add(1, 'days').format('YYYY-MM-DD') dbParams.addMydayDate = _.gte(new Date(curDate)).and(_.lte(new Date(nextDate))) } // 计算需分几次取 const batchTimes = Math.ceil(queryCount / 100) // // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { const promise = db.collection('todos').where(dbParams).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() tasks.push(promise) } // 等待所有 let data = (await Promise.all(tasks)).reduce((acc, cur) => { return { data: acc.data.concat(cur.data), errMsg: acc.errMsg, } }) return { data: data.data, count, isImportantCount, event } } [代码] 总结 整体来说这个小程序很适合新手学习,逻辑相对简单,功能实用性较强。里面有一些代码封装的思维值得学习如:自定义组件、工具类封装这些,如果能封装的更加彻底就更好了。
2021-12-28 - 小程序图片上传,存储,获取,显示
我们在做小程序开发时,难免会遇到图片上传的功能,我们如果自己搭建图片服务器的话,成功太大了,并且还要写后台程序来接收上传的图片,还要有存储服务器。好在小程序云开发为我们提供了云存储的功能,这样我们就可以轻松的实现小程序图片的上传和存储。 老规矩,先看效果图 [图片] 本节知识点 1,小程序图片的选取 2,小程序图片的上传 3,小程序图片的存储 4,获取云端图片并显示 下面就来具体讲解下具体实现步骤 图片的选择和上传 index.wxml文件如下 [代码] <view class='item_root' bindtap='chuantupian'> <text>{{zhaopian}}</text> <view class='right_arrow' /> </view> [代码] index.js文件如下 [代码] //上传图片 chuantupian() { let that = this; let timestamp = (new Date()).valueOf(); wx.chooseImage({ success: chooseResult => { wx.showLoading({ title: '上传中。。。', }) // 将图片上传至云存储空间 wx.cloud.uploadFile({ // 指定上传到的云路径 cloudPath: timestamp + '.png', // 指定要上传的文件的小程序临时文件路径 filePath: chooseResult.tempFilePaths[0], // 成功回调 success: res => { console.log('上传成功', res) wx.hideLoading() wx.showToast({ title: '上传图片成功', }) if (res.fileID) { that.setData({ zhaopian: '图片如下', imgUrl: res.fileID }) } }, }) }, }) }, [代码] 到这里其实我们就可以实现图片的选取和上传功能了。 下面讲讲具体是如何实现的 首先我们通过wx.chooseImage来获取相册里的图片 再获取照片成功后,我们用当前时间戳命名图片,然后使用 wx.cloud.uploadFile方法来实现图片的上传 在上传成功后,会有如下回调。下图中的filenId就是我们在云存储中的路径,可以直接用这个路径来获取图片并显示的。 [图片] 到这里我们就轻松的实现了小程序图片上传的功能,是不是很简单。 有任何关于编程的问题都可以加我微信2501902696(备注编程开发) 编程小石头,码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑。 视频讲解地址:https://edu.csdn.net/course/play/9604/281187 [图片]
2019-06-11 - 借助云开发实现小程序模版消息推送(不用搭建服务器就可以实现微信消息推送)
上一节给大家将了借助云开发实现小程序支付功能,那么我们就要想了,能不能借助云开发实现小程序消息推送功能呢? 还别说,云开发还真能实现推送的功能。 一直关注我的同学肯定知道老师之前也写过借助java后台实现小程序消息推送的文章。 我们借助java后台虽然也能轻松的实现消息推送。但是呢?用java开发后台推送,必须要搭建服务器,学习java代码,部署java代码当然你就是做java开发的,或者学习过java,这没什么。 但是作为小程序开发人员来说,用java显得太重了。 传送门: 《借助小程序云开发实现小程序支付功能(含源码)》 《5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)》 下面就来教大家如何借助云开发实现小程序模版消息的推送功能。 老规矩,先看效果图 [图片] 下面来讲实现步骤 一,定义推送的云函数 由于我们的云推送功能只能在云函数里调用,所以我们这里必须要在云函数里实现推送功能。 1,首先我们定义一个云函数push0524。 如果你还不知道如何使用云开发,如何定义云函数,去翻下老师之前的文章。有写的。 [图片] 把完整的代码贴给大家 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { console.log(event) return sendTemplateMessage(event) } //小程序模版消息推送 async function sendTemplateMessage(event) { const { OPENID } = cloud.getWXContext() // 接下来将新增模板、发送模板消息、然后删除模板 // 注意:新增模板然后再删除并不是建议的做法,此处只是为了演示,模板 ID 应在添加后保存起来后续使用 const addResult = await cloud.openapi.templateMessage.addTemplate({ id: 'AT0002', keywordIdList: [3, 4, 5] }) const templateId = addResult.templateId //新增的模版id const sendResult = await cloud.openapi.templateMessage.send({ touser: OPENID, templateId, formId: event.formId, page: 'pages/index/index', data: { keyword1: { value: '云开发实现推送', }, keyword2: { value: '2019 年 5 月 24 日', }, keyword3: { value: '编程小石头', }, } }) //删除模版id await cloud.openapi.templateMessage.deleteTemplate({ templateId, }) return sendResult } [代码] 上面代码所实现的就是 1,创建模版,拿到模版id 2,使用模版ID,填充模版消息,发送模版 3,删除模版。 我们正常开发时,模版都是在小程序后台获取到的。这里是为例演示方便。所以正常开发时,只需要实现第二步就行了。 推送的关键代码就是这个方法: cloud.openapi.templateMessage.send 通常我们定义完push0524云函数以后,如果直接调用的话,会报错误的。 [图片] 来看下这个错误,看到红色框里的permission就知道,肯定是权限的问题。所以我们在定义完云函数以后,要在push0524云函数下面添加权限配置页面。如下图 [图片] 重要的就是这个: “templateMessage.send”, 推送权限。因为推送是云开发给我们提供的,我们这里调用时,必须配置相关权限,才能使用的。 到这里我们的推送功能就实现了。下面我们来验证下。 二,验证云开发推送 验证其实很简单,和我们之前的《5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)》 类似。只不过一个是在java后台推送,一个是在小城里推送。下面我们简单写个小程序里验证推送的demo。 功能很简单 1,获取formid,因为推送必须有formid的 2,点击调用push0524实现推送 [图片] 简单的贴下代码 [图片] [图片] 需要注意的一点:我们测试时,必须要真机测试。因为模拟器没法获取到formid的。 [图片] 我们在推送成功的success回调中打印下log。如果log中出现,send:ok字样,就代表我们推送成功了。来看下推送成功的效果。 微信聊天列表接收到了消息提醒 [图片] 消息内容 [图片] 到这里我们就用云开发实现完整的消息推送功能了。是不是很简单。 有任何关于编程的问题都可以加老师微信 2501902696(备注小程序)也可以找老师索要完整源码。 编程小石头码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - 小程序读取excel表格数据,并存储到云数据库
最近一直比较忙,答应大家的小程序解析excel一直没有写出来,今天终于忙里偷闲,有机会把这篇文章写出来给大家了。 老规矩先看效果图 [图片] 效果其实很简单,就是把excel里的数据解析出来,然后存到云数据库里。说起来很简单。但是真的做起来的时候,发现其中要用到的东西还是很多的。不信。。。。 那来看下流程图 流程图 [图片] 通过流程图,我看看到我们这里使用了云函数,云存储,云数据库。 流程图主要实现下面几个步骤 1,使用wx.chooseMessageFile选择要解析的excel表格 2,通过wx.cloud.uploadFile上传excel文件到云存储 3,云存储返回一个fileid 给我们 4,定义一个excel云函数 5,把第3步返回的fileid传递给excel云函数 6,在excel云函数里解析excel,并把数据添加到云数据库。 可以看到最神秘,最重要的就是我们的excel云函数。 所以我们先把前5步实现了,后面重点讲解下我们的excel云函数。 一,选择并上传excel表格文件到云存储 这里我们使用到了云开发,使用云开发必须要先注册一个小程序,并给自己的小程序开通云开发功能。这个知识点我讲过很多遍了,还不知道怎么开通并使用云开发的同学,去翻下我前面的文章,或者看下我录的讲解视频《5小时入门小程序云开发》 1,先定义我们的页面 页面很简单,就是一个按钮如下图,点击按钮时调用chooseExcel方法,选择excel [图片] 对应的wxml代码如下 [图片] 2,编写文件选择和文件上传方法 [图片] 上图的chooseExcel就是我们的excel文件选择方法。 uploadExcel就是我们的文件上传方法,上传成功以后会返回一个fildID。我们把fildID传递给我们的jiexi方法,jiexi方法如下 3 把fildID传递给云函数 [图片] 二,解下来就是定义我们的云函数了。 1,首先我们要新建云函数 [图片] 如果你还不知道如何新建云函数,可以翻看下我之前写的文章,也可以看我录的视频《5小时入门小程序云开发》 如下图所示的excel就是我们创建的云函数 [图片] 2,安装node-xlsx依赖库 [图片] 如上图所示,右键excel,然后点击在终端中打开。 打开终端后, 输入 npm install node-xlsx 安装依赖。可以看到下图安装中的进度条 [图片] 这一步需要你电脑上安装过node.js并配置npm命令。 3,安装node-xlsx依赖库完成 [图片] 三,编写云函数 我把完整的代码贴出来给大家 [代码]const cloud = require('wx-server-sdk') cloud.init() var xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async(event, context) => { let { fileID } = event //1,通过fileID下载云存储里的excel文件 const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] //用来存储所有的添加数据操作 //2,解析excel文件里的数据 var sheets = xlsx.parse(buffer); //获取到所有sheets sheets.forEach(function(sheet) { console.log(sheet['name']); for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; //第几行数据 if (rowId > 0 && row) { //第一行是表格标题,所有我们要从第2行开始读 //3,把解析到的数据存到excelList数据表里 const promise = db.collection('users') .add({ data: { name: row[0], //姓名 age: row[1], //年龄 address: row[2], //地址 wechat: row[3] //wechat } }) tasks.push(promise) } } }); // 等待所有数据添加完成 let result = await Promise.all(tasks).then(res => { return res }).catch(function(err) { return err }) return result } [代码] 上面代码里注释的很清楚了,我这里就不在啰嗦了。 有几点注意的给大家说下 1,要先创建数据表 [图片] 2,有时候如果老是解析失败,可能是有的电脑需要在云函数里也要初始化云开发环境 [图片] 四,解析并上传成功 如我的表格里有下面三条数据 [图片] 点击上传按钮,并选择我们的表格文件 [图片] 上传成功的返回如下,可以看出我们添加了3条数据到数据库 [图片] 添加成功效果图如下 [图片] 到这里我们就完整的实现了小程序上传excel数据到数据库的功能了。 再来带大家看下流程图 [图片] 如果你有遇到问题,可以在底部留言,我看到后会及时解答。后面我会写更多小程序云开发实战的文章出来。也会录制本节的视频出来,敬请关注。
2019-11-12 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - http上传云文件图片跨域的另一种解决方案
vue做个后台管理,用微信提供的http操作云储存上传文件会出现跨域问题,我只想搭个前端就能直接上传到云文件,不想再搭个后台中转。 换个思路,云函数是可以直接请求的,云函数里是可以直接上传图片到云文件的 由上可得 可以把图片数据传到云函数在云函数中上传图片 然而 测试发现云函数调用时有最大数据限制,大概十几kb就报错了,这肯定时不行的 但是 数据库新增是可以直接操作的, 来一波骚操作,先把图片数据转换成base64的字符串上传到云数据库,把_id传给云函数,让云函数从数据库中取出图片的数据,转换成 [代码]new Buffer(base64Data, 'base64') [代码] 哈哈哈哈哈哈哈哈哈哈哈,完美上传图片 然而事情并没有那么简单~~ 上传到云数据库时也有数据大小限制的 经过测试 base64字符串在500000字符长度是可以的(大概400kb) 在600000字符长度就会报错, 所以我们还要对上传的图片数据做分批上传 以上测试都没有错误截图了,只有已经可以完美上传的代码(请勿完全复制,根据你项目实际情况修改) [代码] export function add(token, datastr) { const params = { "env": envid, "query": "db.collection(\"testadd\").add({data:"+ datastr + "})", } // console.log("==params==" + JSON.stringify(params)) var url = 'api/tcb/databaseadd?access_token=' + token; return request({ url: url, method: 'POST', data: params }) } addObj({ commit },param) { return new Promise((resolve, reject) => { var accessToken=state.token if(accessToken==null){ accessToken=getToken() } add(accessToken,JSON.stringify(param)).then(response => { // // console.log('上传数据返回结果',response) resolve(response) if(response.errcode == 0){ // if(response.data[0] != null){ // resolve(JSON.parse(response.data[0])) // }else{ // resolve(null) // } } }).catch(error => { reject(error) }) }) }, //这是html中的 <input type="file" @change="handleChange" ref="fileInput1" accept="image/*"> //input上传文件的回调 handleChange(info) { const file = this.$refs.fileInput1.files[0] const fr = new FileReader() var self = this fr.onload = (e) => { try { console.log('file', fr) // self.imgData = fr.result self.imgData = {type:"Buffer", data: fr.result} } catch (error) { self.$message.error(`${file.name} 打开失败`); } } fr.readAsDataURL(file) info.target.value= "" }, //点击确定上传按钮回调 onModalOk(){ var imgData = self.imgData.data var splitCount = 500000 //长度大于一定时就要分断上传 // if(imgData.length > splitCount){ // imgData = imgData.substring(0, splitCount) // } var count = Math.ceil(imgData.length / splitCount) //要上传的次数 var hasAdd = 0 var idList = [] for (let index = 0; index < count; index++) { const subData = imgData.substring(splitCount * index, splitCount * (index + 1)); self.$store .dispatch("user/addObj", {name: 'testName.png', fileStream: subData, index: index}) .then(response => { hasAdd += 1 idList.push(response.id_list[0]) console.log('当前hasAdd', idList, hasAdd, count) if(hasAdd == count){ self.$store .dispatch("user/updateImgObj", {id: idList}) .then(response2 => { self.listLoading = false console.log('关闭返回的结果2', response2) }).catch(error => { self.listLoading = false self.$message.error('上传失败'); }) } }).catch(error => { self.listLoading = false self.$message.error('上传失败'); }) } } [代码] 然后是云函数中的代码: [代码] // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _ = db.command // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() var arr = [] for (var i = 0; i < event.id.length; i++){ var data = await db.collection("testadd").doc(event.id[i]).get() arr.push(data.data) } arr.sort(function(a, b){ return a.index - b.index }) var base64Data = '' for (var i = 0; i < arr.length; i++){ base64Data += arr[i].fileStream } // return data base64Data = base64Data.replace(/^data:image\/\w+;base64,/, ""); var result = await cloud.uploadFile({ cloudPath: arr[0].name, fileContent: new Buffer(base64Data, 'base64') //{ type: 'Buffer', data: data.fileStream}, }) // var result = {} // result.data = base64Data' //上传完成后删除数据库中的数据 for (var i = 0; i < event.id.length; i++) { await db.collection("testadd").doc(event.id[i]).remove() } return result } [代码] 研究了一天,上传图片这块微信的文档里只是简单说了上传格式,却没有例子,一次次尝试才测试出什么样的格式存到云函数中是有效的图片,分享给广大有需求的程序员,减少需要研究花费的时间
2019-12-06 - 同一页面存在多个video时,video无法正常播放一直在加载转圈
不建议同个页面使用多个video组件,建议不超过3个video,如果要实现video列表功能,请进行优化(image列表,选中时将image替换成video)
2019-08-29 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 云开发http上传文件代码
const request = require('request-promise') const config = require('./config'); const fs = require('fs') module.exports = async (ctx) => { const files = ctx.request.files //koa2后台接收到文件组,需要npm koa-body且multipart : true let file = files[0] try { let options = { uri: 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + config.appId +'&secret=' + config.appSecret + '', json:true } let {access_token} = await request(options) //得到access_token let fileName = `example.jpg` let filePath = `dir/${fileName}` options = { method: 'POST', uri: 'https://api.weixin.qq.com/tcb/uploadfile?access_token=' + access_token + '', body: { "env": 'cloud-a8eeXX', "path": filePath, }, json: true } let res = await request(options) //获得文件上传许可以及各种参数 options = { method: 'POST', uri: res.url, formData: { "Signature": res.authorization, "key": filePath, "x-cos-security-token": res.token, "x-cos-meta-fileid": res.cos_file_id, "file": { value: fs.createReadStream(file.path), options: { filename: fileName, contentType: file.type } } } } ctx.body = await request(options) //上传文件,cox.body返回结果给前端 } catch (err) { console.log(err.stack) } } [图片]
2020-10-20 - 基于小程序·云开发的HTTPAPI实现图片上传功能
基于HTTPAPI实现内容管理的图片上传功能 目前基于小程序·云开发可以快速的实现无服务器的小程序快速开发。但是小程序开发工具自带的管理工具,并不方便进行内容管理。因此为了更高效的管理小程序,需要开发后台管理工具,以方便在电脑端进行内容管理。基于此,会陆续连载更新基于小程序·云开发HTTPAPI进行后台管理工具相关开发的一些经验文章。 前端:elment-admin 后端:node.js koa 前端使用elment-ui el-upload组件实现上传功能 对于elment-admin以及elment-ui不熟悉的请自行去官网学习。 elment-admin是基于elment-ui组件构建的后台管理系统UI,功能强大,方便快速构建后台管理系统。 elment-admin:https://github.com/PanJiaChen/vue-element-admin elment-ui:https://element.eleme.io/#/zh-CN/guide/design [代码]<template> <div class="dashboard-container"> <div> <el-upload class="upload-demo" action="http://localhost:3000/upload/imgs" :before-upload="beforeUploadFile" > <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> </div> </div> </template> [代码] 后端基于node koa的后端图片上传功能代码: 学习技术的最好方法就是官方文档,因此要想熟练的使用httpapi构建小程序的后端管理系统,就必须先熟悉官方文档。 小程序官方文档链接:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html 要想使用Httpapi去实现图片上传功能,我们需要先获取访问请求中的参数access_token。access_token获取的官方文档内容: [图片] 通过官方文档可知,传递参数appid和secret,发送请求到文档中的地址,可以获取access_token数据。 access_token获取,存储以及定时更新的相关代码如下: [代码]const rp = require('request-promise') const fs = require('fs') //引用文件操作函数 const path = require('path') //引入文件路径函数 //获取文件的绝对路径 const filePath = path.resolve(__dirname, './access_token.json') //小程序的APPID和Secret const APPID = '小程序的APPID' const Secret = '小程序的Secret' //获取Token的URL const URL = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${Secret}` //从小程序端获取Token函数 const updateAccessToken = async () => { const resStr = await rp(URL) //将字符串类型转为对象 const res = JSON.parse(resStr) if (res.access_token) { //写入文件 fs.writeFileSync(filePath, JSON.stringify({ access_token: res.access_token, //从小程序服务器获取的token createTime: new Date() //获取token时,对应的时间 })) } else { //如果文件不存在,则重新调用获取token函数,重新执行写入 await updateAccessToken() } } //查询Token函数返回给响应的函数去调用云函数 const getAccessToken = async () => { try { //读取文件 const readRes = fs.readFileSync(filePath, 'utf8') const readObj = JSON.parse(readRes) //获取access_token.json中的createtime的时间 const createTime = new Date(readObj.createTime).getTime() //获取当前时间 const nowTime = new Date().getTime() //判断Token的失效是否大于2小时,如果大于2小时则需要重新获取Token,然后在读取Token数据 if ((nowTime - createTime) / 1000 / 60 / 60 >= 2) { await updateAccessToken() await getAccessToken() } //返回Token数据 return readObj.access_token } catch (error) { //如果access_token文件不存在,则读取报错,此时需要重新从服务器获取token数据,再读取token数据 await updateAccessToken() await getAccessToken() } } //定时更新Token setInterval(async () => { await updateAccessToken() }, (7200 - 300) * 1000) module.exports = getAccessToken; [代码] 获取文件上传链接 从官方文档可知,传递access_token和env参数,访问文档中的请求地址,获取上传文件所需的参数数据。 [图片] 从官方文档可知,返回的上传数据中包含如下内容: authorization字段 token字段 cos_file_id字段 url字段 上传文件到云存储。 按照官方文档,访问返回的url字段的链接,并传递相关参数,将图片上传到云存储。 [图片] 图片上传功能的代码如下: [代码]const rp = require('request-promise') const fs = require('fs') //引用文件操作函数 const getAccessToken = require('./getAccessToken') const cloudStorage = { // 1、请求地址 async upload(ctx){ console.log(1,ctx) const ACCESS_TOKEN = await getAccessToken() const file = ctx.request.files.file const path = `test/${Date.now()}-${Math.random()}-${file.name}` var options = { method: 'POST', uri: `https://api.weixin.qq.com/tcb/uploadfile?access_token=${ACCESS_TOKEN}`, body: { env: 'miniprogram-dev-y7a0a', path, }, json: true // Automatically stringifies the body to JSON }; //请求返回的参数 const info = await rp(options) .then(res => { // POST succeeded... return res }) .catch(err => { // POST failed... console.log(err) }); // 2、上传文件到云存储 const params ={ method: 'POST', uri:info.url, header:{ 'content-type':'multipart/form-data' }, formData:{ key:path, Signature:info.authorization, 'x-cos-security-token':info.token, 'x-cos-meta-fileid':info.cos_file_id, file:fs.createReadStream(file.path) }, json: true // Automatically stringifies the body to JSON } await rp(params) return info.file_id } } module.exports=cloudStorage; [代码] 后续会陆续更新相关技术文章,来帮助大家快速熟悉和使用小程序·云开发实现小程序的快速开发,以及基于HTTPAPI实现小程序后端管理系统的开发。
2019-10-11 - http api 上传文件到云存储报错?
目前做法是,客户端上传图片到服务器后,服务器通过 这里获取云存储上传链接, 再将文件内容上传到云存储。 目前可以确定 客户端到服务器这步已经没问题了, 文件已经在服务器上。 获取上传链接也没有问题, 最后一步 通过上传链接上传文件到云存储 总是报错。 [图片] 报错是: Error: Request failed with status code 400 这个问题困扰了很久,要崩溃了。。。。。。。。。
2019-09-16 - 云开发 HTTP API uploadFile上传文件时会被限制文件大小吗?
在使用云开发的 HTTP API 的时候遇到了个问题,就是上传的图片大小最大只能到 6.4KB 左右。在使用axios或者原生ajax上传的时候,小一点的就可以上传成功,但是大一点的就直接返回状态码 400 Bad Request 。在使用Element UI的上传组件的时候,超过6.4KB的图片也能上传,但是在云存储那里的文件大小只有6.4KB左右。 状态码为400时返回的错误描述: [代码]<[代码][代码]Code[代码][代码]>MalformedPOSTRequest</[代码][代码]Code[代码][代码]>[代码][代码]<[代码][代码]Message[代码][代码]>The body of your POST request is not well-formed multipart/form-data.</[代码][代码]Message[代码][代码]>[代码]使用uploadFile获取到了上传用的URL后进行上传的关键代码: [代码]var[代码] [代码]form = [代码][代码]new[代码] [代码]FormData();[代码][代码]form.append([代码][代码]'file'[代码][代码], file);[代码][代码]form.append([代码][代码]'key'[代码][代码], [代码][代码]this[代码][代码].uploadEnv.path);[代码][代码]form.append([代码][代码]'Signature'[代码][代码], res.authorization);[代码][代码]form.append([代码][代码]'x-cos-security-token'[代码][代码], res.token);[代码][代码]form.append([代码][代码]'x-cos-meta-fileid'[代码][代码], res.cos_file_id);[代码] [代码]axios.post(uploadUrl, form, {[代码][代码] [代码][代码]timeout: 60*1000,[代码][代码] [代码][代码]headers: {[代码][代码]"Content-Type"[代码][代码]: [代码][代码]"multipart/form-data;charset=UTF-8"[代码][代码]}[代码][代码]}).then((res)=>{[代码][代码] [代码] [代码]});[代码](uploadUrl是获取到的上传用的URL进行了代理处理后的url) 不知道是我的用法有问题还是在哪里不经意间被限制了大小?
2019-11-28 - 小程序端调用wx.cloud.uploadfile bug
- 当前 Bug 的表现(可附上截图) 在小程序端调用相同路径进行覆盖写时,并未进行覆盖写,而是使用之前写入的文件 - 预期表现 正常情况下 按照官网提供api,对相同路径进行上传,则会进行覆盖写操作,比如对于图片内容,应当是将流数据从文件开头进行写入,现在猜测的情况可能是对同一路径上传的时候生成了一个文件副本而错误的将文件指向了原文件,具体情况不知 - 复现路径 [代码]function[代码] [代码]ui(){[代码][代码] [代码][代码]return[代码] [代码]new[代码] [代码]Promise([代码][代码]function[代码][代码](resolve,reject){[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]i = 0; i < dbarr.length; i++) {[代码][代码] [代码][代码]const db = wx.cloud.database();[代码][代码] [代码][代码]var[代码] [代码]filePath = dbarr[i];[代码][代码] [代码][代码]var[代码] [代码]cloudPath = [代码][代码]'userimg/'[代码][代码]+bn.data.userid+[代码][代码]'/img'[代码][代码]+i+ [代码][代码]'.png'[代码][代码];[代码][代码]//这里是测试片段[代码][代码] [代码][代码]wx.cloud.uploadFile({[代码][代码] [代码][代码]cloudPath,[代码][代码] [代码][代码]filePath,[代码][代码] [代码][代码]success: res => {[代码][代码] [代码][代码]var[代码] [代码]h = res.fileID;[代码][代码] [代码][代码]update.push(h);[代码][代码] [代码][代码]// console.log('[上传文件] 成功:', res);[代码][代码] [代码][代码]if[代码][代码](update.length==dbarr.length){[代码][代码] [代码][代码]resolve(1);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: e => {[代码][代码] [代码][代码]console.error([代码][代码]'[上传文件] 失败:'[代码][代码], e)[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码],[代码][代码] [代码][代码]title: [代码][代码]'上传失败'[代码][代码],[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]complete: () => {[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]});[代码][代码]function[代码] [代码]ui(){[代码][代码] [代码][代码]return[代码] [代码]new[代码] [代码]Promise([代码][代码]function[代码][代码](resolve,reject){[代码][代码] [代码][代码]for[代码] [代码]([代码][代码]var[代码] [代码]i = 0; i < dbarr.length; i++) {[代码][代码] [代码][代码]const db = wx.cloud.database();[代码][代码] [代码][代码]var[代码] [代码]filePath = dbarr[i];[代码][代码] [代码][代码]var[代码] [代码]cloudPath = [代码][代码]'userimg/'[代码][代码]+bn.data.userid+[代码][代码]'/img'[代码][代码]+i+2+ [代码][代码]'.png'[代码][代码]; [代码][代码]//这里是测试片段[代码][代码] [代码][代码]wx.cloud.uploadFile({[代码][代码] [代码][代码]cloudPath,[代码][代码] [代码][代码]filePath,[代码][代码] [代码][代码]success: res => {[代码][代码] [代码][代码]var[代码] [代码]h = res.fileID;[代码][代码] [代码][代码]update.push(h);[代码][代码] [代码][代码]// console.log('[上传文件] 成功:', res);[代码][代码] [代码][代码]if[代码][代码](update.length==dbarr.length){[代码][代码] [代码][代码]resolve(1);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: e => {[代码][代码] [代码][代码]console.error([代码][代码]'[上传文件] 失败:'[代码][代码], e)[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]icon: [代码][代码]'none'[代码][代码],[代码][代码] [代码][代码]title: [代码][代码]'上传失败'[代码][代码],[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]complete: () => {[代码] [代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码]- 提供一个最简复现 Demo 可以将以上函数直接调用
2018-10-11