From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/components/Qrcode/src/Qrcode.vue |  253 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 253 insertions(+), 0 deletions(-)

diff --git a/src/components/Qrcode/src/Qrcode.vue b/src/components/Qrcode/src/Qrcode.vue
new file mode 100644
index 0000000..f0ce7b7
--- /dev/null
+++ b/src/components/Qrcode/src/Qrcode.vue
@@ -0,0 +1,253 @@
+<script lang="ts" setup>
+import { computed, nextTick, PropType, ref, unref, watch } from 'vue'
+import QRCode, { QRCodeRenderersOptions } from 'qrcode'
+import { cloneDeep } from 'lodash-es'
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+import { isString } from '@/utils/is'
+import { QrcodeLogo } from '@/types/qrcode'
+
+defineOptions({ name: 'Qrcode' })
+
+const props = defineProps({
+  // img 鎴栬�� canvas,img涓嶆敮鎸乴ogo宓屽
+  tag: propTypes.string.validate((v: string) => ['canvas', 'img'].includes(v)).def('canvas'),
+  // 浜岀淮鐮佸唴瀹�
+  text: {
+    type: [String, Array] as PropType<string | Recordable[]>,
+    default: null
+  },
+  // qrcode.js閰嶇疆椤�
+  options: {
+    type: Object as PropType<QRCodeRenderersOptions>,
+    default: () => ({})
+  },
+  // 瀹藉害
+  width: propTypes.number.def(200),
+  // logo
+  logo: {
+    type: [String, Object] as PropType<Partial<QrcodeLogo> | string>,
+    default: ''
+  },
+  // 鏄惁杩囨湡
+  disabled: propTypes.bool.def(false),
+  // 杩囨湡鎻愮ず鍐呭
+  disabledText: propTypes.string.def('')
+})
+
+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<Nullable<HTMLCanvasElement | HTMLImageElement>>(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: number = await getOriginWidth(unref(renderText), options)
+    options.scale = props.width === 0 ? undefined : (props.width / _width) * 4
+    const canvasRef: HTMLCanvasElement | any = await toCanvas(
+      unref(wrapRef) as HTMLCanvasElement,
+      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) as HTMLImageElement).src = url
+    emit('done', url)
+    loading.value = false
+  }
+}
+
+watch(
+  () => renderText.value,
+  (val) => {
+    if (!val) return
+    initQrcode()
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+const createLogoCode = (canvasRef: HTMLCanvasElement) => {
+  const canvasWidth = canvasRef.width
+  const logoOptions: QrcodeLogo = 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 as any).src = logoSrc
+
+  // 浣跨敤image缁樺埗鍙互閬垮厤鏌愪簺璺ㄥ煙鎯呭喌
+  const drawLogoWithImage = (image: HTMLImageElement) => {
+    ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
+  }
+
+  // 浣跨敤canvas缁樺埗浠ヨ幏寰楁洿澶氱殑鍔熻兘
+  const drawLogoWithCanvas = (image: HTMLImageElement) => {
+    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: any) => {
+    image.onload = () => {
+      logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image)
+      resolve(canvasRef.toDataURL())
+    }
+  })
+}
+
+// 寰楀埌鍘烸rCode鐨勫ぇ灏忥紝浠ヤ究缂╂斁寰楀埌姝g‘鐨凲rCode澶у皬
+const getOriginWidth = async (content: string, options: QRCodeRenderersOptions) => {
+  const _canvas = document.createElement('canvas')
+  await toCanvas(_canvas, content, options)
+  return _canvas.width
+}
+
+// 瀵逛簬鍐呭灏戠殑QrCode锛屽澶у閿欑巼
+const getErrorCorrectionLevel = (content: string) => {
+  if (content.length > 36) {
+    return 'M'
+  } else if (content.length > 16) {
+    return 'Q'
+  } else {
+    return 'H'
+  }
+}
+
+// copy鏉ョ殑鏂规硶锛岀敤浜庣粯鍒跺渾瑙�
+const canvasRoundRect = (ctx: CanvasRenderingContext2D) => {
+  return (x: number, y: number, w: number, h: number, r: number) => {
+    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>

--
Gitblit v1.8.0