浏览代码

feat: 刮刮卡、签名组件,暂未适配 taro 转小程序 (#630)

* fix: 优化calender滑动

* fix: 修复小程序上calendar 样式问题

* fix: 修复calendar样式

* feat: 添加签名组件

* feat: 新加刮刮卡组件

Co-authored-by: guoxiaoxiao8 <guoxiao158@126.com>
郭晓 4 年之前
父节点
当前提交
1f668ba629

+ 20 - 0
src/config.json

@@ -779,6 +779,26 @@
           "sort": 2,
           "show": true,
           "author": "Ymm0008"
+        },
+        {
+          "version": "3.0.0",
+          "name": "Signature",
+          "type": "component",
+          "cName": "签名",
+          "desc": "签名组件",
+          "sort": 3,
+          "show": true,
+          "author": "guoxiaoxiao"
+        },
+        {
+          "version": "3.0.0",
+          "name": "Luckycard",
+          "type": "component",
+          "cName": "刮刮卡",
+          "desc": "适合抽奖等小游戏",
+          "sort": 4,
+          "show": true,
+          "author": "guoxiaoxiao8"
         }
       ]
     }

+ 44 - 0
src/packages/__VUE/luckycard/demo.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="demo">
+    <h2>基本用法</h2>
+    <nut-luckycard content="1000万"></nut-luckycard>
+    <h2>内容异步</h2>
+    <nut-luckycard :content="val"></nut-luckycard>
+    <h2>刮开层和背景层都支持自定义颜色,奖品信息支持HTML</h2>
+    <nut-luckycard
+      coverColor="#F9CC9D"
+      backgroundColor="#C3D08B"
+      content="<em>1000<em><strong>元</strong>"
+    ></nut-luckycard>
+    <h2>刮开层支持图片</h2>
+    <nut-luckycard content="1000万" :coverImg="coverImage"></nut-luckycard>
+    <h2>事件回调</h2>
+    <nut-luckycard content="1000万" @open="opencard"></nut-luckycard>
+    <h2>设置刮开比列</h2>
+    <nut-luckycard
+      content="1000万"
+      @open="opencard"
+      ratio="0.2"
+    ></nut-luckycard>
+    <p></p>
+  </div>
+</template>
+
+<script lang="ts">
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('luckycard');
+export default createDemo({
+  props: {},
+  setup() {
+    const opencard = () => {
+      alert('刮开事件完成');
+    };
+    return { opencard };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.demo {
+}
+</style>

+ 88 - 0
src/packages/__VUE/luckycard/doc.md

@@ -0,0 +1,88 @@
+#  luckycard组件
+
+### 介绍
+    
+基于 canvas 的一个刮刮卡组件
+    
+### 安装
+ ``` javascript
+import { createApp } from 'vue';
+// vue
+import { Luckcard} from '@nutui/nutui';
+
+const app = createApp();
+app.use(Luckcard);
+
+```   
+    
+ ## 基本用法
+
+```html
+<nut-luckycard 
+content="1000万"
+></nut-luckycard>
+```
+## 异步数据
+
+```html
+<nut-luckycard 
+:content="val"
+></nut-luckycard>
+```
+## 支持自定义颜色
+
+```html
+<nut-luckycard 
+coverColor="#F9CC9D" 
+backgroundColor="#C3D08B" 
+content="<em>1000<em><strong>元</strong>"
+></nut-luckycard>
+```
+## 支持背景是图片
+
+```html
+<nut-luckycard 
+content="1000万" 
+:coverImg="coverImage"
+></nut-luckycard>
+```
+
+## 事件回调
+
+```html
+<nut-luckycard 
+content="1000万"
+@open="opencard"
+></nut-luckycard>
+```
+## 设置刮开比例
+
+```html
+<nut-luckycard 
+content="1000万"
+@open="opencard"
+ratio="0.2"
+></nut-luckycard>
+```
+
+## Prop
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| content | 奖项信息,支持html | String | ''
+| height | 卡片高度 | String | 50px
+| width | 卡片高度 | String | 300px
+| coverColor | 刮开层颜色 | String | ''
+| coverImg | 刮开层是图片(不支持跨域。设置此项后coverColor失效) | String | ''
+| fontSize | 中奖信息字号 | String | 20px
+| backgroundColor | 内容层背景颜色 | String | '#FFFFFF'
+| ratio | 触发事件的刮开比 | Number |0.8(介于0-1之间)
+
+## 事件
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| open | 刮开后回调函数 | function | ''
+
+
+

+ 17 - 0
src/packages/__VUE/luckycard/index.scss

@@ -0,0 +1,17 @@
+.nut-luckycard {
+  position: relative;
+  .nut-cover {
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+  .lucky-content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    width: 100%;
+    line-height: 100%;
+    user-select: none;
+  }
+}

+ 94 - 0
src/packages/__VUE/luckycard/index.vue

@@ -0,0 +1,94 @@
+<template>
+  <div
+    class="nut-luckycard"
+    :style="{ height: height + 'px', width: width + 'px' }"
+    ref="luckycarddom"
+    id="luckycarddom"
+  >
+    <div
+      class="lucky-content"
+      v-html="content"
+      :style="{ backgroundColor: backgroundColor, fontSize: fontSize + 'px' }"
+    ></div>
+  </div>
+</template>
+<script lang="ts">
+import { ref, nextTick, onMounted, reactive } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('luckycard');
+import LuckyCard from './luckycard.js';
+export default create({
+  props: {
+    content: {
+      type: String,
+      default: ''
+    },
+    height: {
+      type: [String, Number],
+      default: 50
+    },
+    width: {
+      type: [String, Number],
+      default: 300
+    },
+    coverColor: {
+      type: String,
+      default: '#C5C5C5'
+    },
+    coverImg: {
+      type: String,
+      default: ''
+    },
+    fontSize: {
+      type: [String, Number],
+      default: 20
+    },
+    backgroundColor: {
+      type: String,
+      default: '#FFFFFF'
+    },
+    ratio: {
+      type: [String, Number],
+      default: 0.5
+    }
+  },
+  components: {},
+  emits: ['click', 'open'],
+
+  setup(props, { emit }) {
+    const luckycarddom = ref<HTMLElement | null>(null);
+    const state = reactive({
+      luckcard: null
+    });
+    const clearCover = () => {
+      state.luckcard.clearCover();
+    };
+    onMounted(() => {
+      let dom = document.getElementById('luckycarddom');
+      nextTick(() => {
+        console.log(LuckyCard);
+        state.luckcard = LuckyCard(
+          {
+            scratchDiv: luckycarddom.value,
+            coverColor: props.coverColor,
+            coverImg: props.coverImg,
+            ratio: Number(props.ratio),
+            callback: function () {
+              //console.log(this);
+              clearCover();
+              emit('open', this);
+            }
+          },
+          null
+        ) as any;
+      });
+    });
+
+    return { luckycarddom };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 219 - 0
src/packages/__VUE/luckycard/luckycard.js

@@ -0,0 +1,219 @@
+/*
+ * lucky-card.js - Scratch CARDS based on HTML5 Canvas
+ *
+ * Copyright (c) 2015 Frans Lee dmon@foxmail.com
+ *
+ * Licensed under the MIT license:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *
+ * Version:  1.0.3
+ */
+'use strict';
+
+/**
+ * Instantiate parameters
+ *
+ * @constructor
+ */
+function LuckyCard(settings, callback) {
+  this.cover = null;
+  this.ctx = null;
+  this.scratchDiv = settings.scratchDiv;
+  this.cardDiv = null;
+  this.cHeight = 0;
+  this.cWidth = 0;
+  this.supportTouch = false;
+  this.events = [];
+  this.startEventHandler = null;
+  this.moveEventHandler = null;
+  this.endEventHandler = null;
+
+  this.opt = {
+    coverColor: '#C5C5C5',
+    coverImg: '',
+    ratio: 0.8,
+    callback: null
+  };
+
+  this.init(settings, callback);
+}
+
+function _calcArea(ctx, callback, ratio) {
+  var pixels = ctx.getImageData(0, 0, this.cWidth, this.cHeight);
+  var transPixels = [];
+  _forEach(pixels.data, function (item, i) {
+    var pixel = pixels.data[i + 3];
+    if (pixel === 0) {
+      transPixels.push(pixel);
+    }
+  });
+
+  if (transPixels.length / pixels.data.length > ratio) {
+    callback && typeof callback === 'function' && callback();
+  }
+}
+
+function _forEach(items, callback) {
+  return Array.prototype.forEach.call(items, function (item, idx) {
+    callback(item, idx);
+  });
+}
+
+function _isCanvasSupported() {
+  var elem = document.createElement('canvas');
+  return !!(elem.getContext && elem.getContext('2d'));
+}
+
+/**
+ * touchstart/mousedown event handler
+ */
+function _startEventHandler(event) {
+  event.preventDefault();
+  this.moveEventHandler = _moveEventHandler.bind(this);
+  this.cover.addEventListener(this.events[1], this.moveEventHandler, false);
+  this.endEventHandler = _endEventHandler.bind(this);
+  document.addEventListener(this.events[2], this.endEventHandler, false);
+}
+
+/**
+ * touchmove/mousemove event handler
+ */
+function _moveEventHandler(event) {
+  event.preventDefault();
+  var evt = this.supportTouch ? event.touches[0] : event;
+  var coverPos = this.cover.getBoundingClientRect();
+  var pageScrollTop =
+    document.documentElement.scrollTop || document.body.scrollTop;
+  var pageScrollLeft =
+    document.documentElement.scrollLeft || document.body.scrollLeft;
+  var mouseX = evt.pageX - coverPos.left - pageScrollLeft;
+  var mouseY = evt.pageY - coverPos.top - pageScrollTop;
+
+  this.ctx.beginPath();
+  this.ctx.fillStyle = '#FFFFFF';
+  this.ctx.globalCompositeOperation = 'destination-out';
+  this.ctx.arc(mouseX, mouseY, 10, 0, 2 * Math.PI);
+  this.ctx.fill();
+}
+
+/**
+ * touchend/mouseup event handler
+ */
+function _endEventHandler(event) {
+  event.preventDefault();
+  if (this.opt.callback && typeof this.opt.callback === 'function')
+    _calcArea.call(this, this.ctx, this.opt.callback, this.opt.ratio);
+  this.cover.removeEventListener(this.events[1], this.moveEventHandler, false);
+  document.removeEventListener(this.events[2], this.endEventHandler, false);
+}
+
+/**
+ * Create Canvas element
+ */
+LuckyCard.prototype.createCanvas = function () {
+  this.cover = document.createElement('canvas');
+  this.cover.className = 'nut-cover';
+  this.cover.height = this.cHeight;
+  this.cover.width = this.cWidth;
+  this.ctx = this.cover.getContext('2d');
+  if (this.opt.coverImg) {
+    var _this = this;
+    var coverImg = new Image();
+    coverImg.src = this.opt.coverImg;
+    coverImg.onload = function () {
+      _this.ctx.drawImage(
+        coverImg,
+        0,
+        0,
+        _this.cover.width,
+        _this.cover.height
+      );
+    };
+  } else {
+    this.ctx.fillStyle = this.opt.coverColor;
+    this.ctx.fillRect(0, 0, this.cover.width, this.cover.height);
+  }
+  this.scratchDiv.appendChild(this.cover);
+  this.cardDiv.style.opacity = 1;
+};
+
+/**
+ * To detect whether support touch events
+ */
+LuckyCard.prototype.eventDetect = function () {
+  if ('ontouchstart' in window) this.supportTouch = true;
+  this.events = this.supportTouch
+    ? ['touchstart', 'touchmove', 'touchend']
+    : ['mousedown', 'mousemove', 'mouseup'];
+  this.addEvent();
+};
+
+/**
+ * Add touchstart/mousedown event listener
+ */
+LuckyCard.prototype.addEvent = function () {
+  this.startEventHandler = _startEventHandler.bind(this);
+  this.cover.addEventListener(this.events[0], this.startEventHandler, false);
+};
+
+/**
+ * Clear pixels of canvas
+ */
+LuckyCard.prototype.clearCover = function () {
+  this.ctx.clearRect(0, 0, this.cover.width, this.cover.height);
+  this.cover.removeEventListener(this.events[0], this.startEventHandler);
+  this.cover.removeEventListener(this.events[1], this.moveEventHandler);
+  this.cover.removeEventListener(this.events[2], this.endEventHandler);
+};
+
+/**
+ * LuckyCard initializer
+ *
+ * @param {Object} settings  Settings for LuckyCard
+ * @param {function} callback  callback function
+ */
+LuckyCard.prototype.init = function (settings, callback) {
+  if (!_isCanvasSupported()) {
+    alert('对不起,当前浏览器不支持Canvas,无法使用本控件!');
+    return;
+  }
+  var _this = this;
+  _forEach(arguments, function (item) {
+    if (typeof item === 'object') {
+      for (var k in item) {
+        if (k === 'callback' && typeof item[k] === 'function') {
+          _this.opt.callback = item[k].bind(_this);
+        } else {
+          k in _this.opt && (_this.opt[k] = item[k]);
+        }
+      }
+    } else if (typeof item === 'function') {
+      _this.opt.callback = item.bind(_this);
+    }
+  });
+  /* this.scratchDiv = document.getElementById('scratch');*/
+  if (!this.scratchDiv) {
+    return;
+  }
+  this.cardDiv = this.scratchDiv.querySelector('.lucky-content');
+  if (!this.cardDiv) {
+    return;
+  }
+  this.cHeight = this.cardDiv.clientHeight;
+  this.cWidth = this.cardDiv.clientWidth;
+  this.cardDiv.style.opacity = 0;
+  this.createCanvas();
+  this.eventDetect();
+};
+
+/**
+ * To generate an instance of object
+ *
+ * @param {Object} settings  Settings for LuckyCard
+ * @param {function} callback  callback function
+ */
+LuckyCard.case = function (settings, callback) {
+  return new LuckyCard(settings, callback);
+};
+
+export default LuckyCard.case;

+ 49 - 0
src/packages/__VUE/signature/demo.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <div>
+      <nut-signature @confirm="confirm" @clear="clear"></nut-signature>
+      <p class="demo-tips demo1">Tips: 点击确认按钮,下方显示签名图片</p>
+    </div>
+    <h2>修改颜色和签字粗细</h2>
+    <div>
+      <nut-signature
+        :lineWidth="lineWidth"
+        :strokeStyle="strokeStyle"
+      ></nut-signature>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { reactive } from 'vue';
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('signature');
+export default createDemo({
+  props: {},
+  setup() {
+    const state = reactive({
+      lineWidth: 4,
+      strokeStyle: 'green'
+    });
+
+    const confirm = (canvas: any, data: any) => {
+      let img = document.createElement('img');
+      img.src = data;
+      document.querySelector('.demo1').appendChild(img);
+    };
+    return { ...state, confirm };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.demo {
+  display: flex;
+  flex-direction: column;
+  .demo-tips {
+    font-size: 12px;
+    color: #666;
+  }
+}
+</style>

+ 81 - 0
src/packages/__VUE/signature/doc.md

@@ -0,0 +1,81 @@
+#  signature组件
+
+### 介绍
+    
+    基于Canvas的签名组件。
+    
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+// vue
+import { Signature} from '@nutui/nutui';
+
+const app = createApp();
+app.use(Signature);
+
+```
+    
+    
+## 代码演示
+    
+### 基础用法1
+    
+```html
+<nut-signature  
+    @confirm="confirm" 
+    @clear="clear"
+></nut-signature>
+<p class="demo-tips demo">Tips: 点击确认按钮,下方显示签名图片</p>
+```
+```javascript
+export default {
+    data(){
+        return{
+        }
+    },
+    methods:{
+        confirm(canvas, data) {
+            let img = document.createElement('img');
+            img.src = data;
+            document.querySelector('.demo').appendChild(img);
+        },
+
+        clear() {
+            let img = document.querySelector('.demo img'); 
+            if (img) {
+                img.remove();
+            }
+        }
+    }
+}
+```
+
+### 修改颜色和签字粗细
+
+```html
+<nut-signature  
+    :lineWidth="lineWidth" 
+    :strokeStyle="strokeStyle"
+></nut-signature>
+
+```
+    
+## API
+    
+### Props
+    
+| 参数 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| custom-class | 自定义class | String | -
+| line-width | 线条的宽度 | Number | 3
+| stroke-style | 绘图笔触颜色 | String | '#000'
+| type | 图片格式 | String | 'png'
+| un-support-tpl | 不支持Canvas情况下的展示文案 | String | '对不起,当前浏览器不支持Canvas,无法使用本控件!'
+
+## Event
+
+| 字段 | 说明 | 回调参数 
+|----- | ----- | ----- 
+| confirm | 点击确认按钮触发事件回调函数 | canvas和签名图片展示的 data URI
+| clear | 点击重签按钮触发事件回调函数 | 无

+ 14 - 0
src/packages/__VUE/signature/index.scss

@@ -0,0 +1,14 @@
+.nut-signature {
+  .nut-signature-inner {
+    height: 10rem;
+    margin-bottom: 1rem;
+    border: 1px solid #dadada;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .nut-signature-btn {
+    margin-right: 15px;
+  }
+}

+ 163 - 0
src/packages/__VUE/signature/index.vue

@@ -0,0 +1,163 @@
+<template>
+  <div :class="classes">
+    <div class="nut-signature-inner" ref="wrap">
+      <canvas
+        ref="canvas"
+        :height="canvasHeight"
+        :width="canvasWidth"
+        v-if="isCanvasSupported"
+      ></canvas>
+      <p class="nut-signature-unsopport" v-else>{{ unSupportTpl }}</p>
+    </div>
+
+    <nut-button class="nut-signature-btn" type="default" @click="clear()"
+      >重签</nut-button
+    >
+    <nut-button class="nut-signature-btn" type="primary" @click="confirm()"
+      >确认</nut-button
+    >
+  </div>
+</template>
+<script lang="ts">
+import { ref, reactive, onMounted, computed } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('signature');
+
+export default create({
+  props: {
+    customClass: {
+      type: String,
+      default: ''
+    },
+    lineWidth: {
+      type: Number,
+      default: 2
+    },
+    strokeStyle: {
+      type: String,
+      default: '#000'
+    },
+    type: {
+      type: String,
+      default: 'png'
+    },
+    unSupportTpl: {
+      type: String,
+      default: '对不起,当前浏览器不支持Canvas,无法使用本控件!'
+    }
+  },
+  components: {},
+  emits: ['confirm', 'clear'],
+
+  setup(props, { emit }) {
+    const canvas = ref<HTMLElement | null>(null);
+    const wrap = ref<HTMLElement | null>(null);
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true,
+        [`${props.customClass}`]: props.customClass
+      };
+    });
+    const state = reactive({
+      canvasHeight: 0,
+      canvasWidth: 0,
+      ctx: null,
+      isSupportTouch: 'ontouchstart' in window,
+      events:
+        'ontouchstart' in window
+          ? ['touchstart', 'touchmove', 'touchend', 'touchleave']
+          : ['mousedown', 'mousemove', 'mouseup', 'mouseleave']
+    });
+    const isCanvasSupported = () => {
+      let elem = document.createElement('canvas');
+      return !!(elem.getContext && elem.getContext('2d'));
+    };
+    const addEvent = () => {
+      canvas.value.addEventListener(state.events[0], startEventHandler, false);
+    };
+
+    const startEventHandler = (event: MouseEvent) => {
+      event.preventDefault();
+      state.ctx.beginPath();
+      state.ctx.lineWidth = props.lineWidth;
+      state.ctx.strokeStyle = props.strokeStyle;
+
+      canvas.value.addEventListener(state.events[1], moveEventHandler, false);
+      canvas.value.addEventListener(state.events[2], endEventHandler, false);
+      canvas.value.addEventListener(state.events[3], leaveEventHandler, false);
+    };
+
+    const moveEventHandler = (event) => {
+      event.preventDefault();
+
+      let evt = state.isSupportTouch ? event.touches[0] : event;
+      let coverPos = canvas.value.getBoundingClientRect();
+      let mouseX = evt.clientX - coverPos.left;
+      let mouseY = evt.clientY - coverPos.top;
+
+      state.ctx.lineTo(mouseX, mouseY);
+      state.ctx.stroke();
+    };
+
+    const endEventHandler = (event) => {
+      event.preventDefault();
+
+      canvas.value.removeEventListener(
+        state.events[1],
+        moveEventHandler,
+        false
+      );
+      canvas.value.removeEventListener(state.events[2], endEventHandler, false);
+    };
+    const leaveEventHandler = (event) => {
+      event.preventDefault();
+      canvas.value.removeEventListener(
+        state.events[1],
+        moveEventHandler,
+        false
+      );
+      canvas.value.removeEventListener(state.events[2], endEventHandler, false);
+    };
+    const clear = () => {
+      canvas.value.addEventListener(state.events[2], endEventHandler, false);
+      state.ctx.clearRect(0, 0, state.canvasWidth, state.canvasHeight);
+      state.ctx.closePath();
+      emit('clear');
+    };
+
+    const confirm = () => {
+      onSave(canvas.value);
+    };
+
+    const onSave = (canvas) => {
+      let dataurl;
+      switch (props.type) {
+        case 'png':
+          dataurl = canvas.toDataURL('image/png');
+          break;
+        case 'jpg':
+          dataurl = canvas.toDataURL('image/jpeg', 0.8);
+          break;
+      }
+      clear(true);
+      emit('confirm', canvas, dataurl);
+    };
+
+    onMounted(() => {
+      if (isCanvasSupported()) {
+        state.ctx = canvas.value.getContext('2d');
+        state.canvasWidth = wrap.value.offsetWidth;
+        state.canvasHeight = wrap.value.offsetHeight;
+        addEvent();
+      }
+    });
+
+    return { canvas, wrap, isCanvasSupported, confirm, clear, classes };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>