diff --git a/locales/en.yaml b/locales/en.yaml index d78665748..e3915040f 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -79,3 +79,4 @@ menus: hsDebounce: Debounce & Throttle hsFormDesign: Form Design hsBarcode: Barcode + hsQarcode: Qarcode diff --git a/locales/zh-CN.yaml b/locales/zh-CN.yaml index 17059726c..8748e8cfb 100644 --- a/locales/zh-CN.yaml +++ b/locales/zh-CN.yaml @@ -79,3 +79,4 @@ menus: hsDebounce: 防抖节流 hsFormDesign: 表单设计器 hsBarcode: 条形码 + hsQarcode: 二维码 diff --git a/package.json b/package.json index 957db9f58..8f60dde38 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "nprogress": "^0.2.0", "path": "^0.12.7", "pinia": "^2.0.13", + "qrcode": "^1.5.0", "qs": "^6.10.1", "resize-observer-polyfill": "^1.5.1", "responsive-storage": "^1.0.11", @@ -93,6 +94,7 @@ "@types/mockjs": "1.0.3", "@types/node": "14.14.14", "@types/nprogress": "0.2.0", + "@types/qrcode": "^1.4.2", "@types/qs": "^6.9.7", "@typescript-eslint/eslint-plugin": "^5.10.2", "@typescript-eslint/parser": "^5.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80590bb41..71871fb0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,7 @@ specifiers: "@types/mockjs": 1.0.3 "@types/node": 14.14.14 "@types/nprogress": 0.2.0 + "@types/qrcode": ^1.4.2 "@types/qs": ^6.9.7 "@typescript-eslint/eslint-plugin": ^5.10.2 "@typescript-eslint/parser": ^5.10.2 @@ -73,6 +74,7 @@ specifiers: postcss-scss: ^4.0.3 prettier: ^2.5.1 pretty-quick: 3.1.1 + qrcode: ^1.5.0 qs: ^6.10.1 resize-observer-polyfill: ^1.5.1 responsive-storage: ^1.0.11 @@ -137,6 +139,7 @@ dependencies: nprogress: 0.2.0 path: 0.12.7 pinia: 2.0.13_typescript@4.6.3+vue@3.2.33 + qrcode: 1.5.0 qs: 6.10.3 resize-observer-polyfill: 1.5.1 responsive-storage: 1.0.11_vue@3.2.33 @@ -174,6 +177,7 @@ devDependencies: "@types/mockjs": 1.0.3 "@types/node": 14.14.14 "@types/nprogress": 0.2.0 + "@types/qrcode": 1.4.2 "@types/qs": 6.9.7 "@typescript-eslint/eslint-plugin": 5.16.0_bc68a9cd5bf604202498b1a9faaf9387 "@typescript-eslint/parser": 5.16.0_eslint@8.11.0+typescript@4.6.3 @@ -1493,6 +1497,15 @@ packages: } dev: true + /@types/qrcode/1.4.2: + resolution: + { + integrity: sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ== + } + dependencies: + "@types/node": 14.14.14 + dev: true + /@types/qs/6.9.7: resolution: { @@ -2476,7 +2489,6 @@ packages: integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== } engines: { node: ">=8" } - dev: true /ansi-styles/3.2.1: resolution: @@ -2495,7 +2507,6 @@ packages: engines: { node: ">=8" } dependencies: color-convert: 2.0.1 - dev: true /ant-design-vue/3.2.0_vue@3.2.33: resolution: @@ -2763,7 +2774,6 @@ packages: integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== } engines: { node: ">=6" } - dev: true /camelcase/6.3.0: resolution: @@ -2872,6 +2882,17 @@ packages: string-width: 4.2.3 dev: true + /cliui/6.0.0: + resolution: + { + integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + } + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + /cliui/7.0.4: resolution: { @@ -2914,7 +2935,6 @@ packages: engines: { node: ">=7.0.0" } dependencies: color-name: 1.1.4 - dev: true /color-name/1.1.3: resolution: { integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= } @@ -3394,7 +3414,6 @@ packages: /decamelize/1.2.0: resolution: { integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= } engines: { node: ">=0.10.0" } - dev: true /deep-is/0.1.4: resolution: @@ -3427,6 +3446,13 @@ packages: engines: { node: ">=0.3.1" } dev: true + /dijkstrajs/1.0.2: + resolution: + { + integrity: sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg== + } + dev: false + /dir-glob/3.0.1: resolution: { @@ -3603,7 +3629,13 @@ packages: { integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== } - dev: true + + /encode-utf8/1.0.3: + resolution: + { + integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== + } + dev: false /encodeurl/1.0.2: resolution: { integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= } @@ -4435,7 +4467,6 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - dev: true /find-up/5.0.0: resolution: @@ -4590,7 +4621,6 @@ packages: integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== } engines: { node: 6.* || 8.* || >= 10.* } - dev: true /get-intrinsic/1.1.1: resolution: @@ -5029,7 +5059,6 @@ packages: integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== } engines: { node: ">=8" } - dev: true /is-glob/4.0.3: resolution: @@ -5373,7 +5402,6 @@ packages: engines: { node: ">=8" } dependencies: p-locate: 4.1.0 - dev: true /locate-path/6.0.0: resolution: @@ -5937,7 +5965,6 @@ packages: engines: { node: ">=6" } dependencies: p-try: 2.2.0 - dev: true /p-limit/3.1.0: resolution: @@ -5957,7 +5984,6 @@ packages: engines: { node: ">=8" } dependencies: p-limit: 2.3.0 - dev: true /p-locate/5.0.0: resolution: @@ -5985,7 +6011,6 @@ packages: integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== } engines: { node: ">=6" } - dev: true /parent-module/1.0.1: resolution: @@ -6024,7 +6049,6 @@ packages: integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== } engines: { node: ">=8" } - dev: true /path-is-absolute/1.0.1: resolution: { integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18= } @@ -6117,6 +6141,14 @@ packages: semver-compare: 1.0.0 dev: true + /pngjs/5.0.0: + resolution: + { + integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + } + engines: { node: ">=10.13.0" } + dev: false + /popmotion/11.0.3: resolution: { @@ -6679,6 +6711,20 @@ packages: engines: { node: ">=0.6.0", teleport: ">=0.2.0" } dev: true + /qrcode/1.5.0: + resolution: + { + integrity: sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ== + } + engines: { node: ">=10.13.0" } + hasBin: true + dependencies: + dijkstrajs: 1.0.2 + encode-utf8: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + dev: false + /qs/6.10.3: resolution: { @@ -6785,7 +6831,6 @@ packages: /require-directory/2.1.1: resolution: { integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I= } engines: { node: ">=0.10.0" } - dev: true /require-from-string/2.0.2: resolution: @@ -6795,6 +6840,13 @@ packages: engines: { node: ">=0.10.0" } dev: true + /require-main-filename/2.0.0: + resolution: + { + integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + } + dev: false + /resize-observer-polyfill/1.5.1: resolution: { @@ -7013,6 +7065,10 @@ packages: lru-cache: 6.0.0 dev: true + /set-blocking/2.0.0: + resolution: { integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc= } + dev: false + /shallow-equal/1.2.1: resolution: { @@ -7274,7 +7330,6 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true /string_decoder/1.3.0: resolution: @@ -7305,7 +7360,6 @@ packages: engines: { node: ">=8" } dependencies: ansi-regex: 5.0.1 - dev: true /strip-final-newline/2.0.0: resolution: @@ -8119,6 +8173,10 @@ packages: loose-envify: 1.4.0 dev: false + /which-module/2.0.0: + resolution: { integrity: sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= } + dev: false + /which/1.3.1: resolution: { @@ -8171,7 +8229,6 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true /wrap-ansi/7.0.0: resolution: @@ -8234,6 +8291,13 @@ packages: xgplayer-subtitles: 1.0.22 dev: false + /y18n/4.0.3: + resolution: + { + integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + } + dev: false + /y18n/5.0.8: resolution: { @@ -8268,6 +8332,17 @@ packages: engines: { node: ">= 6" } dev: true + /yargs-parser/18.1.3: + resolution: + { + integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + } + engines: { node: ">=6" } + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + /yargs-parser/20.2.9: resolution: { @@ -8284,6 +8359,26 @@ packages: engines: { node: ">=12" } dev: true + /yargs/15.4.1: + resolution: + { + integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + } + engines: { node: ">=8" } + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.0 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: false + /yargs/17.4.0: resolution: { diff --git a/src/components/ReQrcode/index.ts b/src/components/ReQrcode/index.ts new file mode 100644 index 000000000..a5a99ca4f --- /dev/null +++ b/src/components/ReQrcode/index.ts @@ -0,0 +1,10 @@ +import { App } from "vue"; +import reQrcode from "./src/index"; + +export const ReQrcode = Object.assign(reQrcode, { + install(app: App) { + app.component(reQrcode.name, reQrcode); + } +}); + +export default ReQrcode; diff --git a/src/components/ReQrcode/src/index.scss b/src/components/ReQrcode/src/index.scss new file mode 100644 index 000000000..5f6a3ff3c --- /dev/null +++ b/src/components/ReQrcode/src/index.scss @@ -0,0 +1,8 @@ +.qrcode { + &--disabled { + background: rgba(255, 255, 255, 0.95); + & > div { + transform: translate(-50%, -50%); + } + } +} diff --git a/src/components/ReQrcode/src/index.tsx b/src/components/ReQrcode/src/index.tsx new file mode 100644 index 000000000..3c9edb401 --- /dev/null +++ b/src/components/ReQrcode/src/index.tsx @@ -0,0 +1,262 @@ +import { + ref, + unref, + watch, + nextTick, + computed, + PropType, + defineComponent +} from "vue"; +import "./index.scss"; +import { isString } from "/@/utils/is"; +import { cloneDeep } from "lodash-unified"; +import { propTypes } from "/@/utils/propTypes"; +import { IconifyIconOffline } from "../../ReIcon"; +import QRCode, { QRCodeRenderersOptions } from "qrcode"; + +interface QrcodeLogo { + src?: string; + logoSize?: number; + bgColor?: string; + borderSize?: number; + crossOrigin?: string; + borderRadius?: number; + logoRadius?: number; +} + +const props = { + // img 或者 canvas,img不支持logo嵌套 + tag: propTypes.string + .validate((v: string) => ["canvas", "img"].includes(v)) + .def("canvas"), + // 二维码内容 + text: { + type: [String, Array] as PropType, + default: null + }, + // qrcode.js配置项 + options: { + type: Object as PropType, + default: (): QRCodeRenderersOptions => ({}) + }, + // 宽度 + width: propTypes.number.def(200), + // logo + logo: { + type: [String, Object] as PropType | string>, + default: (): QrcodeLogo | string => "" + }, + // 是否过期 + disabled: propTypes.bool.def(false), + // 过期提示内容 + disabledText: propTypes.string.def("") +}; + +export default defineComponent({ + name: "epTableProBar", + props, + emits: ["done", "click", "disabled-click"], + setup(props, { emit }) { + 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: number = await getOriginWidth(unref(renderText), options); + options.scale = + props.width === 0 ? undefined : (props.width / _width) * 4; + const canvasRef: HTMLCanvasElement = 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()); + }; + }); + }; + // 得到原QrCode的大小,以便缩放得到正确的QrCode大小 + 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"; + } + }; + // 用于绘制圆角 + 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"); + }; + return () => ( + <> +
+ {props.tag === "canvas" ? ( + + ) : ( + + )} + {props.disabled && ( +
+
+ +
{props.disabledText}
+
+
+ )} +
+ + ); + } +}); diff --git a/src/router/modules/able.ts b/src/router/modules/able.ts index e8087b94a..8c73ee1ad 100644 --- a/src/router/modules/able.ts +++ b/src/router/modules/able.ts @@ -110,6 +110,15 @@ const ableRouter = { title: $t("menus.hsBarcode"), i18n: true } + }, + { + path: "/able/qrcode", + name: "reQarcode", + component: () => import("/@/views/able/qrcode.vue"), + meta: { + title: $t("menus.hsQarcode"), + i18n: true + } } ] }; diff --git a/src/views/able/qrcode.vue b/src/views/able/qrcode.vue new file mode 100644 index 000000000..99fc75bd4 --- /dev/null +++ b/src/views/able/qrcode.vue @@ -0,0 +1,113 @@ + + +