根据Uview官方文档所说，若是小程序和app项目，上传使用压缩图片，可以配置u-upload组件的sizeType属性为'compress'，但这对H5是无效的。在实际开发中，图片压缩上传经常是一个必要的需求，所以本篇文章主要讲的是在H5项目中如何使用Uview的upload组件进行压缩图片tool.js/** * 图片压缩 * imgSrc 地址 * scale 压缩质量 0-1 * type 文件类型 */ const compressImg = (imgSrc, scale, type, callback) => { // uni.$u.toast('压缩中') var img = new Image(); img.src = imgSrc; img.onload = function() { var that = this; var h = (img.height * scale).toFixed(0); // 默认按质量比例压缩 var w = (img.width * scale).toFixed(0); var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var width = document.createAttribute("width"); width.nodeValue = w; var height = document.createAttribute("height"); height.nodeValue = h; canvas.setAttributeNode(width); canvas.setAttributeNode(height); ctx.drawImage(that, 0, 0, w, h); var base64 = canvas.toDataURL('image/jpeg', scale); //压缩比例 canvas = null; if (type == 'base64') { let data = { size: getBase64Size(base64), type: type, source: base64 } callback(base64); } else { let blob = base64ToBlob(base64); console.log('压缩后的大小', blob.size); const blobUrl = window.URL.createObjectURL(blob); //blob地址 blob.source = blobUrl callback(blob); } } }; /**base转Blob */ const base64ToBlob = (base64) => { var arr = base64.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], { type: mime }); }; /**获取base64的文件大小 */ const getBase64Size = () => { let size = 0; if (base64Str) { // 获取base64图片byte大小 const equalIndex = base64Str.indexOf('='); // 获取=号下标 if (equalIndex > 0) { const str = base64Str.substring(0, equalIndex); // 去除=号 const strLength = str.length; const fileLength = strLength - (strLength / 8) * 2; // 真实的图片byte大小 size = Math.floor(fileLength); // 向下取整 } else { const strLength = base64Str.length; const fileLength = strLength - (strLength / 8) * 2; size = Math.floor(fileLength); // 向下取整 } } else { size = null; } return size };在upload-image.vue中使用/** 压缩图片*/ compressImg(source, compressionRatio) { let that = this; return new Promise((resolve, reject) => { that.$.compressImg(source.url, compressionRatio, source.type, compressRes => { resolve(compressRes); }) }).then((res) => { source.size = res.size // window.URL.revokeObjectURL(source.url) // 删除被压缩的缓存文件,这里注意，如果是相册选择上传，可能会删除相册的图片 source.url = res.source source.thumb = res.source return source }).catch(err => { console.log('图片压缩失败', err) }) },完整示例代码,以及我碰到的问题 <view class="actual-download"> <top-bar title="回单影像上传"></top-bar> <view class="main page-content"> <view class="scanCode" v-if="showScan"> <!-- <mumu-get-qrcode @close="showScan=false" @success='getScan' @error="err"></mumu-get-qrcode> --> <mumu-one-code @close="showScan=false" @success='getScan' definition :readers='["code_128_reader"]'></mumu-one-code> </view> <view class="main-top"> <u--input placeholder="请输入内容" border="surround" v-model="value"></u--input> <uni-icons @click="showScan=true" size="28" type="scan" color="#6BA8F8"></uni-icons> </view> <view> <u-upload :fileList="fileList1" @afterRead="afterRead" @delete="deletePic" name="1" multiple :maxCount="10"></u-upload> </view> </view> <bottom-bar rightBtnText="完成" @clickRight="rightBtnClick" :showLeftBtn="false"></bottom-bar> </view> 问题是大于2M后的图片上传后,服务器识别不了文件类型.一开始我在uni.uploadFile使用的是filePath,后面改成大于2M使用file上传,优化了下tools.js的代码.返回多了个fileconst compressImg = (imgSrc, scale, type,fileName,callback) => { // uni.$u.toast('压缩中') var img = new Image(); img.src = imgSrc; img.onload = function() { var that = this; var h = (img.height * scale).toFixed(0); // 默认按质量比例压缩 var w = (img.width * scale).toFixed(0); var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var width = document.createAttribute("width"); width.nodeValue = w; var height = document.createAttribute("height"); height.nodeValue = h; canvas.setAttributeNode(width); canvas.setAttributeNode(height); ctx.drawImage(that, 0, 0, w, h); var base64 = canvas.toDataURL('image/jpeg', scale); //压缩比例 canvas = null; if (type == 'base64') { let data = { size: getBase64Size(base64), type: type, source: base64 } callback(base64); } else { let blob = base64ToBlob(base64); console.log('压缩后的大小', blob.size); const blobUrl = window.URL.createObjectURL(blob); //blob地址 blob.source = blobUrl const file = new File([blob], fileName, { type }); callback({ blob, file }) } } };
引用地址 HBuilder X插件市场 何木木 <view class="canvasBox"> <view class="box"> <view class="line"></view> <view class="angle"></view> </view> <view class="box2" v-if="isUseTorch"> <view class="track" @click="openTrack"> <path d="M651.353043 550.479503H378.752795L240.862609 364.315031c-3.688944-4.897391-5.660621-10.876025-5.660621-17.045466v-60.040745c0-15.773416 12.847702-28.621118 28.621118-28.621118h502.459627c15.773416 0 28.621118 12.847702 28.621118 28.621118v59.977143c0 6.105839-1.971677 12.084472-5.660621 17.045466l-137.890187 186.228074zM378.752795 598.308571v398.024348c0 15.328199 12.402484 27.667081 27.667081 27.667081h217.266087c15.328199 0 27.667081-12.402484 27.66708-27.667081V598.308571H378.752795z m136.300124 176.942112c-14.564969 0-26.331429-11.76646-26.331428-26.331428v-81.283975c0-14.564969 11.76646-26.331429 26.331428-26.331429 14.564969 0 26.331429 11.76646 26.331429 26.331429v81.283975c0 14.564969-11.76646 26.331429-26.331429 26.331428zM512 222.608696c-17.554286 0-31.801242-14.246957-31.801242-31.801243V31.801242c0-17.554286 14.246957-31.801242 31.801242-31.801242s31.801242 14.246957 31.801242 31.801242v159.006211c0 17.554286-14.246957 31.801242-31.801242 31.801243zM280.932174 205.881242c-9.47677 0-18.889938-4.197764-25.122981-12.275279L158.242981 67.991056a31.864845 31.864845 0 0 1 5.597019-44.648944 31.864845 31.864845 0 0 1 44.648944 5.597018l97.502609 125.551305a31.864845 31.864845 0 0 1-5.597019 44.648944c-5.787826 4.579379-12.656894 6.741863-19.46236 6.741863zM723.987081 205.881242c-6.805466 0-13.674534-2.162484-19.462361-6.678261a31.794882 31.794882 0 0 1-5.597018-44.648944l97.566211-125.551304a31.794882 31.794882 0 0 1 44.648944-5.597019 31.794882 31.794882 0 0 1 5.597019 44.648944l-97.566211 125.551305c-6.360248 8.077516-15.709814 12.27528-25.186584 12.275279z" fill="#ffffff" p-id="1352" ></path> {{ trackStatus ? '关闭闪光灯' : '打开闪光灯' }} </view> </view> <view class="mask1 mask" :style="'height:' + maskHeight + 'px;'"></view> <view class="mask2 mask" :style="'width:' + maskWidth + 'px;top:' + maskHeight + 'px;height:' + canvasHeight + 'px'" ></view> <view class="mask3 mask" :style="'height:' + maskHeight + 'px;'"></view> <view class="mask4 mask" :style="'width:' + maskWidth + 'px;top:' + maskHeight + 'px;height:' + canvasHeight + 'px'" ></view> <slot name="error"> <view class="error"> <view class="on1">相机权限被拒绝，请尝试如下操作：</view> <view>· 刷新页面后重试；</view> <view>· 在系统中检测当前App或浏览器的相机权限是否被禁用；</view> <view>· 如果依然不能体验，建议在微信中打开链接；</view> </view> </slot> </view>
一直困惑在uni-app中实现pda的广播扫码功能，今天终于找到了一个解决方案。方案就是做一个全局监听，只引入一次，来防止很多页面都引入了扫码的组件，造成多次扫描，数据出现多次知识点 全局监听：官网的全局监听地址 uni.$emit：触发全局的自定事件。附加参数都会传给监听器回调。uni.$on：监听全局的自定义事件。事件可以由 uni.$emit 触发，回调函数会接收所有传入事件触发函数的额外参数uni.$off：移除全局自定义事件监听器。一定要记得移除全局事件，不燃就会出现多次扫描一：在你的pda中要是设置广播输出：模式是键盘方式输出方法：设置---扫描---Default：把里面的键盘方式输出取消掉，就会看到下面的广播动作和广播数据标签（这两个很重要，在后面会用到）上图:二：在你的uni-app项目里 新建一个公共组件 就叫 scan-code 吧，放到公共组件components中，目录：components/scan-code/scan-code.vue这里就用到了广播动作和广播数据标签filter.addAction("android.intent.ACTION_DECODE_DATA")intent.getStringExtra("barcode_string")代码里的filter.addAction里换成你的广播动作，intent.getStringExtra(里换成你的广播标签代码如下 <view class="content"></view> 三：页面引用，只引用一次就好，我是index.vue 引用的，我的app的其他功能都是在这个页面链接走的直接上代码：切记：首页往各个子页面跳转的时候 移除监听事件uni.$off('scancodedate')，要不首页的监听会一直存在，就会出现首页这个扫码uni.navigateTo({ 之前、之前、之前 uni.$off('scancodedate')import scanCode from '@/components/scan-code/index.vue' export default { components: { MenuList, scanCode, }, data() { return { } }, onShow() { uni.$off('scancodedate') // 每次进来先 移除全局自定义事件监听器 uni.$on('scancodedate', function(data) { console.log(data.code) }) }, onUnload() { // 移除监听事件 uni.$off('scancodedate') },其他的页面引用方法：不需要再次引入scanCode因为其他的页面都是从首页跳转过来的，所以其他的页面需要onUnload() {// 移除监听事件uni.$off('scancodedate')}代码如下:onLoad() { var _this = this uni.$on('scancodedate',function(data){ // _this 这里面的方法用这个 _this.code(data.code) console.log('你想要的code：', data.code) }) }, onUnload() { // 移除监听事件 uni.$off('scancodedate') } 如果这个页面还有详细页面需要跳转，切记 uni.navigateTo({ 之前、之前、之前 uni.$off('scancodedate')到此所有的代码就完事了。重点就是全局监听，真机测试的时候在控制台可以打印出你的code和routes，看下是否每次的扫描只是执行的本页面（路由）下的程序。
vue-calendar-component日历实现多选、回显、取消在进入正题之前，先说一下vue-calendar-component日历插件，需要详细了解使用方式的去百度，我主要说一下针对我的需求对这个插件的一些修改，插件原有的功能是多色标记，标记数组，这跟多选还是有区别的，不能满足我的日历需求（我的需求是点击该日期将该日期插入标记数组中并改变成数组中元素的样式，再次重复点击将该日期移出数组并移除数组样式，另外可以实现提交、回显、重置）。效果图红色标记表示当天，这个随自己，蓝色为标记的数组元素。2、功能（说点我的建议，下载这个插件后，假如跟我一样没有出现在你的node modules中，而是在package-lock.json文件中出现线上链接，由于需要改源码，建议你将文件下载下来放在项目中引入你的组件中方便修改）多选：利用插件中的标记数组，将点击到的日期插入到数组中，数组中的元素样式与点击日期样式修改为相同样式，根据点击日期插入数组改变样式，重复点击移除数据及其样式来实现多选效果，这里的重复点击改变样式要修改的源文件为calendar.vue,这一步是移除了点击日期的固定样式。在点击日期的事件中进行判断，如果选中了这个日期就将样式赋给它，否则无样式 。在左右切换月份事件以及create（一进入页面求取的是当年当月，所以我为当前月份单独写了一个事件）中分别通过传入当前月份和年份中调用接口获取标记数组数据，以此实现回显。另外，在切换月份这个事件中，首先清空一下标记数组再重新获取数组，不然会出现月份数据重叠。我这边的需求是将选中的数据提交，选中的数据会出现重复，提交前我需要做一下去重，这个我就不说了，就上面这样。取消点击取消恢复初始数据，直接将当前标记数组中的年份月份作为参数获取后端数据进行渲染。暂时就这么多了，需要补充了后面再加。我做的是全选 反选 工作日 以及周六周日 组件内容如下 <Calendar ref="Calendar" :markDate="selectDate" v-on:choseDay="clickDay" v-on:changeMonth="changeDate" />
Web UI库ElementUI Plus：面向设计师和开发者的组件库Ant Design of Vue：Ant Design 的 Vue 实现，开发和服务于企业级后台产品BalmUI：基于谷歌的 Material Design设计的UI 组件库Naive UI：基于 Vue 3.0/TypeScript 的免费开源前端 UI 组件库arco.design：字节跳动企业级产品设计系统，支持React和Vue双版本Quasar：一个完整的、以性能为中心的框架移动UI库Vant：移动端组件库NutUI：京东风格的轻量级移动端 Vue 组件库Varlet：Material 风格移动端组件库nutui-bingo：京东基于 NutUI 的抽奖组件库2、Vue3实用开发工具在之前的文章《7款值得收藏的Vue3实用开发工具（分享）》中，我们介绍了7个Vue3实用开发工具：unplugin-vue-components：按需组件自动导入插件vuex-persistedstate：Vuex持久化插件vuex-persist：支持 Typescript 的Vuex持久化插件@vueuse/gesture：手势库，使应用程序具有交互性unplugin-auto-importpinia-plugin-persistedstate：Pinia 数据持久化储存vue-termui.dev：一个基于 Vue.js 的终端 UI 框架3、Vue3可视化工具在科技的频频更新下，可视化开发工具成为了提升开发效率的有效工具。在之前的文章《【整理总结】4个Vue3可视化工具》中，我们介绍了4个Vue3可视化工具：Pdfvuer：用于Vue应用中的一个PDF查看器vue3-marquee：无缝滚动组件Vue-ECharts：百度ECharts 的 Vue.js 组件iconpark：一个通过技术驱动矢量图标样式的开源图标库4、Vue3常用插件在之前的文章《10个提高开发效率的Vue3常用插件（快来收藏）》中，我们了解了10个有助于提高效率的vue插件：vue-multiselect-next：Vue的通用选择/多选/标记组件vue-print-nb：指令包装器vue-i18n-next：Vue3的国际化插件vue-cropper：vue图片裁剪插件Vue Grid Layout：Vue的网格布局系统Vue Qrcode Reader：一组用于检测和解码QR(二维码)码的Vue.js组件Makeit Captcha：滑块验证码插件vue3-clipboard：基于Vue3的粘贴复制插件vue.draggable 基于Sortable.js的Vue 3拖放组件BetterScroll解决移动端（已支持 PC）各种滚动场景需求的插件5、Vue3相关生态在之前的文章《6个实用的Vue3相关生态（总结分享）》中，我们了解了6个Vue3相关生态：Vue插件库（vue365.cn）Pinia：轻量级状态管理库Nuxt ModulesNuxt 3：可用来创建服务端渲染 (SSR) 应用vuepress：Vue驱动的静态网站生成器VueUse：基本 Vue 组合实用函数的集合6、Vue3动画插件1）vue-starport带有动画的跨路由共享组件github地址：https://github.com/antfu/vue-starport/blob/main/README.zh-Hans.md演示地址：https://vue-starport.netlify.app/2）@vueuse/motionVue Composables 让你的组件动起来官方地址：https://motion.vueuse.org/github地址：https://github.com/vueuse/motion7、音视频组件@vueuse/sound用于播放音效的 Vue 组合官方地址：https://sound.vueuse.org/github地址：https://github.com/VueUse/sound
安装scp2npm i scp2 --save-dev 新建文件upload.js与package.json同级 let client = require('scp2'); const ora = require('ora'); const chalk = require('chalk'); const spinner = ora(chalk.green('正在发布到服务器...')); spinner.start(); client.scp('./dist/', { // 本地打包后，项目包的相对路径，默认为./dist "host": '106.12.123.173', // 云服务器的IP地址 "port": '22', // 云服务器端口， 一般为22 "username": 'root', // 云服务器用户名，linux系统一般为root "password": '******', // 云服务器密码，若忘记了，可以去云服务器主页重置密码 "path": '/root/itNote' // 项目包上传到云服务器的目标位置，根据nginx配置决定 }, err =>{ spinner.stop(); if (!err) { console.log("项目发布完毕!") } else { console.log("err", err) } })添加自动部署命令在package.json的scripts的末尾添加 "upload": "node upload.js", "pub": "npm run build && npm run upload"4.运行命令，自动打包部署npm run pub 5.控制台输出 “ 项目发布完毕! ” 时，即部署成功
base64 ---> file方式一function base64toFile(base64Data, filename) { var arr = base64Data.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([u8arr], filename, { type: mime }); } 方式二（兼容性好） //将base64转换为blob function base64toBlob(base64Data) { var arr = base64Data.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], { type: mime }); }; //将blob转换为file function blobToFile(theBlob, fileName) { theBlob.lastModifiedDate = new Date(); theBlob.name = fileName; return theBlob; }; // 使用 let blob = base64toBlob(base64Data); let file = blobToFile(blob, fileName);提示（1） btoa（） 方法用于创建一个base64编码的字符串。该方法使用 "A-Z", "a-z", "0-9", "+", "/" 和 "=" 字符来编码字符串。 语法：window.btoa(str) str 必需项，要编码的字符串 返回值：返回一个base64编码的字符串可用Buffer.from(str).toString('base64')替代（2） atob() 方法用于解码使用 base-64 编码的字符串。 语法：window.atob(encodedStr) encodedStr 必需项，一个通过btoa（）方法编码的字符串 返回值：返回一个解码的字符串可用Buffer.from(b64Encoded, 'base64').toString('binary')替代提示Uint8Array数组类型表示一个8位无符号整型数组，创建时内容被初始化为0，创建完成后，可以以对象的方式或使用数组下标索引的方式引用数组中的元素。 语法格式：new Uint8Array()//创建初始化为0的，包含length个元素的无符号整型数组new Uint8Array(length)图片URL ---> Base64function imgURLToBase64(url) { return new Promise((resolve, reject) => { let canvas = document.createElement('canvas') let context = canvas.getContext('2d') let img = new Image() //通过构造函数绘制图片实例 img.crossOrigin = 'Anonymous' //处理图片跨域问题，服务器也需允许跨域 img.src = url img.onload = function() { //该加载过程为异步事件，请先确保获取完整图片 canvas.width = img.width canvas.height = img.height context.drawImage(img, 0, 0) //将图片绘制在canvas中 const URLData = canvas.toDataURL('image/png') resolve(URLData) canvas = null } img.onerror = function() { reject('图片加载失败'); } }) } Blob（File） ---> Base64/** * Blob转Base64 * @param base64 String base64格式字符串 * 返回值为含有转换结果e.target.result的Promise对象 */ function blobToBase64(blob) { return new Promise((resolve, reject) => { var reader = new FileReader() reader.readAsDataURL(blob) //读取后，result属性中将包含一个data:URL格式的Base64字符串用来表示所读取的文件 reader.onload = function(e) { resolve(e.target.result) } reader.onerror = function() { reject('文件读取失败'); } }) }
这篇文章主要为大家详细介绍了微信小程序自定义顶部导航组件，文中示例代码介绍的非常详细，具有一定的参考价值，感兴趣的小伙伴们可以参考一下本文实例为大家分享了微信小程序自定义顶部导航组件，供大家参考，具体内容如下在components中新建文件夹navbarcomponents/navbar.wxml <view class="navbar" style="height:{{navHeight+5}}px;background-image:url('{{imgUrl}}') "> <view class="navbar_left" style="top:{{navTop}}px;height:{{jnheight-1}}px;width:{{jnwidth-3}}px" wx:if="{{showNav}}"> <view class="navBack" bindtap="navBack"> <image src="/images/back.png" mode="widthFix"></image> </view> <view class="nav_line" bindtap="navHome" wx:if="{{showHome}}"> <image src="/images/index.png" mode="widthFix" style="width:50%"></image> </view> </view> <view class="navbar_title" style="top:{{navTop}}px;">{{pageName}}</view> </view>components/navbar.jsconst App = getApp(); Component({ // 组件的属性列表 properties: { pageName: String, //中间的title showNav: { //判断是否显示左上角的按钮 type: Boolean, value: true }, showHome: { //判断是否显示左上角的home按钮 type: Boolean, value: true }, imgUrl:{//图片背景路径 type: String, value: App.globalData.imgLink+'index.jpg', }, }, // 组件的初始数据 data: { showNav: true, //判断是否显示左上角的home按钮 showHome: true, //判断是否显示左上角的按钮 }, lifetimes: { // 生命周期函数，可以为函数，或一个在methods段中定义的方法名 attached: function() { this.setData({ navHeight: App.globalData.navHeight, //导航栏高度 navTop: App.globalData.navTop, //胶囊按钮与顶部的距离 jnheight: App.globalData.jnheight, //胶囊高度 jnwidth: App.globalData.jnwidth //胶囊宽度 }) } }, // 组件的方法列表 methods: { //回退 navBack: function() { wx.navigateBack() }, //回主页 navHome: function() { wx.switchTab({ url: '/pages/index/index' }) }, } })components/navbar.wxss/* components/navbar.wxss */ .navbar { width: 100%; overflow: hidden; position: sticky; top: 0; left: 0; z-index: 10; flex-shrink: 0; background: #fff; } .navbar_left { display: -webkit-flex; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; align-items: center; position: absolute; left: 10px; z-index: 11; line-height: 1; border: 1rpx solid #f0f0f0; border-radius: 40rpx; overflow: hidden; background: rgba(255, 255, 255, 0.6); } .navBack image{ width: 60%; } .navbar_left view { width: 50%; display: flex; align-items: center; justify-content: center; } .nav_line { border-left: 1rpx solid #f0f0f0; } .navbar_title { width: 100%; box-sizing: border-box; padding-left: 115px; padding-right: 115px; height: 32px; line-height: 32px; text-align: center; position: absolute; left: 0; z-index: 10; color: #333; font-size: 16px; font-weight: bold; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }在公共组件app.js中获取导航高度等信息// app.js App({ onLaunch() { //设置导航栏 //获取菜单按钮的布局位置信息 let menuButtonObject = wx.getMenuButtonBoundingClientRect(); //获取系统信息 wx.getSystemInfo({ success: res => { console.log('xxxx', res) //状态栏的高度 let statusBarHeight = res.statusBarHeight, //胶囊按钮与顶部的距离 navTop = menuButtonObject.top, navHeight = statusBarHeight + menuButtonObject.height + (menuButtonObject.top - statusBarHeight) * 2; let globalData = this.globalData; globalData.navHeight = navHeight;//导航栏高度 globalData.navTop = navTop;//胶囊按钮与顶部的距离 globalData.jnheight = menuButtonObject.height;//胶囊的高度 globalData.jnwidth = menuButtonObject.width;//胶囊的宽度 globalData.screenHeight = res.screenHeight;//屏幕高度 }, fail(err) { console.log(err); } }); }, //设置全局对象 globalData: { navHeight: 0, navTop: 0, jnheight: 0, jnwidth: 0, screenHeight: 0, } }) 在app.json中设置导航自定义，去除默认的导航 “navigationStyle”: "custom" "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black", "navigationStyle": "custom" },引用到pages中的文件里json文件中引用组件**{ "usingComponents":{ "navbar":"/components/navbar/navbar" } }html文件中<navbar page-name="{{navbar.shoppingName}}"></navbar> js文件中Page({ data:{ navbar:{ shoppingName:'首页', }, }
解决：v-model cannot be used on v-for or v-slot scope variables because they are not writable.报错问题
在使用vue进行前端开发时，可能会遇到循环渲染input输入框的需求，当使用v-for循环后，对v-model进行值的绑定时，可能会出现以下错误,如图所示：v-model cannot be used on v-for or v-slot scope variables because they are not writable.错误代码： <el-col :span="24"> <el-form-item label="上传新的事项材料:"> (您上传的材料将直接影响项目画像、项目服务、数据统计等，请慎重) </el-form-item> <el-form-item label="材料审批时间:" v-for="(item, index) in projectApproveEvent.certTempList" :key="index"> <el-date-picker v-model="item.approveTime" type="date" placeholder="选择日期" class="m-2" value-format="YYYY-MM-DD" /> <uploadmaterials v-model="item" @deletematerial="deletematerial(index)" /> </el-form-item> </el-col>通过查阅文档发现，v-model 不可以直接修改 v-for 循环迭代时别名上的数据我们可以使用对象的索引来进行v-model的值的绑定。 <el-col :span="24"> <el-form-item label="上传新的事项材料:"> (您上传的材料将直接影响项目画像、项目服务、数据统计等，请慎重) </el-form-item> <el-form-item label="材料审批时间:" v-for="(item, index) in projectApproveEvent.certTempList" :key="index"> <el-date-picker v-model="item.approveTime" type="date" placeholder="选择日期" class="m-2" value-format="YYYY-MM-DD" /> <uploadmaterials v-model="projectApproveEvent.certTempList![index]" @deletematerial="deletematerial(index)" /> </el-form-item> </el-col>通过以上的方法就可以完美解决了。
1.判断设备/** * UA parser 设备解析器 */ export const uaParser = () => { const { userAgent } = window.navigator const MobileReg = /(ipod|ipad|iphone|android|coolpad|mmp|smartphone|midp|wap|xoom|symbian|j2me|blackberry|wince)/i const isMobile = userAgent.toLowerCase().match(MobileReg) !== null const isAndroid = userAgent.indexOf('Android') > -1 || userAgent.indexOf('Linux') > -1 // g const isIOS = Boolean(userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) // ios终端 const isWechat = userAgent.toLowerCase().indexOf('micromessenger') > -1 return { isMobile, isAndroid, isIOS, isWechat, } } 2.浏览器存储方式/** * Storage方法 * 策略是 cookie的方式，如果项目上禁用cookie，则这里可以修改为其他本地存储 Local */ /** * window.sessionStorage 浏览器临时缓存 * @method set 设置临时缓存 * @method get 获取临时缓存 * @method remove 移除临时缓存 * @method clear 移除全部临时缓存 */ export const Storage = { set: (key: string, value: string, time?: number): void => { const cur = new Date() const day = time ? time : 24 cur.setTime(cur.getTime() + day * 3600000) document.cookie = `${key}=${encodeURIComponent(value)};expires=${time ? cur.toUTCString() : ''}}` }, get: (key: string): string => { const data = document.cookie let startIndex = data.indexOf(key + '=') if (startIndex > -1) { startIndex = startIndex + key.length + 1 let endIndex = data.indexOf(';', startIndex) endIndex = endIndex < 0 ? data.length : endIndex return decodeURIComponent(data.substring(startIndex, endIndex)) } return '' }, remove: (key: string): void => { document.cookie = `${key}=${encodeURIComponent('')};expires=${-1}}` }, clear: () => { document.cookie = '' }, } /** * window.localStorage 浏览器永久缓存 * @method set 设置永久缓存 * @method get 获取永久缓存 * @method remove 移除永久缓存 * @method clear 移除全部永久缓存 */ export const Local = { // 设置永久缓存 set(key: string, val: any) { window.localStorage.setItem(key, JSON.stringify(val)) }, // 获取永久缓存 get(key: string) { const json: any = window.localStorage.getItem(key) return JSON.parse(json) }, // 移除永久缓存 remove(key: string) { window.localStorage.removeItem(key) }, // 移除全部永久缓存 clear() { window.localStorage.clear() }, } 3.页面添加水印const id = '1.23452384164.123412416' // 页面添加水印效果 const setWatermark = (str: string) => { try { const wDom = document.getElementById(id) if (!str && wDom) { document.body.removeChild(<HTMLElement>document.getElementById(id)) return } if (document.getElementById(id) !== null) document.body.removeChild(<HTMLElement>document.getElementById(id)) const can = document.createElement('canvas') can.width = 200 can.height = 130 const cans: any = can.getContext('2d') cans.rotate((-20 * Math.PI) / 180) cans.font = '16px Vedana' cans.fillStyle = 'rgba(200, 200, 200, 0.30)' cans.textBaseline = 'Middle' cans.fillText(str, can.width / 10, can.height / 2) const div = document.createElement('div') div.id = id div.style.pointerEvents = 'none' div.style.top = '15px' div.style.left = '0px' div.style.position = 'fixed' div.style.zIndex = '10000000' div.style.width = '100vw' div.style.height = '100vh' div.style.background = `url(${can.toDataURL('image/png')}) left top repeat` document.body.appendChild(div) return id } catch (error) { console.warn('Watermark Setting Error：', error) } } /** * 页面添加水印效果 * @method set 设置水印 * @method del 删除水印 */ const watermark = { // 设置水印 set: (str: string) => { return setWatermark(str) }, // 删除水印 del: () => { if (document.getElementById(id) !== null) document.body.removeChild(<HTMLElement>document.getElementById(id)) }, } // 导出方法 export default watermark