lifanagju_citu 9 月之前
父節點
當前提交
f15bbe8206
共有 6 個文件被更改,包括 294 次插入0 次删除
  1. 2 0
      components.d.ts
  2. 1 0
      package.json
  3. 263 0
      src/components/QrCode/index.vue
  4. 18 0
      src/hooks/web/useDesign.js
  5. 6 0
      src/styles/global.module.scss
  6. 4 0
      src/styles/variables.scss

+ 2 - 0
components.d.ts

@@ -43,6 +43,8 @@ declare module 'vue' {
     Positions: typeof import('./src/components/Enterprise/components/positions.vue')['default']
     PreviewImg: typeof import('./src/components/PreviewImg/index.vue')['default']
     PublicRecruitment: typeof import('./src/components/publicRecruitment/index.vue')['default']
+    Qrcode: typeof import('./src/components/Qrcode/index.vue')['default']
+    QrCode: typeof import('./src/components/QrCode/index.vue')['default']
     RadioGroup: typeof import('./src/components/FormUI/radioGroup/index.vue')['default']
     Recharge: typeof import('./src/components/Recharge/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "pinia": "^2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",
     "pnpm": "^9.1.0",
+    "qrcode": "^1.5.4",
     "qs": "^6.12.1",
     "roboto-fontface": "*",
     "vue": "^3.4.0",

+ 263 - 0
src/components/QrCode/index.vue

@@ -0,0 +1,263 @@
+<script setup>
+import { computed, nextTick, ref, unref, watch } from 'vue'
+import QRCode from 'qrcode'
+import cloneDeep from 'lodash/cloneDeep'
+import { useDesign } from '@/hooks/web/useDesign'
+import { isString } from '@/utils/is'
+
+defineOptions({ name: 'qr-code' })
+
+const props = defineProps({
+  // img 或者 canvas,img不支持logo嵌套
+  tag: {
+    type: String,
+    default: 'canvas'
+  },
+  // 二维码内容
+  text: {
+    type: [String, Array],
+    default: null
+  },
+  // qrcode.js配置项
+  options: {
+    type: Object,
+    default: () => ({})
+  },
+  // 宽度
+  width: {
+    type: Number,
+    default: 200
+  },
+  // logo
+  logo: {
+    type: [String, Object],
+    default: ''
+  },
+  // 是否过期
+  disabled: {
+    type: Boolean,
+    default: false
+  },
+  // 过期提示内容
+  disabledText: {
+    type: String,
+    default: ''
+  },
+})
+
+const emit = defineEmits(['done', 'click', 'disabled-click'])
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('qrcode')
+
+const { toCanvas, toDataURL } = QRCode
+
+const loading = ref(true)
+
+const wrapRef = ref(null)
+
+const renderText = computed(() => String(props.text))
+
+const wrapStyle = computed(() => {
+  return {
+    width: props.width + 'px',
+    height: props.width + 'px'
+  }
+})
+
+const initQrcode = async () => {
+  await nextTick()
+  const options = cloneDeep(props.options || {})
+  if (props.tag === 'canvas') {
+    // 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
+    options.errorCorrectionLevel =
+      options.errorCorrectionLevel || getErrorCorrectionLevel(unref(renderText))
+    const _width = await getOriginWidth(unref(renderText), options)
+    options.scale = props.width === 0 ? undefined : (props.width / _width) * 4
+    const canvasRef = await toCanvas(
+      unref(wrapRef),
+      unref(renderText),
+      options
+    )
+    if (props.logo) {
+      const url = await createLogoCode(canvasRef)
+      emit('done', url)
+      loading.value = false
+    } else {
+      emit('done', canvasRef.toDataURL())
+      loading.value = false
+    }
+  } else {
+    const url = await toDataURL(renderText.value, {
+      errorCorrectionLevel: 'H',
+      width: props.width,
+      ...options
+    })
+    ;(unref(wrapRef)).src = url
+    emit('done', url)
+    loading.value = false
+  }
+}
+
+watch(
+  () => renderText.value,
+  (val) => {
+    if (!val) return
+    initQrcode()
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+const createLogoCode = (canvasRef) => {
+  const canvasWidth = canvasRef.width
+  const logoOptions = Object.assign(
+    {
+      logoSize: 0.15,
+      bgColor: '#ffffff',
+      borderSize: 0.05,
+      crossOrigin: 'anonymous',
+      borderRadius: 8,
+      logoRadius: 0
+    },
+    isString(props.logo) ? {} : props.logo
+  )
+  const {
+    logoSize = 0.15,
+    bgColor = '#ffffff',
+    borderSize = 0.05,
+    crossOrigin = 'anonymous',
+    borderRadius = 8,
+    logoRadius = 0
+  } = logoOptions
+  const logoSrc = isString(props.logo) ? props.logo : props.logo.src
+  const logoWidth = canvasWidth * logoSize
+  const logoXY = (canvasWidth * (1 - logoSize)) / 2
+  const logoBgWidth = canvasWidth * (logoSize + borderSize)
+  const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2
+
+  const ctx = canvasRef.getContext('2d')
+  if (!ctx) return
+
+  // logo 底色
+  canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius)
+  ctx.fillStyle = bgColor
+  ctx.fill()
+
+  // logo
+  const image = new Image()
+  if (crossOrigin || logoRadius) {
+    image.setAttribute('crossOrigin', crossOrigin)
+  }
+  ;(image).src = logoSrc
+
+  // 使用image绘制可以避免某些跨域情况
+  const drawLogoWithImage = (image) => {
+    ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
+  }
+
+  // 使用canvas绘制以获得更多的功能
+  const drawLogoWithCanvas = (image) => {
+    const canvasImage = document.createElement('canvas')
+    canvasImage.width = logoXY + logoWidth
+    canvasImage.height = logoXY + logoWidth
+    const imageCanvas = canvasImage.getContext('2d')
+    if (!imageCanvas || !ctx) return
+    imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
+
+    canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius)
+    if (!ctx) return
+    const fillStyle = ctx.createPattern(canvasImage, 'no-repeat')
+    if (fillStyle) {
+      ctx.fillStyle = fillStyle
+      ctx.fill()
+    }
+  }
+
+  // 将 logo绘制到 canvas上
+  return new Promise((resolve) => {
+    image.onload = () => {
+      logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image)
+      resolve(canvasRef.toDataURL())
+    }
+  })
+}
+
+// 得到原QrCode的大小,以便缩放得到正确的QrCode大小
+const getOriginWidth = async (content, options) => {
+  const _canvas = document.createElement('canvas')
+  await toCanvas(_canvas, content, options)
+  return _canvas.width
+}
+
+// 对于内容少的QrCode,增大容错率
+const getErrorCorrectionLevel = (content) => {
+  if (content.length > 36) {
+    return 'M'
+  } else if (content.length > 16) {
+    return 'Q'
+  } else {
+    return 'H'
+  }
+}
+
+// copy来的方法,用于绘制圆角
+const canvasRoundRect = (ctx) => {
+  return (x, y, w, h, r) => {
+    const minSize = Math.min(w, h)
+    if (r > minSize / 2) {
+      r = minSize / 2
+    }
+    ctx.beginPath()
+    ctx.moveTo(x + r, y)
+    ctx.arcTo(x + w, y, x + w, y + h, r)
+    ctx.arcTo(x + w, y + h, x, y + h, r)
+    ctx.arcTo(x, y + h, x, y, r)
+    ctx.arcTo(x, y, x + w, y, r)
+    ctx.closePath()
+    return ctx
+  }
+}
+
+const clickCode = () => {
+  emit('click')
+}
+
+const disabledClick = () => {
+  emit('disabled-click')
+}
+</script>
+
+<template>
+  <div v-loading="loading" :class="[prefixCls, 'relative inline-block']" :style="wrapStyle">
+    <component :is="tag" ref="wrapRef" @click="clickCode" />
+    <div
+      v-if="disabled"
+      :class="`${prefixCls}--disabled`"
+      class="absolute left-0 top-0 h-full w-full flex items-center justify-center"
+      @click="disabledClick"
+    >
+      <div class="absolute left-[50%] top-[50%] font-bold">
+        <Icon :size="30" color="var(--el-color-primary)" icon="ep:refresh-right" />
+        <div>{{ disabledText }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+// $prefix-cls: #{$namespace}-qrcode;
+
+// .#{$prefix-cls} {
+//   &--disabled {
+//     background: rgb(255 255 255 / 95%);
+
+//     & > div {
+//       transform: translate(-50%, -50%);
+//     }
+//   }
+// }
+</style>

+ 18 - 0
src/hooks/web/useDesign.js

@@ -0,0 +1,18 @@
+import variables from '@/styles/global.module.scss'
+
+export const useDesign = () => {
+  const scssVariables = variables
+
+  /**
+   * @param scope 类名
+   * @returns 返回空间名-类名
+   */
+  const getPrefixCls = (scope) => {
+    return `${scssVariables.namespace}-${scope}`
+  }
+
+  return {
+    variables: scssVariables,
+    getPrefixCls
+  }
+}

+ 6 - 0
src/styles/global.module.scss

@@ -0,0 +1,6 @@
+@import './variables.scss';
+// 导出变量
+:export {
+  namespace: $namespace;
+  elNamespace: $elNamespace;
+}

+ 4 - 0
src/styles/variables.scss

@@ -0,0 +1,4 @@
+// 命名空间
+$namespace: v;
+// el命名空间
+$elNamespace: el;