- 小程序JS也能做图像处理 - 会员卡主题色提取的方案解析
一、前言 主题色,简而言之,是界面设计中被用作基础配色方案的主要颜色。它是在设计中明确定义的一种核心色彩,用于塑造界面的整体氛围和风格。通过恰当选择和运用主题色,可以 提升界面的可视性和易用性,从而大幅地提升用户体验。 提到图像提取主题色,⼤家可能会想到 OpenCV 这种专业的计算机视觉库,其实Javascript 这种轻量级的 脚本语⾔也是可以被⽤来实现该功能。并且相较于其他语⾔,由于其实时性和动态性以及浏览器的原生支持,JS 具有更好的动态响应能⼒以及定制化能力。 二、背景 目前微盟 CRM 会员模块主要支持两种类型的会员卡,分别是等级会员卡以及权益会员卡。商户可以根据不同的应用场景去创建不同的会员方案(成长值等级类型、消费行为等级类型、权益卡以及付费会员),以及对它们配置、管理。满足丰富的业务场景,能帮助商家针对行业对会员进行个性化运营,帮助商家给会员提供更好的服务,提升会员的黏性和忠诚度。 [图片] 并且每⼀套等级/权益卡的卡面外观,包含了勋章图案、卡面背景、自定义图片、⽂字颜色和背景配色,商户可以对其进行自定义搭配用以塑造界面的整体氛围和风格,提升整体的视觉体验。 [图片] 等级卡装修页面 [图片] 等级卡背景色设置 [图片] 权益卡设置 当商家配置好背景/卡面图片后,其需要手动配置背景/卡面主题色,可能需要进行多次尝试才能找到最合适的配色方案。这无疑增加了配置成本。 此外,可供选择的主题色为固定的几个色板,这导致在设计页面时受到一定的局限性。为了解决这一问题,我们考虑在商户配置好图片后自动生成与其图片相匹配的主题色并自动应用,从而提升整体观感。 三、需求分析 解决该问题最重要的便是获取图片的主题色啦,那么我们该如何去获取到一张图片的主题色呢?市场上常用的提取主题色的方案主要为以下几种: 1.基于平均色的方案:这种方法计算图像中所有像素的RGB值的平均值,然后将这个平均值作为主题色。这种方法简单直接,但可能无法捕捉到图像中的主要色调。 2.颜色直方图:将图像的颜色分布表示为颜色直方图,然后选择直方图中的峰值作为主题色。这种方法对于多种颜色的图像效果较好,但总体效果较差 3.Opencv: 使用 OpenCV 这个强大的计算机视觉库来加载图像,然后使用颜色空间转换和阈值处理来提取特定颜色的部分。 但其实 Javascript 也可以被用来实现该功能,并且相较于其他方案,Js 处理的好处如下: 1.客户端控制:无需后端服务器支持,不用处理图像上传/下载等逻辑。 2.响应性:在处理尺寸较小的图片(CRM会员卡背景图片限制1M以内)时,具有更好的响应性。 3.交互性:可以直接在前端图像上实现交互,例如用户点击图片上的区域来提取特定颜色,或者通过滑块来调整颜色提取的敏感度。 四、需求分析 简而言之,我们的技术方案主要为以下几个步骤: 1.加载图像数据: 使用 Canvas 绘制图像并获取像素数据。 2.数据预处理: 对像素数据进行降噪/缩放/去皮等定制化处理。 3.生成主题色:采用切割算法(中位切分/八叉树)进行颜色提取。 [图片] 接下去我们会一步一步讲解具体的实现步骤。 1、加载图像数据 [图片] 前端开发者对于 Canvas 这个元素应该是再熟悉不过了,我们通过 Canvas 的像素渲染的特性,很轻松的就能获取到图片上的像素数据。但需要注意的是需要使用 IE8及之后版本哦。 首先我们需要通过创建一个 Canvas 元素并调用 Canvas 上下文的 DrawImage 方法将图片绘制到 Canvas 上,再通过 GetImageData 方法获取到图像数据,即ImageData 对象。 并且我们使用了 Web Workers 来对 Canvas 进行离屏渲染,使其在后台线程中处理图像,防止其阻塞主线程的执行。 现在我们已经获取到所有像素数据了,接下来我们便需要对其进行预处理,为我们之后的计算打好基础。 2、数据预处理 2.1 降噪 [图片] 这里的降噪不是指声音的降噪,而是图像处理中的一个专业术语,简单来说是为了去除图像中的干扰。常见的图像降噪处理有高斯滤波、均值滤波、中值滤波等方式。 我们使用高斯滤波(Gaussian Filter)来解释其工作原理,滤波器本质是一种对图像的卷积(image convolutions)。 什么?你说你不知道啥是卷积? 那你知道图像的模糊和锐化吧?那就是一个卷积。 用过P图软件的魔术棒抠图吗?没错那也是卷积。 用手机自拍开过美颜加过滤镜吗?恭喜你!这还是卷积。 卷积无处不在,而且也很好理解,用数学上的术语来表示卷积是对两个相同维度的矩阵进行逐元素相乘并求和。 如果把图片当做一个大矩阵,那么对图片进行模糊、锐化、边缘检测等处理的功能就是一个小矩阵我们称之为核(kernel),通常来说核的大小是MxM(M为奇数)如3x3,这样可以确保其有一个核心坐标(x, y)。 接着把核心坐标放在图像的起始坐标上,将他们重叠区域的元素进行逐个相乘后求和并将计算结果输出到和当前核心坐标相同的图像坐标上,按从左往右从上往下的顺序平滑核心坐标重复此步骤。 [图片] 弄明白卷积后我们回过头来讲高斯滤波就容易多了,万变不离其宗,高斯滤波其实就是创造一个符合高斯分布的卷积核并对图像进行卷积计算(对像素的R、G、B值分别计算得出结果),详细步骤不在这里过多阐述,来看下具体的运行效果。 [图片] 经过高斯滤波处理之后,图片看起来变模糊了。这就是像素被平滑处理之后的效果,为了减少之后计算的复杂度。 2.2 去皮 [图片] 经过降噪处理后,我们还需要剔除一部分边界颜色我们称之为去皮。因为有些配图是带有纯色背景色的比如白色,大面积的白色或黑色会影响统计结果,并且造成统计性能的下降。 在实际运行过程中会发现有些看似是白色的背景实际会有一定的色差。因此我们并不是简单的对颜色值为(255, 255, 255)的像素去除,而是选取一定的范围。这个范围不能过大,且尽量贴近黑、白色的边界。 如果我们选取RGB值小于(5, 5, 5)或大于(250, 250, 250)的像素进行去除的话,代码实现如下: function convertToPixelsArray(imgData) { const data = imgData.data; const pixels = []; const [min, max] = [5, 250]; //像素点RGB值不在此范围内的进行过滤 范围可选 for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; // rgb值都在[5, 250]范围内,则不过滤 if (!(Math.min(r, g, b) >= max || Math.max(r, g, b) <= min)) { // 过滤透明度(可选) pixels.push([data[i], data[i + 1], data[i + 2]]); } } return pixels; } 3、生成主题色 在我们的项目中,我们主要采用了两种算法:中位切分法和八叉树。这些算法被用于不同的情境,以满足不同的图像处理需求。 对于那些需要快速获取主题色,并且对图像细节不太关心的场景,我们选择了中位切分法。这个简单且高效的算法能够帮助用户快速批量地获取图片的主题色。在这种情况下,我们的重点在于迅速处理大量图像,而不需要过多关注图像的微小差异。 在那些对细致和真实主题色有更高要求的场景中,八叉树无疑是更好的选择。比如: 渐变背景色:八叉树能够创造出更加复杂多样的背景效果,从而使渐变过程更加平滑自然,为视觉体验增添层次感。 细致的颜色匹配:八叉树具备实现对图像中不同区域进行精细颜色匹配的能力,从而更好地将主题色与整体设计融合。 下面就来介绍一下怎么用 JS 实现这两种算法。 4.1 中位切分法 中位切分其实非常好理解。首先需要各位读者发挥下空间想象能力,将R、G、B想象成三维空间的X、Y、Z轴,这样我们就得到了一个边长为256的立方体盒子,接着把每个像素点当作一个无体积的小球根据其坐标(r,g,b)放入盒子中。 [图片] 中位切分法的核心思想就是不断对某一根轴上的中位数进行切割,与日常生活中的折纸类似(把一张纸对折42次就可以上月球了,由此可见该算法的强大)。 不同的是,我们每次会选择像素密度最大子空间的最长边上的中位数进行切割,以确保每次切割过后的子空间中的像素数量大致相等。并在完成每次切分后使用插值排序法进行排序,从而找出密度最大子空间(或质量最大子空间)以进行下一次切割。 [图片] 某图像颜色在三维空间中的分布示意,可以看到部分区域的像素点非常密集 [图片] [图片] 沿R轴像素中位数处切割成左右2个子空间,右边空间包含的像素数量更多 当子空间边长小于设定的阀值时,我们便将其存入结果数组中。算法终止条件为切割次数或结果数组达到要求,此时密度排序靠前的几个子空间中包含的颜色即为我们所寻找的主题色。 以下是 JavaScript 的实现: function medianCut(pixels) { const colorRange = getColorRange(pixels);// 获取该颜色盒子的RGB范围 const colorBox = new ColorBox(colorRange, pixels.length, pixels); const divisions = 8; // 最大切割次数,在demo中写死 实际项目中使用者可自定义 let [box1, box2] = cutBox(colorBox); // 第一次切割 return queueCut([box1, box2], divisions); } function queueCut(queue, num) { let isSmallBox = false; while (queue.length < num && !isSmallBox) { const denselyBox = queue.shift(); const resultBox = cutBox(denselyBox); // 只返回一个盒子时,说明最长边已经小于阀值 if (resultBox.length === 1) { isSmallBox = true; queue.unshift(resultBox); return; } queue = insertValueInSortedArray(queue, resultBox); // 插值排序 } return queue; } function cutBox(colorBox) { const { colorRange, total, rgbArr } = colorBox; const cutSide = colorBox.getCutSide(colorRange); if (cutSide === -1) return colorBox; // 当切割边为-1即切割范围小于阀值时 停止切割 const colorInCutSide = rgbArr.map((item) => item[cutSide]); // 统计出各个值的数量 let medianColor = getMedianColor(colorInCutSide, total); // 此处做简易处理,实际处理中需考虑中位数值的像素数量是否过大 if (medianColor === colorRange[cutSide][0]) { medianColor++; } // 防止空切 const newRange = getCutRange(colorRange, cutSide, medianColor); // 获取切割后的两个盒子的RGB范围 const dividedPixels = dividePixel(rgbArr, cutSide, medianColor); // 分割像素到两个盒子中并返回... return [boxOne, boxTwo]; } 看到这里是否对中位切分算法有了更深的理解?在实际运用中,要注意切割次数和最小子空间的设定会影响到结果的准确性。 3.2 八叉树 我知道有些同学看到这个名字的时候已经开始头疼了。但请先别急,听我慢慢讲解 先用一句话简单概况,八叉树(Octree)是一种用于描述三维空间的树状数据结构,它是一种递归的树形数据结构,可以将三维空间划分为八个等分的小立方体(子节点),每个空间可以进一步细分为八个子空间,以此类推。 八叉树怎么和RGB颜色盒子连接起来呢? 首先,要将三维空间划分成八个等分的小立方体,我们需要沿着每条轴的中点切割,确保每个子空间的边长相等,从而确保每个立方体的体积相等。 比如将一座葫芦山(完整颜色盒子)用八叉树切分后会得到八种不同颜色的葫芦娃; [图片](没办法葫芦娃只有七个人,拿蛤蟆凑个数) 但是其颜色,比如蓝色的范围太广泛了,其分为天蓝色、宝蓝色、钢蓝色、葫蓝色、蔚蓝色、紫蓝色等等,天蓝色又分为淡蓝色、浅蓝色、鲜蓝色、翠蓝色、深蓝色等等。那么我们就需要对这些葫芦娃进行递归切割以获取这些更精确的颜色。 那么我们要切割到第几层才能获取到具体的像素点(1*1*1的颜色盒子)呢?我们可以观察到,每对颜色盒子进行一次切割,会导致子节点继承的RGB范围减半,而256=2^8,这意味着在对一个完整的颜色盒子划分到第八层时,我们就可以获得单个像素点啦。 我们知道了八叉树对于颜色盒子的划分规则后,我们该怎么使用八叉树来提取图像主题色呢? 上文中提到八叉树是按照三条轴的中点对空间进行递归切割的,也就是说在八叉树第一层结构中,各轴会被分成[0, 127], [128, 255]两个区间,颜色盒子被均分成八个小盒子。那我们该如何将这些小盒子其与八叉树中的第一层节点对应起来呢? 与二分查找法类似,我们需要将像素的RGB值分别与其轴中点进行比较并将结果用0、1来表示(0-小于中点,1-大于中点),在第一层中得到的对应关系如下图所示。 [图片] 以此类推,我们最终得到以下对应关系。 [图片] 以此类推,我们可以得到各像素点在八叉树中所挂载的节点(如下图所示)。八叉树将颜色数据按照层级关系组织与管理起来,从而实现高效的存储和查找操作。 [图片] 知道了怎么将像素数据存入八叉树结构后,我们就可以开始提取主题色啦。 首先,在八叉树最后一层结构中,每个节点表示的是一个具体的RGB值,我们称其为叶子节点。与此同时,在最后一层中每个兄弟节点的RGB差值范围为1,也就是说该父节点的所有子节点的颜色都十分类似。我们可以发现,在八叉树较深层级中,兄弟节点的RGB差值范围较小,公式为diff=2^(8-n),其中diff为RGB差值,n为层数。 那么我们可以利用这个特性来合并相似元素从而更有效地表示数据,当一个颜色盒子中存有过多小盒子(子节点)与葫芦娃(像素)的时候,丢弃其中所有小盒子,并保留所有葫芦娃至父盒子中,从而进行空间压缩,减少空间复杂度。 为什么要进行合并呢?在介绍中位切分法的篇幅中,我们提到,提取主题色其实就是找到密度最集中的几个簇,被合并的盒子的像素都为那些像素集中且密度较大的盒子。这便是我们所需找到的簇,也就是主题色。 如果将所有像素全都储存在八叉树结构后,才进行合并叶子节点的步骤的话,这无疑极大的增加了空间复杂度,所以我们设定了一个阀值N(可自定义),一般来说,N越大,得到的结果越精确,但时间复杂度与空间复杂也会同步上升。 在将像素插入八叉树结构的过程中,当叶子节点数量超过N时,我们便会将挂有最多叶子节点的父节点进行合并操作。合并后,将该父节点设为叶子节点,且之后所有在该分支上的像素均挂载至该父节点上。 当所有的像素数据遍历结束后,我们就可以得到N个叶子节点了。 我们需要根据每个叶子节点上的像素数量对其进行排序,排名前列的便是我们所需要的主题色啦。 以上介绍了中值切分法和八叉树两种算法的实现 中位切分法适用于资源有限且需要快速提取图像主题色的场景,尤其适合实时或大规模处理需求。它的简单性和计算效率使其在这些场景下成为很好的选择,尤其是当主要目标是从图像中快速捕捉主题颜色而不需要过多考虑细节和渐变色时。比如当商户只需要快速批量地获取图片的主题色,且不关心主题色的精度时 而八叉树在动态适应性和处理颜色渐变方面具有优势,适用于需要保持细节不和平滑度的图像颜色提取任务,但其算法复杂度较高,内存消耗较大。主要应用场景如下: 并且该两种算法我们都会放在 Web Worker 中处理,以提高页面性能和用户体验,同时保持主线程的渲染进程不受影响。 五、成果 以上就是这篇文章的总体内容了,通过 Canvas,可以在 Web 前端对图像进行操作和渲染。结合切割算法,可以实现类似的图像颜色提取功能。这种方法尤其适合需要在用户界面中实时显示颜色提取结果的交互式应用。并且通过对于 Web Worker的一个使用,避免了对主线程渲染的影响,从而提升了用户体验。 接下来我们看一下会员卡接入该功能之后的一个整体效果吧。 会员权益卡接入该功能后,当用户上传完卡面图片,便会自动提取其中的主题色。这样,用户可以直接选择提取出的色彩作为主题色应用,以确保卡面图片和主题色的协调性。具体效果如下所示: [图片] 权益卡效果展示 会员等级卡接入该功能后的效果图如下所示: [图片] 等级卡效果展示 我们还可以利用该工具,当加载一些比较耗时的大图片时使用提取出的主题色进行渐变填充,实现一种"模糊渐变加载"的过渡效果。这种过渡场景可以增加页面加载的视觉吸引力和平滑性。如下图所示: [图片]
2024-02-21 - css新世界笔记
CSS新世界是知名博主张鑫旭CSS世界三部曲的最后⼀部,主要讲述CSS3及其之后很多实⽤ 的新特性,介绍了很多潜藏的特性和有⽤的细节。CSS世界介绍的是CSS2.1规范及其之前内 容,CSS选择器世界介绍的是CSS LV1-LV4的选择器知识,CSS新世界介绍的是CSS3及其之 后的知识。 CSS基础知识 CSS全局关键字属性值 inherit 继承关键字 实现属性继承,如 input { font-family:inherit;height:inherit } initial 初始关键字 适合⽤在需要重置某些CSS样式,但⼜不记得初始值的场景,如 ul { font-size: 14px } ul li:last-child { font-size: initial } unset 不固定值关键字 只有配合all属性使⽤才有意义,批量重置内置样式,如dialog { all: unset } revert 恢复关键字 让当前元素的样式还原成浏览器内置的样式,如 ol { list-style-type: repeat } @supports规则⽤来检测当前浏览器是否⽀持某个CSS新特性 <style> /* ⽀持⽹格布局 */ @supports (display:grid) { .item { background:red } } /* 不⽀持⽹格布局 */ @supports not (display:grid) { .item { background:red } } /* 同时⽀持弹性布局和⽹格布局 */ @supports (display:flex) and (display:grid) { .item { background:red } } /* ⽀持弹性布局或⽹格布局 */ @supports (display:flex) or (display:grid) { .item { background:red } } /* ⽀持弹性布局 但不⽀持⽹格布局 */ @supports (display:flex) and (not (display:grid)) { .item { background:red } } /* @supports规则的⼤括号⾥ 可以包含其他@规则*/ @supports (display:flex) { @media screen and (max-width: 750px) {} @supports (animation: none) { .box { animation: skip }} @keyframes skip {} } </style> <!-- 浏览器还提供了CSS.supports()⽅法,⽤来在javascript代码中检测 当前浏览器是否⽀持某个CSS特性--> <script> // CSS.supports(propertyName, value) if (window.CSS || CSS.supports || CSS.supports('position', 'sticky')){ document.getElementById('box').style.position = 'sticky' } </script> 增强的CSS属性 fit-content关键字 实现元素尺⼨的⾃适应<!-- 设置元素的width或height为fit-content关键字后,元素的尺⼨就是⾥⾯内容的尺 ⼨ --> <!-- 使⽤width:fit-content加margin:0 auto实现单⾏⽂字居中,多⾏⽂字居左对⻬ --> <div class="box">单⾏⽂字居中,多⾏⽂字居左对⻬</div> <style> .box { width:fit-content; margin:0 auto; } /* fit-content关键字的兼容性处理 IE浏览器、Edge79之前不⽀持 但可以⽤在移动端 */ .ex { width:-webkit-fit-content; width:-moz-fit-content;width:fit- content; } </style> position:sticky 黏性定位1.黏性定位:当元素在屏幕内时,随屏幕滚动;当元素滚出屏幕时,元素变成固定定位 2.黏性定位过去都是使⽤javascript实现,在现代浏览器可以使⽤position: sticky实现 3.在使⽤position: sticky时,务必保证黏性定位元素的祖先元素没有可滚动元素 currentColor 关键字<!-- currentColor 表示当前元素所使⽤的color属性的计算值 --> <div class="box">currentColor 使⽤当前color相同的颜⾊</div> <style> .box { color: red; border: 1px solid currentColor; /* 边框的颜⾊和⽂字 颜⾊⼀致*/ } </style> zoom 缩放 除Firefox外其他浏览器都⽀持语法 zoom: normal | reset | <number> | <percentage> zoom 和 scale 的区别 1. zoom属性缩放的中⼼坐标是元素的左上⻆,且不能修改。transform中的scale缩放默 认的中⼼坐标是元素的中⼼点 2. zoom属性缩放会改变元素占据的尺⼨空间,transform中的scale缩放不会改变元素占 据的尺⼨ 3. zoom属性不会改变元素的层级,不会影响元素的fixed定位 backface-visiblility 元素背⾯是否可⻅backface-visiblility: visible 默认值 元素背⾯是可⻅的 backface-visiblility: hidden 元素背⾯是不可⻅的 常⽤于transform变换中,是否要隐藏元素的背⾯,使变换效果更好 justify-content: space-evenly 每个flex⼦项空⽩间距相等ul{ display: flex; justify-content: space-evenly; } prefers-color-scheme ⽤来检测当前⽹⻚是否处于深⾊(⿊暗)模式的媒体属性<style> /* ⽀持三个属性: dark 系统倾向于使⽤深⾊模式 light 系统倾向于使⽤浅⾊模式 no-perference 系统未告知⽤户使⽤的颜⾊⽅案 */ /* 深⾊模式 */ @media (prefers-color-scheme: light) { body{ color:#333; background-color: #fff; } } /* 浅⾊模式 */ @media (prefers-color-scheme: dark) { body{ color:#fff; background-color: #000; } } /* 快速对现有⽹⻚进⾏深⾊模式改造的技巧 */ body { /* 使⽤filter:invert(1)滤镜对⽂字颜⾊和背景⾊等进⾏反相 */ filter: invert(1) hue-rotate(180deg); background-color: #000; } /* 对图⽚进⾏再次反相将图⽚还原成真实颜⾊ 避免应body反相后 图⽚颜⾊出现异常*/ img{ filter: invert(1) hue-rotate(180deg); } </style> <script> // 判断当前⼿机浏览器是否⽀持深⾊或浅⾊模式 // 是否⽀持 prefers-color-scheme 属性 console.log(window.matchMedia('(prefers-color-scheme)').matches) // true // 是否是浅⾊模式 prefers-color-scheme: light console.log(window.matchMedia('(prefers-color-scheme: light)').matches) // true // 是否是深⾊模式 prefers-color-scheme: dark console.log(window.matchMedia('(prefers-color-scheme: dark)').matches) // false </script> touch-action: manipulation 取消移动端点击事件300ms延迟和双击⾏为/* touch-action: manipulation 只允许浏览器进⾏滚动和持续缩放操作 */ html { touch-action: manipulation; } scroll-behavior: smooth 平滑滚动 safari不⽀持.box{ scroll-behavior: smooth; } /* 平滑滚动 */ overscroll-behavior: contain 滚动嵌套时,终⽌外层滚动 safari不⽀持/* 局部滚动的滚动条滚动到底部边缘时,再继续滚动时,外部容器不会再跟滚动 */ .box{ overscroll-behavior: contain;-ms-overscroll-behavior: contain; } pointer-events: none 元素⽆法点击caret-color: red 更改输⼊光标的颜⾊ /* 设置输⼊光标的颜⾊ 也可以⽤于设置了 contenteditable的html标签 */ input{ caret-color: red; } 简单实⽤的css函数 calc()函数的使⽤⽀持“+”、“-”、“*” 和 “/”四则运算; 可以使⽤百分⽐、px、em、rem等单位,不能使⽤当前CSS属性不⽀持的单位,可以混合使⽤ 各种单位进⾏计算; 表达式中有“+”和“-”时,其前后必须要有空格,如"widht: calc(12%+5em)"这种没有空 格的写法是错误的; 表达式中有“*”和“/”时,其前后可以没有空格,但建议留有空格。 .elm { width: calc(100% - 20px + 5px*2)) } min()函数的使⽤min(expression [, expression]) ⽀持⼀个或多个表达式,使⽤逗号分隔,将最⼩表 达式的值作为返回结果 实现⽹⻚在⼤于等于1024px的PC浏览器显示宽度为1024px,在⼩于1024px时显示宽度为 100% el { width: 1024px; max-width:100% } 使⽤min()函数 el { min(1024px, 100%) } min()⽤来限制最⼤值(即最⼤宽度值为1024px),IE和其他低版本浏览器不⽀持 max()函数的使⽤max(expression [, expression]) ⽀持⼀个或多个表达式,使⽤逗号分隔,将最⼤表 达式的值作为返回结果 实现⽹⻚在⼩于等于1024px的PC浏览器显示宽度为1024px,在⼤于1024px时显示宽度为 100% 使⽤max()函数 el { max(1024px, 100%) } max()⽤来限制最⼩值(即最⼩宽度值为1024px),IE和其他低版本浏览器不⽀持 clamp()函数的使⽤clamp()函数的作⽤是返回⼀个区间范围的值,语法:clamp(MIN, VAL, MAX),MIN表示 最⼩值,MAX表示最⼤值, VAL表示⾸选值,如果VAL在MIN~MAX范围内,则将VAL作为返回值,如果VAL⼤于MAX则将 MAX作为返回值,如果VAL ⼩于MIN,则将MIN作为返回值。clamp(MIN, VAL, MAX)等同于max(MIN, min(VAL, MAX)) IE和其他低版本浏览器不⽀持 <button>宽度⾃适应变化</button> button{ width: clamp(100px, 50%, 600px) } 不断改变浏览器视⼝的宽度,按钮宽度在100px~600px范围内变化,当屏幕宽度的⼀半⼤于 600px时,宽度为600px 当屏幕宽度的⼀半⼩于600px时,宽度在200px~600px之间,当屏幕宽度的⼀半⼩于200px 时,宽度为200px。 env() 环境变量函数环境变量env()规范的制定是源于解决‘刘海屏’和‘底部触摸⿊条’的移动设备的出现 在使⽤safe-area-inset-*等env的属性时,⼀定要设置meta信息<meta name="viewport" content="viewport-fit="cover""> 使⽤4个安全内边距值, 同时设置兜底尺⼨,⽆法使⽤safe-area-inset-*时,会使⽤兜 底的值 env(safe-area-inset-top, 20px) 距离顶部 设置顶部刘海区域安全距离 env(safe-area-inset-right, 1em) 距离右边 env(safe-area-inset-bottom, 20px) 距离底部 env(safe-area-inset-left, 20px) 距离左边 设置底部⼩⿊条安全距离 Css变量和⾃定义属性 变量名:值 声明css变量属性:var(--变量名) 使⽤css变量 <div class="box">深浅⾊模式</div> <style> :root{ --primary-color: pink; } /* 声明css变量 */ .box{ color: var(--primary-color)} /* 使⽤css变量 */ .box{ color: var(--primary-color, red)} /* 使⽤ var(css变量, 备⽤值) 当css变量⽆效时 使⽤备⽤值 */ </style> <script> // 在js中设置和获取 css⾃定义属性 // 只能借助 xx.style.setProperty 在js中设置 css变量 document.documentElement.style.setProperty('--myColor', '#0000ff') // 使⽤ getComputedStyle(document.querySelector('.box')).getPropertyValue(' --myColor') 获取css变量 console.log(getComputedStyle(document.querySelector('.box')).getPro pertyValue('--myColor')) // #0000ff </script>
2022-11-08 - 小程序获取不到用户头像和昵称返回微信用户问题解决,即小程序授权获取用户头像规则调整的最新解决方案
最近好多同学在学习石头哥小程序课程的时候,遇到了下面这样的问题,在小程序授权获取用户头像和昵称时,获取到的是下面这样的。 [图片] 到底是什么原因导致的呢,去小程序官方文档一看,又是官方改规则了。 [图片] 点进去一看,原来小程序官方,在2022年11月8日以后,又把获取用户头像的接口回收了 [图片] 再看看网友的评论,真是骂声一片啊。 [图片] 真是我的地盘我做主啊,我说怎么样就怎么样啊。有点店大欺客的嫌疑了。。。 但是呢,作为我们苦命的小程序开发者,官方虐我千百遍,我待官方如初恋。没办法啊,我们还是得用小程序不是吗。。。。 所以石头哥这里给大家提供几种解决方案。 一,临时解决方案,降低基础库 其实官方又说一句话,对于低于2.27.1版本的小程序,还是可以使用授权接口的,也就是说,只要我们的基础库低于2.27.1,就可以接着获取用户头像的。 [图片] 带着试一试的心态,石头哥就去把基础库调低为2.27.0 [图片] 调低后,再试下获取头像功能,果然还是可以获取到的。 [图片] 这里要注意一点,调低后,要记得清空下缓存 [图片] 虽然这个方法可以解决,但是只是一时的,因为小程序官方一直有bug,所以官方会一直升级基础库的,如果我们使用这个方法太久,就会导致基础库版本落后太多,这样的话,后续就会因为基础库太低,导致一些官方新功能无法使用。所以这个方案只是临时解决方案。 二,(不推荐)官方方案,头像昵称填写能力 官方为了补偿我们呢,给我们提供了一个新的方案。 [图片] 虽然这个方案可以获取到头像和昵称,但是呢。。。。。 [图片] 我们这里是可以获取到用户头像,但是官方给我们返回的这个头像是一个临时的链接。 [图片] 既然是临时链接,就意味着这个链接用不了太久就会失效了 [图片] 如果我们想用这个头像作为商品评论里的头像,那么就不能用这个临时链接了。所以官方出的这个有点鸡肋,基本上没有什么大用。 有用的也就是这个获取昵称。 [图片] 就是在填写昵称的时候,给input设置一个属性,就可以选用微信昵称或者自己输入新的昵称。 [图片] 所以如果你只需要使用用户昵称不使用头像,可以用这个方法。 三,(推荐)自己存储头像和昵称 既然官方老是变来变去,还不给我们可以长久使用的头像链接,那么我们就来自己存储用户头像,让这个头像是长久可以用的链接。所以我们这里需要自己开发后台存储头像。有以下几种方式 1,用Java或者php开发后台,存储头像和昵称 2,用云开发的云存储存储头像生成永久链接 我这里推荐大家使用云开发,因为云开发比较简单,当然大家如果会Java或者php开发,可以自己开发后台用于头像的上传和存储。 获取昵称和头像 首先看下效果图 [图片] 可以看到这里可以获取到昵称,并且可以自己自定义头像,这个自定义的头像存到云存储里,返回的是一个永久的可以使用的链接。 [图片] 这样我们后面再使用这个头像和昵称,就是永久的了,并且也不用再管小程序官方是不是又改规则了。 我把对应的代码,完整的贴出来给到大家 wxml [代码]<view class="item"> <view class="tip">编辑用户昵称</view> <input type="nickname" bindinput="getName" /> </view> <view class="line"></view> <view class="item" bindtap="chooseImage"> <view class="tip">点击修改头像</view> <image src="{{avatarUrl}}" /> </view> [代码] wxss样式 [代码].item { display: flex; align-items: center; justify-content: space-between; margin: 15rpx; border-bottom: 1rpx solid gray; padding-bottom: 20rpx; } .tip { font-size: 44rpx; margin: 20rpx; color: red; } .item image { width: 200rpx; height: 200rpx; border-radius: 10rpx; margin: 10rpx 30rpx; } .item input { flex: 1; border: 1px solid gray; border-radius: 20rpx; padding: 5rpx 15rpx; } [代码] js逻辑代码 [代码]const app = getApp() //这里要注意,初始化云开发,下面env要换成你自己的 wx.cloud.init({ env: 'cloud1-3g3xyg1a9ff9d8fe' }) const db = wx.cloud.database() Page({ getName(e) { this.setData({ name: e.detail.value }) }, //选择图片 chooseImage() { wx.chooseMedia({ count: 1, mediaType: ['image'], sizeType: ['compressed'], //可以指定是原图还是压缩图,这里用压缩 sourceType: ['album', 'camera'], //从相册选择 success: (res) => { console.log("选择图片成功", res) let avatarUrl = res.tempFiles[0].tempFilePath this.setData({ avatarUrl: avatarUrl }) wx.cloud.uploadFile({ cloudPath: new Date().getTime() + '.png', filePath: avatarUrl, // 文件路径 }).then(res => { let fileID = res.fileID console.log("上传返回的头像永久链接", fileID) }).catch(error => { console.log("上传失败", error) }) } }) }, }) [代码] 当然这里是基于云开发的,如果大家对云开发不了解,可以去看石头哥录得云开发视频:https://www.bilibili.com/video/BV1x54y1s7pk 然后关于最新版的获取头像和昵称,我也在二手商城小程序的视频里有做详细讲解:https://www.bilibili.com/video/BV1WA411M7Dp [图片] 后面会分享更多小程序相关的知识给到大家,欢迎点赞留言加关注。
2022-11-15 - 微信云开发成长笔记
33.如何批量下载云储存文件 参考地址 32.联表查询生成消费记录统计,用户数量和消费次数 联表生成的数据 [图片] 想要的数据统计(统计当天总消费次数,和当天访问总人数) [图片] [代码] const storeId = '0ab5303b62cc290d0e0799a838ca9158' const createTimeStart = '2022-06-15' const createTimeEnd = '2022-09-01' db.collection('deal').aggregate().match({ _storeId: storeId }).sort({ createTime: -1 }).lookup({ from: 'users', localField: '_userId', foreignField: '_id', as: 'userList', }) .replaceRoot({ newRoot: $.mergeObjects([ $.arrayElemAt(['$userList', 0]), { count: '$count', createTime: $.dateToString({ //将日期对象格式化为 年月日 date: $.dateFromString({ //将日期字符串转化为日期对象 dateString: '$createTime' }), format: '%Y-%m-%d' }), type: '$type', mobile: "$mobile" } ]) }) .match({ type: _.or([_.eq(2), _.eq(4)]), //筛选只要扣除的类型 createTime: _.and(_.gte(createTimeStart), _.lte(createTimeEnd)) //查询的时间区间 }) .group({ _id: '$createTime', //根据日期进行分组 consumeCount: $.sum('$count'), userCount: $.addToSet('$mobile'), //向数组中添加值,如果数组中已存在该值 }).project({ _id: 0, createTime: '$_id', consumeCount: 1, userCount: $.size('$userCount'), }) .sort({ createTime: -1 }) .end() [代码] 31.web本地上传文件 cloud.uploadFile cloud.uploadFile ,建议配合 Cloud.getTempFileURL 转换为 [代码]https://[代码]进行访问 [图片] 你可能会遇到跨域问题,则需要再腾讯云中配置本地测试的安全域名,一般默认的 [代码]localhost:8080端口[代码]时可用的,其它要自行配置 [图片] [图片] [图片] [图片] [图片] [图片] 30.查询条件根据时间区间筛选 createTimeStart <= createTime <= createTimeEnd [代码]createTimeStart 可以为字符串格式 "2022-08-17 9:50:00"[代码] [代码]createTime:_.and(_.get(createTimeStart),_.lte(createTimeEnd)) [代码] 29.静态网站访问云储存图片路径 小程序端一般上传图片后,会生成一条云路径地址 [代码]cloud://xxxxxx[代码],但事实上外部(web)访问这个路径是行不通的。 但是可以通过 Cloud.getTempFileURL 转换为 [代码]https://[代码]进行访问 28.web端如何实现连接云数据库 参考文章 27.联表查询 Aggregate().lookup() Aggregate().lookup() .project() $.arrayElemAt [代码]//联和 store_users和users,将store_users中的_userId与users中的_id匹配,把匹配符合条件的存放到userInfo数组中 db.collection('store_users').aggregate() .lookup({ from: 'users', localField: '_userId', foreignField: '_id', as: 'userInfo', //聚合匹配出的数组 }).project({ // 取匹配数组的第0个对象 userInfo:$.arrayElemAt(['$userInfo', 0]) }) .end() [代码] 26.如何实现扫码处理成功后,实时监听获取结果 .watch() Collection.watch 如果非本人操作处理扫码结果,需要对应开启表权限 [图片] 25.如何删除所有记录 [代码]_.neq(0) 不等于0的值 [代码] [图片] 24.云开发发送手机验证码 cloudbase.sendSms 微信小程序cloudbase.sendSms中template_id怎么获取? [代码]你可能会遇到发送成功但是一直收不到验证码的问题[代码] [图片] 或者在云开发控制台,查看 [代码]短信监控[代码] 日志 [图片] 23.云开发获取当前微信用户绑定的手机号 1.用户点击授权按钮获取code 2.接口通过code获取手机号 [代码]<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"></button> ... Page({ getPhoneNumber (e) { console.log(e.detail.code) } }) [代码] [代码]//云函数 config.json 中配置 { "permissions": { "openapi": [ "phonenumber.getPhoneNumber" ] } } ... //云函数调用 const result = await cloud.openapi.phonenumber.getPhoneNumber({ code: '获取的code' }); return { success: true, data: result, message: '获取成功' }; [代码] [代码]你可能会遇到 获取不到code的问题,建议更新手机微信版本或者开发工具版本。[代码] 确认当前基础库版本是否为 2.21.2 及以上。 正常情况下,基础库 2.21.2 对应的是客户端版本号为 8.0.16,如果你的版本低于 8.0.16 并且是通过开发者工具向客户端推送的 2.21.2 基础库,大几率不会返回 code 参数,请手动更新微信版本。 22.如何实现刷新当前页面 网上有很多方法,我这边采用的是 [代码]this.onLoad()[代码],不过这种方式需要你重新初始化一遍[代码]data[代码]里面的数据 简单实现一个倒计时,结束时刷新页面 [代码]let timer = null const time = 120 //有效时间120,超时自动刷新 Page({ data: { validTime: time }, onLoad: function (options) { this.validTimeInit() }, onUnload: function () { clearInterval(timer) }, validTimeInit() { let _this = this timer = setInterval(() => { let validTime = this.data.validTime validTime-- if (validTime < 0) { clearInterval(timer) _this.setData({ validTime: time }) _this.onLoad() } else { this.setData({ validTime: validTime }) } }, 1000); } }) [代码] 21._.or()查询多个符合条件的数据 云开发查询or的用法 _.eq() 查询筛选条件,表示字段等于某个值 [代码] const stores = data.map(s => { return _.eq(s._storeId) }) || [] const { data: storesInfo } = await db.collection('hyc_stores').where({_id: _.or(stores) }).get() [代码] 20…add()数据时返回值是res .add() 类似[代码].where,.update[代码]返回的都是 [代码]res.data[代码],而[代码].add[代码]返回的是[代码]res[代码],且只有添加成功的[代码]_id[代码] [图片] 19.moment 格式化时间不匹配 不清楚具体原因,但是网上说是 时区问题 加上 [代码].utcOffset(480)[代码] 就可以了 [代码] moment().utcOffset(480).format('YYYY-MM-DD HH:mm:ss') [代码] 18.onShow(options) 参数拿不到 onShow和onLoad的区别 Page页面级 [代码]onShow[代码] 没有 [代码]options[代码],全局的App里面的 [代码]onShow[代码]才有,想获取到参数尽量用 [代码]onLoad[代码] https://developers.weixin.qq.com/community/develop/doc/0008005f68c300c912070098b56800 17.常用云函数指令 [代码]const _ = db.command[代码] _.or :满足其中一个条件即可 跨字段的或操作; 查询更新数组/对象 [代码]//数组 arr 第 index 下标对象的 id [`arr.${index}.id`]: id [代码] 联表查询 16.父子组件通信 使用微信小程序全局变量 小程序的全局变量存储在文件 app.js 的globalData对象中 [代码]// app.js 中定义 App({ globalData: { hasLogin: false, openid: null }, }) // 其他文件中读取使用 const app = getApp(); console.log(app.globalData.hasLogin) [代码] 使用微信小程序的 数据缓存 [代码] wx.setStorageSync('userInfo', data) wx.getStorageSync('userInfo') [代码] 使用父子组件间的属性 父组件传值给子组件,子组件可以在 properties 中接收并使用 子组件传值给父组件,可以通过方法触发 triggerEvent [代码]//子组件 this.triggerEvent('myEvent',传参) //父组件 <子组件 bind:myEvent="传递方法" /> [代码] 15.input修改数组对象的某一个值 解决方案 14.微信map地图组件闪退问题 安卓手机在切换tab的时候,频繁切换时,小程序会闪退 解决方案 13.日期格式化: [代码]<wxs src="../../utils/filter.wxs" module="filter"></wxs> <text class="time"> {{filter.format(newsMsg.createAt,'YYYY-MM-DD')}}</text> [代码] 利用[代码].wxs[代码]方法,在页面上引入 utils/filter.wxs [代码]function format(date, fmt) { let ret; const opt = { "Y+": date.getFullYear().toString(), // 年 "m+": (date.getMonth() + 1).toString(), // 月 "d+": date.getDate().toString(), // 日 "H+": date.getHours().toString(), // 时 "M+": date.getMinutes().toString(), // 分 "S+": date.getSeconds().toString() // 秒 // 有其他格式化字符需求可以继续添加,必须转化成字符串 }; for (let k in opt) { ret = new RegExp("(" + k + ")").exec(fmt); if (ret) { fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0"))) }; }; return fmt; } function formatNumber(n) { n = n.toString() return n[1] ? n : '0' + n } function getWeek(n) { switch (n) { case 1: return '星期一' case 2: return '星期二' case 3: return '星期三' case 4: return '星期四' case 5: return '星期五' case 6: return '星期六' case 7: return '星期日' } } module.exports.format = format [代码] 12.下拉加载更多: .json [代码]"enablePullDownRefresh":true, [代码] .wxml [代码] /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function () { this.setData({ pages: 0, list: [], 'queryModel.current': 1, 'queryModel.size': 10, 'queryModel.totalCount': 0 }) // 接口加载数据 this.getNewsPage() // 停止加载,下拉返回 wx.stopPullDownRefresh(); }, [代码] 11.空格,’ ’问题 👉 复 制 吧 👈 微信小程序中的多个空格怎么打? | 微信开放社区 10.text文本属性 如果属于纯文本,切属于 <textarea>输出的内容,尽量用 <text>组件标签 [代码]<text>{{details.intro}}</text> [代码] 9.rich-text 富文本图片和换行问题 rich-text [代码] <rich-text nodes="{{details.content}}"></rich-text> [代码] 图片百分比 [代码]//rich-text富文本图片自适应处理 // content 为富文本内容 content.replace(/\<img/gi, '<img style="max-width:100%;height:auto" ') [代码] 不换行问题/app.wxss [代码]/* 富文本样式,解决富文本不换行问题 */ rich-text { word-break: break-all; white-space: pre-wrap; } [代码] 8.对象深拷贝 [代码]//对象深拷贝 const deepClone = function (obj, newObj) { var newObj = newObj || {}; for (let key in obj) { if (typeof obj[key] == 'object') { let isArray = Array.isArray(obj[key]); //判断是否数组 newObj[key] = (isArray == true ? [] : {}) deepClone(obj[key], newObj[key]); } else { newObj[key] = obj[key] } } return newObj; } module.exports = { deepClone } [代码] 7.request请求封装 utils/request.js [代码]const baseURL = '服务器地址'; function request(method, url, data) { return new Promise(function (resolve, reject) { let header = { 'content-type': 'application/json', }; wx.request({ url: baseURL + url, method: method, data: method === 'POST' ? JSON.stringify(data) : data, header: header, success(res) { //请求成功 //判断状态码---errCode状态根据后端定义来判断 if (res.data.code == 200) { resolve(res.data); } else { //其他异常 reject('运行时错误,请稍后再试'); } }, fail(err) { //请求失败 reject(err) } }) }) } export default request [代码] api/index.js使用 [代码]// 小程序接口 import request from '../utils/request' const API = { getFlightPage: (data) => request('POST', '/flight/page', data), // 查询各模块的在线生活服务 lifeServeList: (moduleId) => request('GET', `/module/life/service/list?module=${moduleId}`), } module.exports = { API: API } [代码] app.js入口引入 [代码]//引入api接口 const $api = require('./api/index').API ... App({ $api, ... }) [代码] .wxml [代码]const app = getApp() ... //接口调用 app.$api.newsDetails().then(res => {}) [代码] 6.修改页面背景色 .wxss [代码]page{ background-color:'每个页面都可以自定义背景色' } [代码] 5.<image>mode属性 <image>组件 4.IOS<video>黑屏问题 [代码]custom-cache="{{false}}"[代码] [代码]一般缓存为一级缓存、二级缓存、和自定义缓存,换而言之custom-cache="{{false}}"就是不使用自定义缓存的意思。为什么用了视频组件会卡loading加载不出来呢,可能是因为微信设置的自定义缓存的位置有容量限制,因此将它关闭了以后会自动使用系统缓存,可能就没有这个问题了。[代码] [代码]<video autoplay="true" src="{{url}}" controls custom-cache="{{false}}"></video> [代码] 3.微信小程序暗黑模式 暗黑模式详解文章 2.微信小程序escape转码 例如你要传递一个URL链接参数到下一个页面,你可能发现微信会自动给你截取一部分参数,因此可以通过此种方式避免。 [代码]//转码 escape() //解码 unescape() [代码] 1.微信小程序生成二维码 weapp-qrcode.js [代码]//引入 const QRCode = require('../../utils/weapp-qrcode.js') [代码] [代码]//使用 new QRCode('myQrcode', { text: `xxx链接`, width: 200, height: 200, padding: 12, // 生成二维码四周自动留边宽度,不传入默认为0 correctLevel: QRCode.CorrectLevel.L, // 二维码可辨识度 callback: (res) => { console.log(res.path) // 接下来就可以直接调用微信小程序的api } }) [代码] [代码]<canvas class="canvas-code"canvas-id="myQrcode" style="background:#fff;width: 200px;height: 200px;"/> [代码] [代码]//调用扫一扫获取结果 // 只允许从相机扫码 wx.scanCode({ onlyFromCamera: true, success(res) { wx.navigateTo({ url: res.result //xxx链接 }) } }) [代码]
2022-09-21 - “视频号+”的机遇之社群篇(三):如何借助视频号激活微信社群
“视频号+”的机遇之社群篇(三)微信视频号的出现,彻底地将微信生态的商业系统打通了,进一步提升了微信社群的商业价值,视频号成为社群主打造IP、社群用户互动的新型可视化社交载体,没有视频号,社群用户的信任力打造、内容运营、用户互动等效率会大大降低;没有社群,视频号的涨粉、评论、点赞、直播带货也会变得艰难,可以说视频号和社群是微信社群商业的新机遇,视频号轻创业适合每一个平凡的个体和企业。 如何借助视频号激活微信社群 相信很多社群新手在刚刚建立社群的时候,都会遇到这样的问题:不知道社群顶层设计怎么做,群建好了不知道怎么和用户沟通、投放什么内容,害怕透支人脉等。或者是经营了很久的群普遍半死不活,又不想放弃,想通过一些运营方法激活它。你是否遇到过这些情况呢?有没有一些标准化的方法激活我们的社群呢?答案是标准的模式有,但每个群的运营以及活跃程度完全取决于运营者的性格和用心,同一个方法在不同人的手上会呈现出不同的效果。我在这里提供一些运营经验供大家参考。 1、激活社群的五种玩法在视频号还没有出来之前,我们社群的运营压力其实很大,毕竟用户的注意力都被抖音、电商、实体店、生活琐事、工作等分散得很厉害,要想让用户把注意力放在你的社群里,就得使用一些方法了,比如持续输出价值、有重磅大咖驻场、付费入群、群内有相互认识的好友等都是决定社群后续能否活跃的关键。过去我们经常会用一些方法来带动社群的活跃度,以下五种玩法总有一种适合你: 方法一:轮值当群主 社群的线上运营,要保证活跃度,有三个关键: 一是有持续输出价值的内容;二是有信任你的用户;三是社群的人群经过了精准筛选。 这三个基本条件符合后,我们再来根据自己的情况,看看使用哪一种运营策略才能带动更多人参与。很显然轮值当群主是一个不错的选择,那些没有持续内容输出能力的群主,做好一件事就可以了,那就是用户关系的连接。把用户当作社群里的IP来打造,群主以及团队在幕后服务他,这样既带动了群友的积极性,也增加了大量的UGC(用户生成内容),还能帮助轮值的群主对接资源,一举多得。 但这种玩法也有弊端,比如不是每个群友都具备内容输出的能力,大部分的人只会讲产品,很容易变成一次广告宣传,这对于群的运营极其不利,作为群主必须防范这类事件的发生。当值群主输出内容前,我们需要提前3天和他沟通,并做好内容的审核、话术框架的标准化。这样就确保了那些有内容的人能讲出特别打动人心的故事,也增加了社群的温度感,不至于因为不同轮值群主的输出,导致社群变成了广告群。另外一点要特别注意的是,尽管这个方法适合做社群促活,但我们必须把好关以及提前沟通,通过“接龙+一对一私信沟通”的方式分批确定参与轮值群主的人选,把优质的群友筛选出来,而没有输出能力的人,尽量不要给予其做轮值群主的机会。 方法二:签到打卡 一些消费类、教育类的企业社群,通常会用到这种玩法。直接面对消费者的社群,基本上是以成交为目的的,但是我们又不能拉群直接做转化,需要有一个培养用户习惯的过程。这个时候签到打卡就成了消费类社群必然的选择。具体的玩法是这样的:用户进群后,群助手会引导用户签到打卡,并告知签到得到多少积分可以兑换什么产品福利;驱动用户邀约3个新人,邀约后获得第一个礼包;如果想加入进阶群,那就需要连续打卡7天积累一定的积分之后,才给予更高的福利和加入进阶群的机会。 这种玩法纯微信群是玩不转的,一般会结合第三方打卡工具,如微打卡、小打卡等小程序来协助群主,通过技术手段引导用户打卡、分享海报等。这么做的好处是:能坚持7天的用户,证明他对于我们设置的奖励或进阶群是非常感兴趣的,也逐步培养了用户的习惯,为后续产品变现建立了用户信任关系。 方法三:群接龙+红包 微信群中有一个“群接龙”的功能,我相信很多人都看到过。但是如何使用“群接龙”增加社群的活跃度呢?这里就要设计使用场景,第一种是销售场景,比如我们常见的社区团购、课程拼团、产品发售等都会用到接龙功能;第二种是学习场景,尤其是结合训练营的学习场景,我们会在交作业环节使用接龙刺激那些懒惰的学习者积极提交作业,在答疑环节引导大家接龙提出问题和导师互动,这样也增加了社群的互动性和价值感。 群接龙虽然能带动群的活跃度,但也容易让部分用户养成惰性,习惯性潜水,为了判断有多少人在关注这个群,我们有时候也会用发红包的方式来测试,用抢红包的速度、人数来判断这个群的关注度,发红包是社群运营一个非常重要的检测手段。 方法四:晨读打卡 近几年,学习型的社群是最火的,尤其是各种读书会、早读营等。晨读不在于读了什么,而在于养成一种早起的习惯,和一群优秀的人在一起,勉励自己不能偷懒,刻意练习让自己变得更优秀。 了解了“晨读打卡”的核心使命之后,你就应该知道这种玩法对于社群主的核心价值了。最重要的两个价值就是筛选优质的用户以及寻找潜在消费者。为了让晨读打卡做得更有仪式感,一般都会分组进行,每个组都会安排一个组长带头在群里领读,其他人可以跟随学习和参与互动,这样就把整个社群的氛围带起来了。 方法五:训练营 训练营是一种促活手段,也是社群常见的一种变现模式,比如你一定参与或听说过类似“7天英语学习营”“14天社群实战营”等。训练营更像是一个学习场景,比如一起减肥、一起实操社群裂变、一起学英语等,它会在短时间内形成一个聚合的场景,能激发用户的积极性。通常这类训练营都采取付费入群的方式,所以不用担心人群不精准的问题,关键是要做好训练营的流程设计、积分体系、奖励机制、小组分工、仪式感的营造,等等。 上述五种方法是比较常见的,这些方法在未来的社群运营中依然会发挥很大的价值。而视频号的出现,能更加真实地代入场景感,更加深度地连接真实用户,更有利于社群用户信任力的打造。 2、“视频号+社群运营”的五种玩法在视频号出现的这段时间里,我们已经尝试开启了“12天视频号社群裂变营”“视频号就是冲锋号”等社群,在实践过程中也有一些心得,在这里一并分享出来。 方法一:视频号+早读+训练营 微信社群的封闭性,导致我们无法做到让运维、引流和转化三者形成合力,这也是为什么我们很多群主在使用“早读训练营”模式的时候通过海报的方式引导用户转发,目的就是想借助用户的朋友圈做社交引流,而早读本身也是一件封闭的事情,很多用户不太喜欢被要求分享到朋友圈,只愿意在群里打卡。 视频号的出现解决了上述几个问题,我们不需要刻意让用户去转发海报了,直接把早读分享的方式切换成拍视频,这样做有什么好处呢?用户拍完视频号短视频后,按照打卡要求使用“#话题”、点赞、收藏、评论等视频号功能,就已经实现了传播的效果。用户拍完视频后再转发到群里,群里的用户就成了他的潜在用户,同时也帮助群主做了传播,这是多赢的局面。这个方法为社群引流增长、社群促活提供了新的思路。 方法二:视频号+资源推荐 用户进入一个社群,他的需求可能比较多,主要有学习、交友、获取资源、卖货等,而获取资源是很多社群新人最想要的。以前我们只能在几个环节嵌入资源对接,比如新人入群会安排做自我介绍,提供一个标准化的介绍模板,方便其他人快速了解他并产生链接;另一种情况是,我们会安排新人在固定时间来分享自己的故事,让其他群友能更加具体地了解分享者。但这些信息如果不做二次内容加工,很有可能被后续信息流淹没。这些内容其实价值巨大,属于这个社群曾经发生过的故事,必须通过二次加工将内容整理好发布到公众号或者其他内容平台,甚至有的社群做得更细致,会将这些内容整理成一个“群友资源录”,更加真实地让新人感受到社群的价值。 现在视频号如何解决资源对接的问题? 我们自己在“12天视频号社群裂变营”就使用过视频号社群推荐的方法,让参与者拍一段视频为社群代言,并带上同一个话题“#社群行业内容联盟”,这样做既能建立社群的长尾流量,同时也为社群记录下精彩的瞬间。我们也看到一些视频号博主做视频号大咖推荐、视频号直播推荐等都是结合社群来做的。这些方式都可以增加用户参与度和提升社群主的IP影响力。 方法三:视频号+微课分享 你现在在微信群里做分享,是否还在用“图文+语音”,或者用小鹅通、千聊?如果你现在还在用这些方式,那就要抓紧时间改变一下了,之前的那些方式不是不能再用了,是信息的触达率非常低了。视频号出来以后,我们在做社群运营的时候,已经逐渐摆脱了“微信群+私聊”的单一方式。我们在做“12天视频号社群裂变营”的时候,视频号成了很好的内容输出阵地,我们会提前录制好一分钟的短视频讲述要分享的内容大纲,让学员提前知道能学到什么,然后使用视频号直播的方式和学员分享,这个流程走过几遍以后,发现体验非常好。视频号直播可以最小化,学员可以一边在群里互动一边听直播,整体的互动性比过去好很多倍。再加上视频号连麦功能,用户互动会更有效率。即使是不同行业的用户,我们也可以把公开课形式的内容输出用视频号结合微信群的方式来做。 方法四:视频号+话题接龙 社群的运营一定要围绕高效率的信息触达来做,我们觉得一个信息发出去用户就会看到,可实际情况是用户可能延后两三天才知道,有很多用户不会看群里的信息,信息就永远错过了。为了提高用户的活跃度,我们会采用群接龙的方式来造势,让那些潜水围观的用户也主动站出来,这有利于社群的氛围营造。视频号怎么接龙?就是群主发起一个话题,引发群友围绕这个话题讨论,然后接龙,比如针对“#视频号就是冲锋号”这个话题,大家可以用视频号作品来发表自己的看法,并在视频号作品的“描述”里添加这个话题。很多人一起使用“同一个话题”输出内容,在视频号的生态里声浪就会比较大,传播就能做得更好。类似的玩法还有“视频号+荐书”“视频号+隔空对话”“视频号+在线答疑”等,都是新的社群促活手段,我们不妨多去实践,总有一种可以帮助你做好社群促活。视频号给社群的内容运营、关系运营、线上活动运营提供了无限可能,能够深帮助我们连接一群人一起来做一件事,逐步形成聚变效应,让更多认同群主的人融入我们的社群里。 方法五:视频号直播+私域社群+红包抽奖 视频号直播最大的好处是用户可以看到群主真人,真人出镜更有利于产生信任。通过视频号直播,还能获得“关注”流量、“朋友在看”流量、“附近”流量等公域流量,通过直播间直播的方式+评论区+投放二维码等形式,可以引导新用户添加个人微信号和加入群,形成一个流量互通的商业闭环。 在社群端,为了进一步扩大流量半径和促活,我们会邀请多个赞助商作为社群联办方,同时设计一个引流策略,引导用户全员转发直播间到他的社群或朋友圈。在群里还可以做红包抽奖,凡是“关注+回复666”,并且在群里抢红包手气最佳的,就能获得我们的产品福利,这样一来又可以获得新一轮的社交流量,同时也让社群互动变得非常活跃。视频号直播的重点是和用户多互动,内容大于互动,除了这些运营方法,还可以在群里收集问题,然后群主在直播间专门解答这些话题,进一步增加社群的活跃度。 下期分享内容:“视频号+”的机遇之社群篇(四):如何借助视频号倍增社群势能
2022-08-10 - 新版交易组件接入的指引与Q&A(本文不在更新,看文章内新地址)
本文不在更新,请看新版自定义交易组件接入指引 看帖不点赞,bug千千万 需要先申请开通“交易组件场景专用商户号”才可以完成新版交易组件场景接入(申请场景经营商户号这是必要条件),进行接入时一定要按照文档流程顺序进行接入,不要新旧接口混合调用,否则无法正常跑通完整流程,切记!切记!切记! 先配个图证明新版接入已完成 [图片] 有新问题可以留言,有准确答案(方案)后补充更新 一、升级版自定义交易组件接入说明 1、组件介绍 若商家此前已经完成视频号接入小程序,在小程序中调用升级版自定义交易组件组件后,可在保留原有的界面、功能及交易链路的情况下接入微信视频号场景。通过调用商品上传、订单生成、状态同步等接口,实现在视频号场景中交易资金流、售后、交易纠纷、客服等能力的标准化。 2、功能特点 可在视频号场景实现商品展示和带货等功能 未来可支持更多直播营销玩法(券、 秒杀、预售等) 支持小程序客服组件,商家能更方便收到用户的客服咨询 订单中心显示更完善的订单信息,用户可自行查看订单状态 支持用户在视频号订单中心继续付款、发起售后 3、上线案例 升级版自定义交易组件为商户提供保障用户体验的直播电商全链路能力: 可以使用微信支付商户号,资金结算更规范。 小程序和视频号的订单进行了双向打通,用户可以任选在小程序或视频号订单中心处理订单,例如重新发起支付、确认收货等,大大提升用户体验。 通过打通小程序客服组件,增强了商家处理商品咨询的能力。 [图片][图片][图片] 4、接入流程及官方文档 注意:整个接入流程需要15-30个工作日不等,建议提前准备商品的品牌、资质、类目信息,与开发调试并行,避免延误直播带货计划。 详情见:接入视频号指引 5、关键流程逻辑 注意“橙色”为新加入部分: [图片] [图片] 二、接入过程中常见问题 有新问题可以留言,有准确答案(方案)后补充更新 Q1:新版交易组件需要重新申请商户号吗?是否可以使用原有商户号? A1:不可以,新版交易组件必须要申请开通场景专用商户号 Q2:新版场景专用商户号费率是多少,是否有优惠,结算周期是多久? A2:商户号费率为0.6%,无费率优惠,结算周期为7+7日,即用户收货后7天后结算。 Q3:申请新商户号时,最后一步签约遇到“微信实名信息与管理员信息不一致”是什么原因? A3:申请新的场景专用商户号时,“超级管理员”这一项不支持修改,默认为小程序“超级管理员”实名信息,如需修改,需要为该用户前往成员管理为小程序绑定超级管理员。 Q4:申请新的商户号时,为什么不能修改主体信息? A4:“当前主体”这一项不支持修改,因为商户号主体必须和该小程序注册主体保持一致。 Q5:通过新版自定义交易组件申请的场景专用商户号是否对跨境类小程序(自助报关)有影响? A5:会,二级商户当前暂不支持自助清关接口调用,留意后续更新通知 Q6:自定义交易组件“升级版”跟升级前的自定义交易组件有什么区别,哪些接口需要升级? A6: 新支付接口,必须走新商户号。 取消订单, 小程序(小程序内以及发现-小程序我的订单)和视频号双向可取消,之前只可以在小程序上取消,然后同步给视频号状态。 申请退款,小程序和视频号双向可申请退款。 申请退货退款,小程序和视频号双向可申请退货退款,之前只有小程序上操作。 未付款订单,小程序和视频号 可在各自订单中心重新支付,同步状态。 确认收货,小程序和视频号双向可确认收货。 同步发货状态接口更新。 Q7:自定义交易组件验收流程走完后, 在MP后台点击完成依旧提示"检测到你未完成此项步骤, 请确认后重试"是什么原因? A7:需要通过调用新接口进行验收才可以通过。 Q8:调用自定义交易组件创建售后接口ecaftersale/add时报47001错误{“errcode”:47001,“errmsg”:"data format error "} A8:请检查“product_info”字段,注意对应类型为“object”。 Q9:调用自定义交易组件创建售后接口ecaftersale/add时报错2747002,参数错误{“errcode”:2747002,“errmsg”:"参数错误 "} A9:1.请检查“orderamt”参数,传参金额应不含邮费。 2.新旧接口不可混合调用,新接口不支持对旧接口生成的订单创建售后。 3.一个商品仅可以有一笔在流程的售后单,已创建或售后完结也会报次错误。 Q10:调用自定义交易组件“同意退货”接口ecaftersale/acceptreturn时报错“同意退货失败没有默认退货地址,需要在接口中传入” {“errcode”:9700210,“errmsg”:“errmsg” =>”同意退货失败没有默认退货地址,需要在接口中传入"} A10:需要调用“更新商家信息”接口,补充默认退货地址 Q11:调用自定义交易组件“添加商品”接口shop/spu/add时报错“该账号客服方式必须包含微信客服/小程序客服” {“errcode”:1040042,“errmsg”:"该账号客服方式必须包含微信客服/小程序客服”} A11:需要在MP后台配置微信客服/小程序客服后,然后通过“更新商家信息”接口更新商家信息[图片] 调用“获取商家信息”接口应返回一下内容才为成功,“service_agent_type”字段需要同时包含0,1,2三个值 [图片] Q12:调用自定义交易组件“创建订单”接口shop/order/add时报错“不支持的发货方式” {“errcode”:1010036,“errmsg”:"不支持的发货方式“} A12:视频号场景当前只支持“正常快递”方式,其他请留意后续更新。 Q13:自定义交易组件“创建售后单”接口中“refund_reason_type”字段 定义见枚举值定义 “emAfterSalesReason ”,“emAfterSalesReason”对应枚举值是什么? A13:INCORRECT_SELECTION = 1; // 拍错/多拍 NO_LONGER_WANT = 2; // 不想要了 NO_EXPRESS_INFO = 3; // 无快递信息 EMPTY_PACKAGE = 4; // 包裹为空 REJECT_RECEIVE_PACKAGE = 5; // 已拒签包裹 NOT_DELIVERED_TOO_LONG = 6; // 快递长时间未送达 NOT_MATCH_PRODUCT_DESC = 7; // 与商品描述不符 QUALITY_ISSUE = 8; // 质量问题 SEND_WRONG_GOODS = 9; // 卖家发错货 THREE_NO_PRODUCT = 10; // 三无产品 FAKE_PRODUCT = 11; // 假冒产品 OTHERS = 12; // 其它 Q14:自定义交易组件“获取售后单详情”接口中“status”字段 定义见枚举值定义 “AfterSalesState ”,“AfterSalesState”对应枚举值是什么? A14:AFTERSALESTATUS_INVALID = 0; USER_CANCELD = 1; // 用户取消申请 MERCHANT_PROCESSING = 2; // 商家受理中 MERCHANT_REJECT_REFUND = 4; // 商家拒绝退款 MERCHANT_REJECT_RETURN = 5; // 商家拒绝退货退款 USER_WAIT_RETURN = 6; // 待买家退货 RETURN_CLOSED = 7; // 退货退款关闭 MERCHANT_WAIT_RECEIPT = 8; // 待商家收货 MERCHANT_OVERDUE_REFUND = 12; // 商家逾期未退款 MERCHANT_REFUND_SUCCESS = 13; // 退款完成 MERCHANT_RETURN_SUCCESS = 14; // 退货退款完成 PLATFORM_REFUNDING = 15; // 平台退款中 PLATFORM_REFUND_FAIL = 16; // 平台退款失败 USER_WAIT_CONFIRM = 17; // 待用户确认 MERCHANT_REFUND_RETRY_FAIL = 18; // 商家打款失败,客服关闭售后 MERCHANT_FAIL = 19; // 售后关闭 Q15:自定义交易组件申请视频号专用商户号后,唤起支付报错: “商户号该产品权限未开通” A15:需要先调用“生成订单”接口,然后调用“生成支付参数”接口获取调取支付所需参数,不要调用微信支付统一下单接口获取调用支付参数 Q16:调用自定义交易组件“同意退款”接口shop/ecaftersale/acceptrefund时报错“同意退款失败” {“errcode”:9700209,“errmsg”:"同意退款失败 退款失败“} A:该问题是订单流转状态不对导致,请严格按照文档流程进行操作调用;新旧接口混合调用也会报此错误。 Q17:二级商户号订单支付流程与原有订单支付流程有什么区别? A17:主要区别是:二级商户号订单调起支付所需参数是通过“生成支付参数”获取,无需同步支付结果;原流程调起支付是需要通过微信支付统一下单获取,需要同步支付结果。 Q18:调用自定义交易组件售后相关接口:“创建售后单”、“用户取消售后单”、“用户上传物流信息”、“获取售后单列表”、“获取售后单详情”、“同意退款“、”同意退货“、“拒绝售后”、“上传退款凭证”、“更新售后单”等接口时报错{“errcode”: 48001,“errmsg”: “api unauthorized”} A18:未开通视频号场景经营商户号,需要先开通场景经营商户号才可以调用。 Q19:自定义交易组件二级商户单调起支付时报错“JSAPI缺少参数total_fee” A19:生成支付参数失败,没返回正确的预支付 ID,重新调用生成支付参数接口获取新的支付参数即可 Q20:调用自定义交易组件接口报错{“errcode”:61007,“errmsg”:“api is unauthorized to component”} A20:没有完成服务商授权。 Q21:已经开通了自定义交易组件,调用接口还是报错48001 A21:接口鉴权有本地缓存,一般最多10分钟,请稍后再试。 Q22:调用自定义组件接口报错“json异常” A22:结构体比较复杂,请检查字段层级。划重点: json不支持注释!!!json不支持注释!!!json不支持注释!!! Q23:调用自定义组件接口报错{“errcode”:1000000,“errmsg”:“订单状态流转异常”} A23: 订单严格按照:创建、支付、发货、收货的事件流转,如果已经取消,则不能继续流转。 Q24:调用自定义组件上传图片接口报错{“errcode”:1070008,“errmsg”:"获取图片失败,请使用流式上传 "} A24:一般是图片url在微信侧获取不刀,可能为图片cdn设置了白名单或者cdn服务商把微信出口ip 给“ban”了 Q25:调用自定义组件上传图片接口报错{“errcode”:1070001,“errmsg”:"文件/图片为空 "} A25:检查请求报文协议,需[代码]Content-Type: multipart/form-data[代码] Q26:调用自定义组件上传图片接口报错{“errcode”:1000035,“errmsg”:"无效链接 "} A26:请检查图片链接是否为有效链接 Q27:自定义交易组件接入后没有收到事件回调消息 A27:使用公众平台调试工具确保回调链路正常。事件消息如下 [图片] Q28:视频号橱窗管理获取不到对应小程序 A28:1、检查是否开通视频号场景;2、检查是否绑定了推广员(非小程序超管需要绑定推广员) 持续更新中~~~
2022-04-14 - 常用正则表达式
一、校验数字的表达式数字:^[0-9]*$n位的数字:^\d{n}$至少n位的数字:^\d{n,}$m-n位的数字:^\d{m,n}$零和非零开头的数字:^(0|[1-9][0-9]*)$非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(\.[0-9]{1,2})?$带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})$正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$有两位小数的正实数:^[0-9]+(\.[0-9]{2})?$有1~3位小数的正实数:^[0-9]+(\.[0-9]{1,3})?$非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$非负整数:^\d+$ 或 ^[1-9]\d*|0$非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$二、校验字符的表达式汉字:^[\u4e00-\u9fa5]{0,}$英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$长度为3-20的所有字符:^.{3,20}$由26个英文字母组成的字符串:^[A-Za-z]+$由26个大写英文字母组成的字符串:^[A-Z]+$由26个小写英文字母组成的字符串:^[a-z]+$由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+禁止输入含有~的字符:[^~\x22]+三、特殊需求表达式Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}电话号码正则表达式(支持手机号码,3-4位区号,7-8位直播号码,1-4位分机号): ((\d{11})|^((\d{7,8})|(\d{4}|\d{3})-(\d{7,8})|(\d{4}|\d{3})-(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1})|(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1}))$)身份证号(15位、18位数字),最后一位是校验位,可能为数字或字符X:(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$日期格式:^\d{4}-\d{1,2}-\d{1,2}一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$钱的输入格式:有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧。下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$中文字符的正则表达式:[\u4e00-\u9fa5]双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))空白行的正则表达式:\n\s*\r (可以用来删除空白行)HTML标记的正则表达式:<(\S*?)[^>]*>.*? |<.*? /> ( 首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)IP地址:((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))原文地址:https://www.qjson.cn/Tools/js_expression/
2022-04-06 - JavaScript对象、数组、日期操作方法封装
1.获取对象属性个数 objLength(obj) { var count = 0; for(var i in obj) { if(obj.hasOwnProperty(i)) { count++; } } return count; }, 2.数组排序 sortArr(arr,s){ // s:true 升序 false 降序 var s = '' || s; arr.sort(function (a, b) { if (s) {//从小到大排序 return a - b; }else{//从大到小排序 return b - a; } }); return arr;//返回已经排序的数组 }, 3.根据数组对象中的某个属性值进行排序的方法 sortBy(arr,attr,rev){ /*** arr 需要排序的数组 * attr 排序的属性 如number属性 * rev true表示升序排列,false降序排序 * */ //第二个参数没有传递 默认升序排列 if(rev == undefined){ rev = 1; }else{ rev = (rev) ? 1 : -1; } return arr.sort(function(a,b){ a = a[attr]; b = b[attr]; if(a < b){ return rev * -1; } if(a > b){ return rev * 1; } return 0; }) }, 4.数组去重 unique(arr) { return Array.from(new Set(arr)) }, 5.根据数组对象的某个属性进行去重 uniqueObj(arr1,from) { // arr1:要去重的数组 from:属性 const res = new Map(); return arr1.filter((a) => !res.has(a[from]) && res.set(a[from], 1)) }, 6.对象数组去重并且保留最后的对象 arrayUnique2(arr, name) { var hash = {}; return arr.reduce(function (acc, cru,index) { if (!hash[cru[name]]) { hash[cru[name]] = {index:index} acc.push(cru) }else{ acc.splice(hash[cru[name]]['index'],1,cru) } return acc; }, []); }, 7.删除数组中小于某个值的元素 handleArr(arr,smail){ // arr:数组 small:需要比这个值小 var newArr = arr.filter(item => item > smail); return newArr; }, 8.将一个数组等分成若干个数组,每个数组里有n条数据 bisectionArr(arr,n){ /**arr:数据 n每个数组里保留几条数据 * 用法 * app.bisectionArr(this.data.Arr,5); **/ var n=Number(n); var newarr = []; var len = arr.length / n; len = Math.ceil(len); for (var j = 1; j <= len; j++) { newarr[j - 1] = []; for (var i = (n * j - n); i < n * j; i++) { if (arr[i] != undefined) { newarr[j - 1].push(arr[i]); } } } return newarr; }, 9.根据身份证号获取出生年月日 getBirthdayFromIdCard(idCard) { var birthday = ""; if (idCard != null && idCard != "" && checkIdcard(idCard)) { if (idCard.length == 15) { birthday = "19" + idCard.substr(6, 6); } else if (idCard.length == 18) { birthday = idCard.substr(6, 8); } birthday = birthday.replace(/(.{4})(.{2})/, "$1-$2-"); } return birthday; }, 10.只能输入金额 onlyMoney(val){ var regStrs = [ ['^0(\\d+)$', '$1'], //禁止录入整数部分两位以上,但首位为0 ['[^\\d\\.]+$', ''], //禁止录入任何非数字和点 ['\\.(\\d?)\\.+', '.$1'], //禁止录入两个以上的点 ['^(\\d+\\.\\d{2}).+', '$1'] //禁止录入小数点后两位以上 ]; for(var i=0; i<regStrs.length; i++){ var reg = new RegExp(regStrs[i][0]); val = val.replace(reg, regStrs[i][1]); } return val; }, 11.根据身份证号返回所在省 getProvinceFromIdCard(idCard) { var aCity={11:"北京",12:"天津",13:"河北",14:"山西",15:"内蒙古",21:"辽宁",22:"吉林",23:"黑龙江 ",31:"上海",32:"江苏",33:"浙江",34:"安徽",35:"福建",36:"江西",37:"山东",41:"河南",42:"湖北 ",43:"湖南",44:"广东",45:"广西",46:"海南",50:"重庆",51:"四川",52:"贵州",53:"云南",54:"西藏 ",61:"陕西",62:"甘肃",63:"青海",64:"宁夏",65:"新疆",71:"台湾",81:"香港",82:"澳门",91:"国外"}; return aCity[idCard.substr(0,2)]; }, 12.获取当前日期 getNowDate(){ var date=new Date(); var year=date.getFullYear(); var month=date.getMonth()+1; var day=date.getDate(); var hh=date.getHours(); var mm=date.getMinutes(); var ss=date.getSeconds(); var arr=['日','一','二','三','四','五','六']; var week=arr[date.getDay()]; function add0(val){ return (val<10? '0'+val : val); } month=add0(month);day=add0(day);hh=add0(hh);mm=add0(mm);ss=add0(ss); return { data:year+'-'+month+'-'+day+' '+hh+':'+mm+':'+ss, year:year, month:month, day:day, hh:hh, mm:mm, ss:ss, week:week, }; }, 13.日期转时间戳 dateToChuo(riqi){ //riqi格式 2020-01-01 00:00:00 riqi=riqi.replace(/-/g,"/"); var date = new Date(riqi);//兼容ios var time = parseInt(date.getTime()/1000);//除以1000为10位时间戳 不除为13位 return time; }, 14.时间戳转日期 chuoToDate(timestamp){ if(!timestamp){ return false; } timestamp=timestamp.toString().length<13? (timestamp * 1000):timestamp; var date = new Date(timestamp);//时间戳为10位需*1000,时间戳为13位的话不需乘1000 var Y = date.getFullYear(); var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1); var D = (date.getDate()<10? '0'+date.getDate() : date.getDate()); var h = (date.getHours()<10? '0'+date.getHours() : date.getHours()); var m = (date.getMinutes()<10? '0'+date.getMinutes() : date.getMinutes()); var s = (date.getSeconds()<10? '0'+date.getSeconds() : date.getSeconds()); // + h + m + s return { data:Y+'-'+M+'-'+D+' '+h+':'+m+':'+s, year:Y, month:M, day:D, hh:h, mm:m, ss:s } }, 15.获取当前时间的后20分钟、后一小时(add是正数)、前一天(add是负数)等等相对时间 setDefaultTime(add,danwei){ // 调用方法:app.setDefaultTime(20,'天');20天以后的时间 var danwei=danwei || '秒';//默认单位是秒 if(danwei=='秒'){ add=add*1000;// add 单位是秒 }else if(danwei=='分'){ add=add*1000*60;// add 单位是分 }else if(danwei=='时'){ add=add*1000*60*60;// add 单位是时 }else if(danwei=='天'){ add=add*1000*60*60*24;// add 单位是天 } let t = new Date().getTime() + add; let d = new Date(t); let theMonth = d.getMonth() + 1; let theDate = d.getDate(); let theHours = d.getHours(); let theMinutes = d.getMinutes(); let getSeconds=d.getSeconds(); function add0(val){ return (val<10? '0'+val : val); } theMonth=add0(theMonth); theDate=add0(theDate); theHours=add0(theHours); theMinutes=add0(theMinutes); getSeconds=add0(getSeconds); let date = d.getFullYear() + '-' + theMonth + '-' + theDate; let time = theHours + ':' + theMinutes + ':' + getSeconds; let Spare = date + ' ' + time; return { datas:d.getFullYear()+'-'+theMonth+'-'+theDate, data:Spare, year:d.getFullYear(), month:theMonth, day:theDate, hh:theHours, mm:theMinutes, ss:getSeconds } }, 16.已知开始日期和结束日期 计算出中间的所有日期 getAllDate(start, end){ // start:2020-07-14 end:2020-07-20 let dateArr = [] let startArr = start.split('-') let endArr = end.split('-') let db = new Date() db.setUTCFullYear(startArr[0], startArr[1] - 1, startArr[2]) let de = new Date() de.setUTCFullYear(endArr[0], endArr[1] - 1, endArr[2]) let unixDb = db.getTime() let unixDe = de.getTime() let stamp const oneDay = 24 * 60 * 60 * 1000; for (stamp = unixDb; stamp <= unixDe;) { // parseInt(stamp) 13位的时间戳 dateArr.push(this.format(new Date(parseInt(stamp)))) stamp = stamp + oneDay } return dateArr }, format(time){ // time=new Date(13位的时间戳) let ymd = '' let mouth = (time.getMonth() + 1) >= 10 ? (time.getMonth() + 1) : ('0' + (time.getMonth() + 1)) let day = time.getDate() >= 10 ? time.getDate() : ('0' + time.getDate()) ymd += time.getFullYear() + '-' // 获取年份。 ymd += mouth + '-' // 获取月份。 ymd += day // 获取日。 return ymd // 返回日期。2020-07-14 }, 17.获取上个月的年月 getLastMonth(riqi){ riqi=riqi.replace(/-/g,"/"); var date = new Date(riqi); var year = date.getFullYear(); var month = date.getMonth(); if(month == 0){ year = year -1; month = 12; } return { year:year, month:month<10? '0'+month : month }; }, 18.获取指定日期的星期 getWeek(riqi){ riqi=riqi.replace(/-/g,"/"); var date = new Date(riqi);//兼容ios var arr=['日','一','二','三','四','五','六']; var week=arr[date.getDay()]; return week; }, 19.当前日期是今年的第几周 getYearWeek(year,month,date){ /* app.getYearWeek(2019,4,19) dateNow是当前日期 dateFirst是当年第一天 dataNumber是当前日期是今年第多少天 用dataNumber + 当前年的第一天的周差距的和在除以7就是本年第几周 */ let dateNow = new Date(year, parseInt(month) - 1, date); let dateFirst = new Date(year, 0, 1); let dataNumber = Math.round((dateNow.valueOf() - dateFirst.valueOf()) / 86400000); return Math.ceil((dataNumber + ((dateFirst.getDay() + 1) - 1)) / 7); }, 20.当前日期是当月的第几周 getMonthWeek(year,month,date){ /* app.getMonthWeek(2019,4,19) month = 6 - w = 当前周的还有几天过完(不算今天) year + month 的和在除以7 就是当天是当前月份的第几周 */ let dateNow = new Date(year, parseInt(month) - 1, date); let w = dateNow.getDay();//星期数 let d = dateNow.getDate(); return Math.ceil((d + 6 - w) / 7); }, 21.判断某年某月有多少天 getCountDays(ym) { var curDate = new Date(ym.replace(/-/g,"/")); /* 获取当前月份 */ var curMonth = curDate.getMonth(); /* 生成实际的月份: 由于curMonth会比实际月份小1, 故需加1 */ curDate.setMonth(curMonth + 1); /* 将日期设置为0 */ curDate.setDate(0); /* 返回当月的天数 */ return curDate.getDate(); }, 22.获取指定日期的第几个月 getHowMonth(date,num) { // date 格式为yyyy-mm-dd的日期,如:2014-01-25 // num 第几个月 下一个月 1 下两个月 2 上一个月-1 上两个月-2 以此类推 date=date.replace(/-/g,"/"); var dt=new Date(date); return this.chuoToDate(dt.setMonth(dt.getMonth() + Number(num))); }, 23.计算两个日期之间相差的年月日 monthDayDiff(startDate,endDate) { let flag = [1, 3, 5, 7, 8, 10, 12, 4, 6, 9, 11, 2]; let start = new Date(startDate); let end = new Date(endDate); let year = end.getFullYear() - start.getFullYear(); let month = end.getMonth() - start.getMonth(); let day = end.getDate() - start.getDate(); if (month < 0) { year--; month = end.getMonth() + (12 - start.getMonth()); } if (day < 0) { month--; let index = flag.findIndex((temp) => { return temp === start.getMonth() + 1 }); let monthLength; if (index <= 6) { monthLength = 31; } else if (index > 6 && index <= 10) { monthLength = 30; } else { monthLength = 28; } day = end.getDate() + (monthLength - start.getDate()); } return { year,month,day }; }, 24.计算两个时间之间的差 diffTime(startDate,endDate) { //用法 diffTime('2017-03-02 09:10:10','2017-03-17 04:10:12') startDate=startDate.replace(/-/g,'/');//ios兼容 endDate=endDate.replace(/-/g,'/');//ios兼容 startDate= new Date(startDate); endDate = new Date(endDate); var diff=endDate.getTime() - startDate.getTime();//时间差的毫秒数 //计算出相差天数 var days=Math.floor(diff/(24*3600*1000)); //计算出小时数 var leave1=diff%(24*3600*1000);//计算天数后剩余的毫秒数 var hours=Math.floor(leave1/(3600*1000)); //计算相差分钟数 var leave2=leave1%(3600*1000);//计算小时数后剩余的毫秒数 var minutes=Math.floor(leave2/(60*1000)); //计算相差秒数 var leave3=leave2%(60*1000);//计算分钟数后剩余的毫秒数 var seconds=Math.round(leave3/1000); var returnStr = seconds + "秒"; if(minutes>0){ returnStr = minutes + "分" + returnStr;} if(hours>0){returnStr = hours + "小时" + returnStr;} if(days>0){returnStr = days + "天" + returnStr;} return { data:returnStr, day:days,hh:hours,mm:minutes,ss:seconds }; }, 25.倒计时 countDown(jssj,success,times){ /*用法:app.countDown("2020-08-24 07:23:00",function(res){ console.log(res) },1000) jssj:设置结束时间 2020-08-10 12:12:12 times:设置倒计时的时间间隔 */ fun(); var timer=setInterval(function(){ fun(); },times); function fun(){ var lefttime = parseInt((new Date(jssj.replace(/-/g,"/")).getTime() - new Date().getTime())); if(lefttime <= 0) { success({day:"00",hour:"00",min:"00",sec:"00"}); clearInterval(timer); return; } var d = parseInt(lefttime /1000 /3600 /24); //天数 var h = parseInt(lefttime / 1000 / 3600 % 24); //小时 var m = parseInt(lefttime / 1000 / 60 % 60); //分钟 var s = parseInt(lefttime / 1000 % 60); //当前的秒 d < 10 ? d = "0" + d : d; h < 10 ? h = "0" + h : h; m < 10 ? m = "0" + m : m; s < 10 ? s = "0" + s : s; success({ day: d, hour: h, min: m, sec:s }) } }, 26.多长时间之前 timeago(stringTime){ var minute = 1000 * 60; var hour = minute * 60; var day = hour * 24; var week = day * 7; var month = day * 30; var time1 = new Date().getTime();//当前的时间戳 var time2 = Date.parse(new Date(stringTime));//指定时间的时间戳 var time = time1 - time2; var result = "刚刚"; if (time < 0) { console.log("设置的时间不能早于当前时间!"); } else if (time / month >= 1) { // result = "" + parseInt(time / month) + "月前"; result=stringTime.slice(0,10);//大于等于1个月的时候显示具体日期 } else if (time / week >= 1) { result=stringTime.slice(0,10);//大于等于1周的时候显示具体日期 // result = "" + parseInt(time / week) + "周前"; } else if (time / day >= 1) { result = "" + parseInt(time / day) + "天前"; } else if (time / hour >= 1) { result = "" + parseInt(time / hour) + "小时前"; } else if (time / minute >= 1) { result = "" + parseInt(time / minute) + "分钟前"; } else { result = "刚刚"; } return result; }, 27.获取n到m之间的所有数 getNDMnumber(n,m){ // n,m是整数 且n<m; var n=Number(n); var m=Number(m); var arr=[]; var i=n; while(i<=m){ arr.push(i); i++; } return arr; },
2022-02-23 - 服务端后台接口开发实战
5.1学习对象+课程目的+推荐工具 [视频] 5.2网络请求基本知识 [视频] 5.3Mysql基本概念入门 [视频] 5.4手把手1:树洞数据库设计 [视频] 5.5phpmyadmin可视化操作入门及树洞数据库实现(1) [视频] 5.5phpmyadmin可视化操作入门及树洞数据库实现(2) [视频] 5.5phpmyadmin可视化操作入门及树洞数据库实现(3) [视频] 5.6PHP基本概念入门(1) [视频] 5.6PHP基本概念入门(2) [视频] 5.6PHP基本概念入门(3) [视频] 5.7Thinkphp基本概念及操作-框架介绍(1) [视频] 5.7Thinkphp基本概念及操作-框架介绍(2) [视频] 5.8Thinkphp基本概念及操作-路由(1) [视频] 5.8Thinkphp基本概念及操作-路由(2) [视频] 5.9Thinkphp基本概念及操作-新增一条数据(1) [视频] 5.9Thinkphp基本概念及操作-新增一条数据(2) [视频] 5.10Thinkphp基本概念及操作-查询多条数据(1) [视频] 5.10Thinkphp基本概念及操作-查询多条数据(2) [视频] 5.11Thinkphp基本概念及操作-查询一条数据(1) [视频] 5.11Thinkphp基本概念及操作-查询一条数据(2) [视频] 5.12Thinkphp基本概念及操作-保存修改(1) [视频] 5.12Thinkphp基本概念及操作-保存修改(2) [视频] 5.13Thinkphp基本概念及操作-删除数据(1) [视频] 5.13Thinkphp基本概念及操作-删除数据(2) [视频] 5.14手把手3:用户的注册接口(1) [视频] 5.14手把手3:用户的注册接口(2) [视频] 5.15手把手4:用户的登录接口(1) [视频] 5.15手把手4:用户的登录接口(2) [视频] 5.16手把手5:发布新树洞接口(1) [视频] 5.16手把手5:发布新树洞接口(2) [视频] 5.17手把手6:获取所有树洞接口(1) [视频] 5.17手把手6:获取所有树洞接口(2) [视频] 5.18自主实操1:获取指定用户的所有树洞接口 [视频] 5.19手把手7:点赞接口 (1) [视频] 5.19手把手7:点赞接口 (2) [视频] 5.20自主实操2:删除指定树洞接口 [视频] 5.21手把手8:代码部署云服务器(1) [视频] 5.21手把手8:代码部署云服务器(2)(3)(4) [视频] 5.22手把手9:SVN进行代码的版本控制(MAC)(1) [视频] 5.22手把手9:SVN进行代码的版本控制(MAC)(2) [视频] 5.23手把手9:SVN进行代码的版本控制(Windows)(1) [视频] 5.23手把手9:SVN进行代码的版本控制(Windows)(2) [视频] 5.24课后小练 [视频]
2021-09-15 - 云开发日期型字段的比较
不要直接对比字符串,应该将时间字段转换成字符串类型再进行对比。 比如: db.collection("rideRecords") .aggregate() .match({ 'record.subLineId': 'l1', creationDate: _.gte('2022-01-30 00:00:00').and(_.lte('2022-01-30 23:59:59')), 'record.people._id': "381d149061ac0a5a00921a680d1281fe" }) .lookup({ from: "sublineRecords", localField: "_id", foreignField: "rideRecords", as: "sublineRecords" }) .lookup({ from: "driverRecords", localField: "sublineRecords.driverRecordID", foreignField: "_id", as: "driverRecords" }) .end() 上面语句,执行时返回空。 改成: db.collection("rideRecords") .aggregate() .addFields({ formatDate: $.dateToString({ date:'$creationDate', format:'%Y-%m-%d %H:%M:%S', timezone:'Asia/Shanghai' }) }) .match({ 'record.subLineId': 's4', formatDate:_.gte('2022-01-30 00:00:00').and(_.lte('2022-01-30 23:59:59')), }) .end() 关键点:把日期型通过格式转化:dateToString,转成字符类型再做比较 dateToString 相关文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/aggregate/AggregateCommand.dateToString.html 参考文档:https://www.jianshu.com/p/8da04042ffdd
2022-01-30 - 云函数设置TZ 为 Asia/Shanghai无效的解决方案(云函数时区问题)
[图片] 官方文档表明Node10以及Node15+版本可用环境变量:TZ 为 Asia/Shanghai,方式获取北京时间。其他Node版本需要借助第三方包,现给出第三方包方案。 云函数: //1.安装 moment-timezone npm install moment-timezone //2.编写云函数 const cloud = require('wx-server-sdk') const moment = require("moment-timezone") exports.main = async (event, context) => { let formatTime=moment().tz("Asia/Shanghai").format()//北京时间 2022-01-24T14:14:50+08:00 let hour=moment().tz("Asia/Shanghai").get('hour') //获取北京时间小时数 let minute=moment().tz("Asia/Shanghai").get('minute') //获取北京时间分数 let second=moment().tz("Asia/Shanghai").get('second') //获取北京时间秒数 return formatTime //result:{} 小程序中以result为载体 } //构建完毕重新部署上传云函数 js: //调用云函数 wx.cloud.callFunction({ name: "云函数名称", }).then(res => { //res 是云函数调用成功后return出来的值 console.log('云函数调用', res) }).catch(res => { console.log(res) })
2022-01-24 - 小程序生成酷炫二维码
0.引言 在小程序的业务中会有一些需要展示二维码的场景。静态二维码可以直接存放在本地,使用图片方式展示,但不适合根据用户相关信息生成动态的二维码。本文将介绍根据小程序的canvas能力绘制二维码。 1.方式一:通过wx-qr直接生成1.0 样例微信开发者工具打开 [图片] [图片] [图片] 1.1 安装# 通过 npm 安装 npm i wx-qr -S # 通过 yarn 安装 yarn add wx-qr 1.2 使用组件首先在你所开发的小程序根目录 [代码]app.json[代码] 或需要使用该组件的 [代码]xxx.json[代码] 中引用组件 (注意:请不要将组件命名为 [代码]wx-xxx[代码] 开头,可能会导致微信小程序解析 tag 失败 ) { "usingComponents": { "qr-container": "wx-qr" } } 之后就可以在 wxml 中直接使用组件 WXML <qr-container text="{{qrTxt}}" size="750"></qr-container> js Page({ data: { qrTxt: 'https://github.com/liuxdi/wx-qr', }, }); 当然,还可以支持很多种配置,详见github 或者 码云代码。 2.方式二:基于QRCode.js结合canvas绘制2.1 引入二维码数据生成库复制qrcode.js至你的小程序相应目录。 2.2 小程序中建立canvas标签,并给canvas设置长宽<canvas id="qr" type="2d" style="height: 750rpx;width: 750rpx;"></canvas> 2.3获取canvas实例及上下文const query = this.createSelectorQuery(); let dpr = wx.getSystemInfoSync().pixelRatio; query.select('#qr').fields({ node: true, size: true, id: true }) .exec((res) => { let { node: canvas, height, width } = res[0]; let ctx = canvas.getContext('2d'); canvas.width = width * dpr canvas.height = height * dpr ctx.scale(dpr, dpr); }) 2.4 定义一些变量及绘制二维码的数据区其中QRCodeModel是从qrCode.js中导入的 // 二维码的颜色 const colorDark = '#000'; // 获取二维码的大小,因css设置的为750rpx,将其转为px const rawViewportSize = getPxFromRpx(750); // 二维码容错率{ L: 1, M: 0, Q: 3, H: 2 } const correctLevel = 0; // 创建二维码实例对象,并添加数据进行生成 const qrCode = new QRCodeModel(-1, correctLevel); qrCode.addData(url); qrCode.make(); // 每个方向的二维码数量 const nCount = qrCode.moduleCount; // 计算每个二维码方块的大小 const nSize = getRoundNum(rawViewportSize / nCount, 3) // 每块二维码点的大小比例 const dataScale = 1; // 计算出dataScale不为1时,每个点的偏移值 const dataXyOffset = (1 - dataScale) * 0.5; for (let row = 0; row < nCount; row++) { for (let col = 0; col < nCount; col++) { // row 和 col 处的模块是否是黑色区 const bIsDark = qrCode.isDark(row, col); // 是否是二维码的图案定位标识区 Position Detection Pattern(如本模块,是三个顶点位置处的大方块) const isBlkPosCtr = (col < 8 && (row < 8 || row >= nCount - 8)) || (col >= nCount - 8 && row < 8); // 是否是Timing Patterns,也是用于协助定位扫描的 const isTiming = (row == 6 && col >= 8 && col <= nCount - 8) || (col == 6 && row >= 8 && row <= nCount - 8); // 如果是这些区域 则不进行绘制 let isProtected = isBlkPosCtr || isTiming; // 计算每个点的绘制位置(left,top) const nLeft = col * nSize + (isProtected ? 0 : dataXyOffset * nSize); const nTop = row * nSize + (isProtected ? 0 : dataXyOffset * nSize); // 描边色、线宽、填充色配置 ctx.strokeStyle = colorDark; ctx.lineWidth = 0.5; ctx.fillStyle = bIsDark ? colorDark : "rgba(255, 255, 255, 0.6)"; // 如果不是标识区,则进行绘制 if (!isProtected) { ctx.fillRect( nLeft, nTop, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize ); } } } 此时已经绘制出二维码的数据区: [图片] 2.5 绘制图形识别区// 绘制Position Detection Pattern ctx.fillStyle = colorDark; ctx.fillRect(0, 0, 7 * nSize, nSize); ctx.fillRect((nCount - 7) * nSize, 0, 7 * nSize, nSize); ctx.fillRect(0, 6 * nSize, 7 * nSize, nSize); ctx.fillRect((nCount - 7) * nSize, 6 * nSize, 7 * nSize, nSize); ctx.fillRect(0, (nCount - 7) * nSize, 7 * nSize, nSize); ctx.fillRect(0, (nCount - 7 + 6) * nSize, 7 * nSize, nSize); ctx.fillRect(0, 0, nSize, 7 * nSize); ctx.fillRect(6 * nSize, 0, nSize, 7 * nSize); ctx.fillRect((nCount - 7) * nSize, 0, nSize, 7 * nSize); ctx.fillRect((nCount - 7 + 6) * nSize, 0, nSize, 7 * nSize); ctx.fillRect(0, (nCount - 7) * nSize, nSize, 7 * nSize); ctx.fillRect(6 * nSize, (nCount - 7) * nSize, nSize, 7 * nSize); ctx.fillRect(2 * nSize, 2 * nSize, 3 * nSize, 3 * nSize); ctx.fillRect((nCount - 7 + 2) * nSize, 2 * nSize, 3 * nSize, 3 * nSize); ctx.fillRect(2 * nSize, (nCount - 7 + 2) * nSize, 3 * nSize, 3 * nSize); // 绘制Position Detection Pattern 完毕 // 绘制Timing Patterns const timingScale = 1; const timingXyOffset = (1 - timingScale) * 0.5; for (let i = 0; i < nCount - 8; i += 2) { _drawDot(ctx, 8 + i, 6, nSize, timingXyOffset, timingScale); _drawDot(ctx, 6, 8 + i, nSize, timingXyOffset, timingScale); } // 绘制Timing Patterns 完毕 这时候,一个朴素的二维码就绘制成功啦~ [图片] 具体代码详见代码片段 该代码只是提供了一个简单二维码的生成逻辑。若需要更复杂的二维码展示功能,还是建议使用wx-qr。欢迎各位提Issue和Star~~
2022-01-04 - 汉光百货
坐拥100万公众号粉丝和30万服务号粉丝的汉光百货,一直以来都在尝试将这块巨大的粉丝蛋糕,转化成直接消费力。 文章+H5商城,是他们想到的第一个办法,但是玩法过于单一,只能在公众号、服务号文章末位嵌入H5线上商城。由于链路过长、场景单一,用户体验非常糟糕。 彼时,汉光百货商务电子部产品总监董有良也正在思考更好的解决方案。凭借着自己在腾讯做产品经理时沉淀的互联网嗅觉,在张小龙提出“应用号”概念时,董有良敏锐地感觉“应用号”可能是个方向。 经过一年多的尝试,汉光百货推出了多款小程序,并借助小程序这个工具,打造了智慧零售模型:不仅优化了线上商城,还打通了线上线下,解决了线下购物场景的一些痛点。 01 单点突破,小步迭代 互联网产品有个基本的原则:“小步快跑,快速迭代”,就是不要想着一次性做出好的产品,而要通过快速迭代的方式进行更新,保证每一小步都跑得很快。 董有良深谙此道,所以,在设计小程序最初,董有良的想法很简单,就是找到一个场景“单点突破”,并通过小步迭代的方式优化产品,让产品接近完美。 但是,在场景设计上,汉光内部却出现了三个版本,他们分别是:会员卡、百货品牌导航、线上商城。 几番评估下来,董有良认为会员卡是最为合适的场景,主要原因在于: 1、会员卡轻量的服务,符合小程序即用即走的理念; 2、微信也在探索,并没有一下子放开很多能力,汉光只能在现有的能力做文章。 此时的汉光百货有几个迫切需要解决的痛点:用户在线上H5购物体验糟糕;而到了线下收银台,却常常要长时间排队。 董有良左思右想,决定用会员卡先消灭收银台长队,一边积累经验,一边观察小程序发展,以便未来更好的布局小程序。 [图片] 除了线下排队的痛点,汉光百货在会员卡创新上也遇到了一些难题:由于会员卡内含有优惠券、积分等信息,如果不加处理,被人截图滥用,这会损害用户和商场的利益,所以汉光百货希望通过定制的加密二维码来解决问题,但是,卡包并没有办法支持定制二维码。 但小程序很好的解决了这个问题。用户出示小程序里的定制二维码,收银员扫码,小程序可以通过接口调用唤起微信支付,用户只要输入密码并可以完成支付。 [图片] 通过会员卡小程序,汉光百货实现了一码支付功能,极大优化了用户体验和收银流程,也缩减了汉光百货的运营成本。 02 小程序+爆款将线上用户引流线下 会员卡是汉光百货使用小程序的初始体验,此后,尝到了甜头的董有良,对小程序的研究从未停止。 董有良观察到,随着微信对小程序能力释放不断加速,小程序摆脱了功能单一、入口很少的局限,摇身一变,既能实现APP操作的丝滑体验,又无需下载、即用即走,庞大的入口数量可以触达大量场景,充分连接了线上线下,时机成熟,汉光百货立刻推出了汉光百货+小程序,这也终结了汉光百货公众号+H5的线上商城时代。 [图片] 线上下单、线下自提,汉光百货+小程序延续了之前线上的特色玩法:每周推出一期闪购活动,通过公众号+小程序的方式推广,用户在小程序下单后,必须到店自提,并不支持邮寄,用户到线下品牌店只要出示二维码,导购打开企业微信的闪购提货应用,扫描用户的二维码,即可进行核销。 除了闪购,汉光百货还推出了微信试用、生日特惠等活动,均采取类似玩法,目的很简单,就是将线上用户引流线下场景。 [图片] 这背后是汉光百货对用户需求的深刻洞察: 1、闪购的品类一定要是爆品,并且要全网最低价格,比其他电商平台还要便宜很多; 2、每个闪购产品必须只能购买一件,给用户造成一种稀缺感,而做到上述两点,就很容易能够把用户从线上引向线下了。 线上商城闪购为什么一定要引导用户线下提货?董有良有自己的考虑:用户到了线下场景,产生连带消费的可能性就大,以汉光为例,此举引流到店的用户,连带率超过40%。 数据显示,小程序上线后,汉光百货的线上收入同比增长70%。经测算,通过闪购等玩法,预计每年将拉动10万以上用户进店,给汉光百货带来4000万元+的增量收入,这些数字未来还会不断上涨。 [图片] 此外,截止目前为止,汉光百货+上线一个月,已积累17万用户,留存率达47.4%。 03 小程序+企业微信完成服务大串联 董有良分享了一个小故事。 在一个暴雨天,商场客流骤减,导购通过汉光百货在企业微信上开发的手机开票应用给有顾客分享商品,顾客选中后,导购通过汉光百货自己开发的手机开票功能开单,顾客收到订单信息并付款后,导购提货并去商场邮件中心,选中订单打印运单,顾客实时收到物流信息提醒。整个流程通畅、合规。 这一天,汉光百货里的一家专柜仅用手机开票功能,就完成了当天80%的业绩。在此前,导购卖货给非店内客人是要通过加微信转账的。这样会存在两个问题: 1、没有商场、品牌背书,导购个人和顾客的交易,给顾客不安全感,成交率低; 2、流程复杂,用户体验差。用户选中商品→截图会员卡信息→微信转账,导购去收银台结账→提货→找快递发货→给用户发物流单号,用户自己查询物信息,步骤繁多。 但现在,用户直接通过手机开票应用完成购物交易,此后商品的物流信息也会及时反馈给用户。 [图片] 除了手机开票应用,汉光百货还通过闪购提货应用、导购对账等各种服务都放在了企业微信上,这样做的好处是,整个服务链条的角色方行为都留在了汉光系统里,便于汉光更好的服务用户,挖掘用户的需求。 [图片] 而这必须有一个前提是:汉光百货需要将品牌方导购直接搬到其企业微信上。 我们都知道,百货公司对品牌的导购并没有管理权,那么如何调动导购积极性呢? 董有良认为这是一个双赢的选择,导购愿意配合取决两个因素: 1、汉光通过闪购等玩法成功将用户引流到了线下; 2、线上商城部分,也给导购带来了订单,所以,导购的配合度高。 小程序矩阵+企业微信,为汉光百货积累了很多数字化经验,为后续智慧升级铺了一条好路,未来,汉光百货将继续围绕购物核心优化服务: 1、加强导购与顾客的链接,在不打扰顾客的前提下,进行加强沟通,增加销售; 2、开发扫码购,让顾客在活动大促等导购很忙的期间,可以自助扫码下单。 扫码体验“汉光百货+”小程序 [图片]
2020-01-17