Skip to content

Commit

Permalink
feat(qrcode): 新增二维码logo功能
Browse files Browse the repository at this point in the history
  • Loading branch information
anncwb committed Aug 11, 2020
1 parent a473da8 commit f1f2da1
Show file tree
Hide file tree
Showing 17 changed files with 361 additions and 59 deletions.
9 changes: 9 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Thank you for contributing to vue-vben-admin!

Please follow this steps:

1. Fork it ( https://github.com/anncwb/vue-vben-admin/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'feat(feat-name): Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
8 changes: 8 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Description

## Fix or Feature?

### Environment

- OS: Write here
- NPM Version: Write here
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"vuedraggable": "^2.24.0",
"vuex": "^3.4.0",
"vuex-module-decorators": "^0.17.0",
"xlsx": "^0.16.4"
"xlsx": "^0.16.4",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/register": "^7.10.5",
Expand Down
21 changes: 14 additions & 7 deletions src/components/file/src/FileDownload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ export function downloadByData(data: BlobPart, filename: string, mime?: string,
* 根据文件地址下载文件
* @param {*} sUrl
*/
export function downloadByUrl(sUrl: string, target: TargetContext = '_blank'): boolean {
export function downloadByUrl({
url,
target = '_blank',
fileName,
}: {
url: string;
target?: TargetContext;
fileName?: string;
}): boolean {
const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1;

Expand All @@ -40,12 +48,11 @@ export function downloadByUrl(sUrl: string, target: TargetContext = '_blank'): b
}
if (isChrome || isSafari) {
const link = document.createElement('a');
link.href = sUrl;
link.href = url;
link.target = target;

if (link.download !== undefined) {
const fileName = sUrl.substring(sUrl.lastIndexOf('/') + 1, sUrl.length);
link.download = fileName;
link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length);
}

if (document.createEvent) {
Expand All @@ -55,10 +62,10 @@ export function downloadByUrl(sUrl: string, target: TargetContext = '_blank'): b
return true;
}
}
if (sUrl.indexOf('?') === -1) {
sUrl += '?download';
if (url.indexOf('?') === -1) {
url += '?download';
}

window.open(sUrl, target);
window.open(url, target);
return true;
}
4 changes: 3 additions & 1 deletion src/components/file/src/UploadPreviewModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@
}
function handleDownload(file: UploadResult) {
downloadByUrl(file.url);
downloadByUrl({
url: file.url,
});
}
function renderFile() {
return state.fileList.map((file, index) => {
Expand Down
1 change: 1 addition & 0 deletions src/components/password-strength-meter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// import zxcvbn from 'zxcvbn';
2 changes: 2 additions & 0 deletions src/components/qrcode/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getAsyncComponent } from '@/common/factory/getAsyncComponent';

export const QrCode = getAsyncComponent(() => import('./src/index.vue'));

export * from './src/types';
28 changes: 28 additions & 0 deletions src/components/qrcode/src/drawCanvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { toCanvas, QRCodeRenderersOptions } from 'qrcode';
import { RenderQrCodeParams, ContentType } from './types';
export const renderQrCode = ({ canvas, content, width = 0, options = {} }: RenderQrCodeParams) => {
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
options.errorCorrectionLevel = options.errorCorrectionLevel || getErrorCorrectionLevel(content);

return getOriginWidth(content, options).then((_width: number) => {
options.scale = width === 0 ? undefined : (width / _width) * 4;
return toCanvas(canvas, content, options);
});
};

// 得到原QrCode的大小,以便缩放得到正确的QrCode大小
function getOriginWidth(content: ContentType, options: QRCodeRenderersOptions) {
const _canvas = document.createElement('canvas');
return toCanvas(_canvas, content, options).then(() => _canvas.width);
}

// 对于内容少的QrCode,增大容错率
function getErrorCorrectionLevel(content: ContentType) {
if (content.length > 36) {
return 'M';
} else if (content.length > 16) {
return 'Q';
} else {
return 'H';
}
}
89 changes: 89 additions & 0 deletions src/components/qrcode/src/drawLogo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { isString } from '@/utils/is';
import { RenderQrCodeParams, LogoType } from './types';
export const drawLogo = ({ canvas, logo }: RenderQrCodeParams) => {
if (!logo) {
return new Promise((resolve) => {
resolve((canvas as HTMLCanvasElement).toDataURL());
});
}

const canvasWidth = (canvas as HTMLCanvasElement).width;
const {
logoSize = 0.15,
bgColor = '#ffffff',
borderSize = 0.05,
crossOrigin,
borderRadius = 8,
logoRadius = 0,
} = logo as LogoType;

const logoSrc: string = isString(logo) ? logo : 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 = canvas.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 || 'anonymous');
}
image.src = logoSrc;

// 使用image绘制可以避免某些跨域情况
const drawLogoWithImage = (image: CanvasImageSource) => {
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) => {
image.onload = () => {
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
resolve((canvas as HTMLCanvasElement).toDataURL());
};
});
};

// copy来的方法,用于绘制圆角
function 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;
};
}
73 changes: 63 additions & 10 deletions src/components/qrcode/src/index.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
<script lang="tsx">
import { defineComponent, watchEffect, PropOptions, ref, unref } from 'compatible-vue';
import { toCanvas, QRCodeOptions, toDataURL } from 'qrcode';
import {
defineComponent,
watchEffect,
PropOptions,
ref,
unref,
getCurrentInstance,
} from 'compatible-vue';
import { toCanvas, QRCodeRenderersOptions, LogoType } from './qrcodePlus';
import { toDataURL } from 'qrcode';
import { downloadByUrl } from '@/components/file/index';
export default defineComponent({
name: 'QrCode',
props: {
value: {
type: [String, Array],
default: null,
} as PropOptions<string | any[]>,
// 参数
options: {
type: Object,
default: null,
} as PropOptions<QRCodeOptions>,
} as PropOptions<QRCodeRenderersOptions>,
// 宽度
width: {
type: Number,
default: 200,
} as PropOptions<number>,
// 中间logo图标
logo: {
type: [String, Object],
default: '',
} as PropOptions<LogoType | string>,
// img 不支持内嵌logo
tag: {
type: String,
default: 'canvas',
Expand All @@ -21,30 +42,62 @@
},
setup(props, { emit }) {
const wrapRef = ref<HTMLCanvasElement | HTMLImageElement | null>(null);
const urlRef = ref<string>('');
async function createQrcode() {
try {
const { tag, value, options } = props;
const { tag, value, options = {}, width, logo } = props;
const renderValue = String(value);
const wrapEl = unref(wrapRef);
if (!wrapEl) return;
if (tag === 'canvas') {
await toCanvas(wrapEl, renderValue, options || {});
emit('done');
const url: string = await toCanvas({
canvas: wrapEl,
width,
logo,
content: renderValue,
options: options || {},
});
urlRef.value = url;
emit('done', url);
return;
}
if (tag === 'img') {
const url = await toDataURL(renderValue, { errorCorrectionLevel: 'H', ...options });
const url = await toDataURL(renderValue, {
errorCorrectionLevel: 'H',
width,
...options,
});
(unref(wrapRef) as HTMLImageElement).src = url;
emit('done');
urlRef.value = url;
emit('done', url);
}
} catch (error) {
emit('error', error);
}
}
/**
* 文件下载
*/
function download(fileName?: string) {
const url = unref(urlRef);
if (!url) return;
downloadByUrl({
url,
fileName,
});
}
watchEffect(() => {
createQrcode();
});
const currentInstance = getCurrentInstance() as any;
if (currentInstance) {
currentInstance.download = download;
}
return () => {
const Tag = props.tag as string;
Expand Down
5 changes: 5 additions & 0 deletions src/components/qrcode/src/qrcodePlus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 参考 qr-code-with-logo 进行ts版本修改
import { toCanvas } from './toCanvas';
export * from './types';

export { toCanvas };
10 changes: 10 additions & 0 deletions src/components/qrcode/src/toCanvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { renderQrCode } from './drawCanvas';
import { drawLogo } from './drawLogo';
import { RenderQrCodeParams } from './types';
export const toCanvas = (options: RenderQrCodeParams) => {
return renderQrCode(options)
.then(() => {
return options;
})
.then(drawLogo) as Promise<string>;
};
32 changes: 32 additions & 0 deletions src/components/qrcode/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { QRCodeSegment, QRCodeRenderersOptions } from 'qrcode';

export type ContentType = string | QRCodeSegment[];

export { QRCodeRenderersOptions };
export interface RenderQrCodeParams {
canvas: any;
content: ContentType;
width?: number;
options?: QRCodeRenderersOptions;
logo?: LogoType | string;
image?: HTMLImageElement;
downloadName?: string;
download?: boolean | ((...arg) => void);
}

export type LogoType = {
src: string;
logoSize: number;
borderColor: string;
bgColor: string;
borderSize: number;
crossOrigin: string;
borderRadius: number;
logoRadius: number;
};

export type ToCanvasFn = (options: RenderQrCodeParams) => Promise<unknown>;

export interface QrCodeActionType {
download: (fileName?: string) => void;
}
5 changes: 5 additions & 0 deletions src/utils/is/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,8 @@ export const isElement = (val: unknown): val is Element => {
};

export const isServer = typeof window === 'undefined';

// 是否为图片节点
export function isImageDom(o: Element) {
return o && ['IMAGE', 'IMG'].includes(o.tagName);
}
8 changes: 4 additions & 4 deletions src/views/examples/components/file/DownloadFile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
type="primary"
class="my-4"
onClick={() => {
downloadByUrl(
'https://codeload.github.com/anncwb/vue-vben-admin-doc/zip/master',
'_self'
);
downloadByUrl({
url: 'https://codeload.github.com/anncwb/vue-vben-admin-doc/zip/master',
target: '_self',
});
}}
>
文件地址下载
Expand Down
Loading

0 comments on commit f1f2da1

Please sign in to comment.