feat(工具箱/二维码解析和生成): 添加生成二维码功能

This commit is contained in:
2025-02-22 15:23:31 +08:00
parent 084afc0cef
commit f27a869d6a
2 changed files with 301 additions and 25 deletions

View File

@@ -4,6 +4,15 @@ import {
writeBarcode,
} from 'zxing-wasm/full';
/** 默认背景颜色 */
const DEFAULT_BGC = 'transparent';
/** 默认前景颜色 */
const DEFAULT_FGC = '#000000';
/** 模块名称 */
const PREFIX = '[qr-code]';
/**
* @desc 二维码读取配置选项
* @type { import('zxing-wasm').ReaderOptions }
@@ -25,7 +34,7 @@ const readerOptions = {
const writerOptions = {
ecLevel: '',
format: 'QRCode',
scale: 1,
scale: 0,
};
// 配置 wasm 文件路径
@@ -46,7 +55,7 @@ prepareZXingModule({
* @param {Blob} blob
* @param {Callback} callback
*/
export function blobToDataURL(blob, callback) {
function blobToDataURL(blob, callback) {
/** @typedef {(data: { error: boolean, result: string }) => void} Callback */
@@ -70,11 +79,77 @@ export function blobToDataURL(blob, callback) {
}
/**
* @description 修改 SVG 内容,获取信息
* @param {object} options
* @param {string} options.background
* @param {string} options.foreground
* @param {string} options.svgString
*/
function modifySvgContent(options) {
let {
background = DEFAULT_BGC,
foreground = DEFAULT_FGC,
svgString,
} = options;
let divElement = document.createElement('div');
let gElement = null;
let pathElement = null;
let rectElement = null;
let svgElement = null;
// 添加 DOM 元素,用于获取位置大小信息
document.body.appendChild(divElement);
divElement.innerHTML = svgString;
gElement = divElement.getElementsByTagName('g')[0] || null;
pathElement = divElement.getElementsByTagName('path')[0] || null;
rectElement = divElement.getElementsByTagName('rect')[0] || null;
svgElement = divElement.getElementsByTagName('svg')[0] || null;
if (!(gElement && pathElement && rectElement && svgElement)) {
return null;
}
// 修改填充颜色
gElement.setAttribute('fill', foreground);
rectElement.setAttribute('fill', background);
// 获取位置大小信息
let rectOfG = gElement.getBoundingClientRect();
let rectOfPath = pathElement.getBoundingClientRect();
let offsetX = Math.round(rectOfPath.left - rectOfG.left);
let offsetY = Math.round(rectOfPath.top - rectOfG.top);
let sizeW = Math.round(rectOfPath.width);
let sizeH = Math.round(rectOfPath.height);
let result = {
offsetX: offsetX,
offsetY: offsetY,
sizeW: sizeW,
sizeH: sizeH,
svgString: divElement.innerHTML,
};
// 输出处理结果
console.debug(PREFIX, '处理 SVG', result);
// 处理完成,移除 DOM 元素
document.body.removeChild(divElement);
return result;
}
/**
* @description 将 SVG 字符串渲染到 Canvas
* @param {object} options
* @param {HTMLCanvasElement} options.canvas
* @param {string} options.svgString
* @param {string} options.background
* @param {string} options.foreground
* @param {number} options.drawLeft
* @param {number} options.drawTop
* @param {number} options.drawWidth
@@ -85,27 +160,42 @@ function renderSvgToCanvas(options) {
let {
canvas, svgString,
background = DEFAULT_BGC, foreground = DEFAULT_FGC,
drawLeft = 0, drawTop = 0,
drawWidth = 0, drawHeight = 0,
} = options;
let svgInfo = modifySvgContent({
background: background,
foreground: foreground,
svgString: svgString,
});
if (!svgInfo) {
return Promise.resolve(false);
}
return new Promise((resolve) => {
let svgBlob = new Blob([svgString], {
let svgBlob = new Blob([svgInfo.svgString], {
type: 'image/svg+xml;charset=utf-8',
});
let svgUrl = URL.createObjectURL(svgBlob);
let image = new Image();
image.onerror = () => {
console.error('加载 SVG 失败');
console.error(PREFIX, '加载 SVG 失败');
URL.revokeObjectURL(svgUrl);
resolve(false);
};
image.onload = () => {
let ctx = canvas.getContext('2d');
ctx.drawImage(image, drawLeft, drawTop, drawWidth, drawHeight);
ctx.drawImage(
image,
svgInfo.offsetX, svgInfo.offsetY, svgInfo.sizeW, svgInfo.sizeH,
drawLeft, drawTop, drawWidth, drawHeight,
);
URL.revokeObjectURL(svgUrl);
resolve(true);
};
@@ -138,7 +228,7 @@ export function readQrCodeImage(image) {
// 处理读取异常
fileReader.onerror = function () {
console.error('解析二维码失败:读取图片失败');
console.error(PREFIX, '解析二维码失败:读取图片失败');
returns.error = '读取图片失败';
resolve('');
};
@@ -163,10 +253,10 @@ export function readQrCodeImage(image) {
let textList = returns.textList;
if (resultList.length === 0) {
console.warn('解析二维码失败:未识别到内容');
console.warn(PREFIX, '解析二维码失败:未识别到内容');
return returns;
} else {
console.debug('解析二维码成功:', resultList);
console.debug(PREFIX, '解析二维码成功:', resultList);
}
for (let i = 0; i < resultList.length; i++) {
@@ -180,7 +270,7 @@ export function readQrCodeImage(image) {
return returns;
}).catch((error) => {
console.error('解析二维码失败:');
console.error(PREFIX, '解析二维码失败:');
console.error(error);
returns.error = String(error);
return returns;
@@ -192,13 +282,19 @@ export function readQrCodeImage(image) {
* @description 生成二维码图片
* @param {object} options
* @param {string} options.content
* @param {string} options.background
* @param {string} options.foreground
* @param {number} options.width
* @param {number} options.height
* @returns 二维码图片 DataURL
*/
export function writeQrCodeImage(options = {}) {
let { content = '', width = 256, height = 256 } = options;
let {
content = '',
background = DEFAULT_BGC, foreground = DEFAULT_FGC,
width = 256, height = 256,
} = options;
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
@@ -208,15 +304,15 @@ export function writeQrCodeImage(options = {}) {
canvas.height = height;
// 设置背景颜色
ctx.fillStyle = '#FFFFFF';
ctx.fillStyle = background;
ctx.fillRect(0, 0, width, height);
return writeBarcode(content, writerOptions).then((result) => {
console.debug('生成二维码', result);
console.debug(PREFIX, '生成二维码', result);
if (result.error) {
console.error(`生成二维码失败:${result.error}`);
console.error(PREFIX, `生成二维码失败:${result.error}`);
return '';
} else {
return result.svg;
@@ -227,6 +323,8 @@ export function writeQrCodeImage(options = {}) {
return renderSvgToCanvas({
canvas: canvas,
svgString: svgString,
background: background,
foreground: foreground,
drawLeft: 0,
drawTop: 0,
drawWidth: width,
@@ -244,7 +342,7 @@ export function writeQrCodeImage(options = {}) {
}
}).catch((error) => {
console.error('生成二维码失败:');
console.error(PREFIX, '生成二维码失败:');
console.error(error);
return '';
});

View File

@@ -3,6 +3,14 @@
<!-- 解析二维码 -->
<n-card size="small" title="解析二维码">
<template #header-extra>
<n-button
type="error"
:disabled="!readerData.dataURL"
:text="true"
@click="handleClearReader"
>清空</n-button>
</template>
<n-form
class="form-no-feedback"
label-align="left"
@@ -58,13 +66,94 @@
</n-card>
<!-- 生成二维码 -->
<n-card v-if="false" size="small" title="生成二维码">
<n-card size="small" title="生成二维码">
<template #header-extra>
<n-flex>
<n-button
type="primary"
:disabled="!writerData.content"
:text="true"
@click="handleGenerateQrCode"
>生成</n-button>
<n-button
type="primary"
:disabled="!writerData.dataURL"
:text="true"
@click="handleDownloadQrCode"
>下载</n-button>
<n-button
type="error"
:disabled="!writerData.dataURL"
:text="true"
@click="handleClearWriter"
>清空</n-button>
</n-flex>
</template>
<n-form
class="form-no-feedback"
label-align="right"
class="form-no-feedback writer-config"
label-align="left"
label-placement="top"
label-width="auto"
></n-form>
>
<n-form-item label="文本内容">
<n-input
v-model:value="writerData.content"
placeholder="请输入需要生成的二维码文本内容"
type="textarea"
:rows="4"
></n-input>
</n-form-item>
<n-form-item label="分辨率">
<n-flex align="center">
<n-input-number
v-model:value="writerData.resolution"
:min="64"
:max="8192"
:step="1"
></n-input-number>
<span>px</span>
</n-flex>
</n-form-item>
<n-form-item label="颜色">
<n-flex align="center">
<span>前景颜色</span>
<n-color-picker
v-model:value="writerData.colorForeground"
:modes="['hex']"
:show-alpha="true"
:show-preview="true"
:swatches="['#00000000', '#000000FF', '#FFFFFFFF']"
/>
<span>背景颜色</span>
<n-color-picker
v-model:value="writerData.colorBackground"
:modes="['hex']"
:show-alpha="true"
:show-preview="true"
:swatches="['#00000000', '#000000FF', '#FFFFFFFF']"
/>
</n-flex>
</n-form-item>
<n-form-item label="二维码预览">
<div class="writer-preview">
<div class="writer-preview__wrapper">
<n-image
v-show="writerData.dataURL"
object-fit="contain"
width="100%"
height="100%"
:preview-disabled="true"
:src="writerData.dataURL"
/>
</div>
</div>
</n-form-item>
</n-form>
</n-card>
</div>
@@ -72,9 +161,9 @@
<script setup>
import {
NCard, NFlex, NForm, NFormItem,
NImage, NLi, NOl,
NUpload, NUploadDragger,
NButton, NCard, NColorPicker, NFlex,
NForm, NFormItem, NInput, NInputNumber,
NImage, NLi, NOl, NUpload, NUploadDragger,
} from 'naive-ui';
import {
@@ -86,7 +175,7 @@ import {
} from '@/assets/js/naive-ui';
import {
readQrCodeImage,
readQrCodeImage, writeQrCodeImage,
} from '@/assets/js/qr-code';
/** 二维码解析相关数据 */
@@ -103,9 +192,75 @@ const readerData = reactive({
});
// /** 二维码生成相关数据 */
// const writerData = reactive({
// });
/** 二维码生成相关数据 */
const writerData = reactive({
/** 背景颜色 */
colorBackground: '#FFFFFF',
/** 前景颜色 */
colorForeground: '#000000',
/** 文本内容 */
content: '',
/** 图片 DataURL */
dataURL: '',
/** 分辨率 */
resolution: 256,
});
/** 清空信息 */
function handleClearReader() {
readerData.dataURL = '';
readerData.results = [];
}
/** 清空信息 */
function handleClearWriter() {
writerData.content = '';
writerData.dataURL = '';
writerData.resolution = 256;
}
/** 处理下载二维码 */
function handleDownloadQrCode() {
let url = writerData.dataURL;
let element = document.createElement('a');
if (url) {
element.download = '二维码.png';
element.href = url;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
}
/** 处理生成二维码 */
function handleGenerateQrCode() {
return writeQrCodeImage({
content: writerData.content,
background: writerData.colorBackground,
foreground: writerData.colorForeground,
width: writerData.resolution,
height: writerData.resolution,
}).then((dataURL) => {
if (dataURL) {
$message.success('生成二维码成功');
} else {
$message.error('生成二维码失败');
}
writerData.dataURL = dataURL;
});
}
/**
* @description 处理选择图片
@@ -162,4 +317,27 @@ function handleSelectQrImage(options) {
user-select: text;
}
}
.writer-config {
.n-color-picker {
width: 128px;
}
}
.writer-preview {
width: 100%;
.writer-preview__wrapper {
display: flex;
width: 256px;
height: 256px;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
.n-image {
margin: auto;
image-rendering: pixelated;
}
}
}
</style>