feat(工具箱/二维码解析和生成): 添加生成二维码功能
This commit is contained in:
@@ -4,6 +4,15 @@ import {
|
|||||||
writeBarcode,
|
writeBarcode,
|
||||||
} from 'zxing-wasm/full';
|
} from 'zxing-wasm/full';
|
||||||
|
|
||||||
|
/** 默认背景颜色 */
|
||||||
|
const DEFAULT_BGC = 'transparent';
|
||||||
|
|
||||||
|
/** 默认前景颜色 */
|
||||||
|
const DEFAULT_FGC = '#000000';
|
||||||
|
|
||||||
|
/** 模块名称 */
|
||||||
|
const PREFIX = '[qr-code]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc 二维码读取配置选项
|
* @desc 二维码读取配置选项
|
||||||
* @type { import('zxing-wasm').ReaderOptions }
|
* @type { import('zxing-wasm').ReaderOptions }
|
||||||
@@ -25,7 +34,7 @@ const readerOptions = {
|
|||||||
const writerOptions = {
|
const writerOptions = {
|
||||||
ecLevel: '',
|
ecLevel: '',
|
||||||
format: 'QRCode',
|
format: 'QRCode',
|
||||||
scale: 1,
|
scale: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 配置 wasm 文件路径
|
// 配置 wasm 文件路径
|
||||||
@@ -46,7 +55,7 @@ prepareZXingModule({
|
|||||||
* @param {Blob} blob
|
* @param {Blob} blob
|
||||||
* @param {Callback} callback
|
* @param {Callback} callback
|
||||||
*/
|
*/
|
||||||
export function blobToDataURL(blob, callback) {
|
function blobToDataURL(blob, callback) {
|
||||||
|
|
||||||
/** @typedef {(data: { error: boolean, result: string }) => void} 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
|
* @description 将 SVG 字符串渲染到 Canvas
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {HTMLCanvasElement} options.canvas
|
* @param {HTMLCanvasElement} options.canvas
|
||||||
* @param {string} options.svgString
|
* @param {string} options.svgString
|
||||||
|
* @param {string} options.background
|
||||||
|
* @param {string} options.foreground
|
||||||
* @param {number} options.drawLeft
|
* @param {number} options.drawLeft
|
||||||
* @param {number} options.drawTop
|
* @param {number} options.drawTop
|
||||||
* @param {number} options.drawWidth
|
* @param {number} options.drawWidth
|
||||||
@@ -85,27 +160,42 @@ function renderSvgToCanvas(options) {
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
canvas, svgString,
|
canvas, svgString,
|
||||||
|
background = DEFAULT_BGC, foreground = DEFAULT_FGC,
|
||||||
drawLeft = 0, drawTop = 0,
|
drawLeft = 0, drawTop = 0,
|
||||||
drawWidth = 0, drawHeight = 0,
|
drawWidth = 0, drawHeight = 0,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
let svgInfo = modifySvgContent({
|
||||||
|
background: background,
|
||||||
|
foreground: foreground,
|
||||||
|
svgString: svgString,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!svgInfo) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|
||||||
let svgBlob = new Blob([svgString], {
|
let svgBlob = new Blob([svgInfo.svgString], {
|
||||||
type: 'image/svg+xml;charset=utf-8',
|
type: 'image/svg+xml;charset=utf-8',
|
||||||
});
|
});
|
||||||
let svgUrl = URL.createObjectURL(svgBlob);
|
let svgUrl = URL.createObjectURL(svgBlob);
|
||||||
let image = new Image();
|
let image = new Image();
|
||||||
|
|
||||||
image.onerror = () => {
|
image.onerror = () => {
|
||||||
console.error('加载 SVG 失败');
|
console.error(PREFIX, '加载 SVG 失败');
|
||||||
URL.revokeObjectURL(svgUrl);
|
URL.revokeObjectURL(svgUrl);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
let ctx = canvas.getContext('2d');
|
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);
|
URL.revokeObjectURL(svgUrl);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
@@ -138,7 +228,7 @@ export function readQrCodeImage(image) {
|
|||||||
|
|
||||||
// 处理读取异常
|
// 处理读取异常
|
||||||
fileReader.onerror = function () {
|
fileReader.onerror = function () {
|
||||||
console.error('解析二维码失败:读取图片失败');
|
console.error(PREFIX, '解析二维码失败:读取图片失败');
|
||||||
returns.error = '读取图片失败';
|
returns.error = '读取图片失败';
|
||||||
resolve('');
|
resolve('');
|
||||||
};
|
};
|
||||||
@@ -163,10 +253,10 @@ export function readQrCodeImage(image) {
|
|||||||
let textList = returns.textList;
|
let textList = returns.textList;
|
||||||
|
|
||||||
if (resultList.length === 0) {
|
if (resultList.length === 0) {
|
||||||
console.warn('解析二维码失败:未识别到内容');
|
console.warn(PREFIX, '解析二维码失败:未识别到内容');
|
||||||
return returns;
|
return returns;
|
||||||
} else {
|
} else {
|
||||||
console.debug('解析二维码成功:', resultList);
|
console.debug(PREFIX, '解析二维码成功:', resultList);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < resultList.length; i++) {
|
for (let i = 0; i < resultList.length; i++) {
|
||||||
@@ -180,7 +270,7 @@ export function readQrCodeImage(image) {
|
|||||||
return returns;
|
return returns;
|
||||||
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('解析二维码失败:');
|
console.error(PREFIX, '解析二维码失败:');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
returns.error = String(error);
|
returns.error = String(error);
|
||||||
return returns;
|
return returns;
|
||||||
@@ -192,13 +282,19 @@ export function readQrCodeImage(image) {
|
|||||||
* @description 生成二维码图片
|
* @description 生成二维码图片
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {string} options.content
|
* @param {string} options.content
|
||||||
|
* @param {string} options.background
|
||||||
|
* @param {string} options.foreground
|
||||||
* @param {number} options.width
|
* @param {number} options.width
|
||||||
* @param {number} options.height
|
* @param {number} options.height
|
||||||
* @returns 二维码图片 DataURL
|
* @returns 二维码图片 DataURL
|
||||||
*/
|
*/
|
||||||
export function writeQrCodeImage(options = {}) {
|
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 canvas = document.createElement('canvas');
|
||||||
let ctx = canvas.getContext('2d');
|
let ctx = canvas.getContext('2d');
|
||||||
@@ -208,15 +304,15 @@ export function writeQrCodeImage(options = {}) {
|
|||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
|
||||||
// 设置背景颜色
|
// 设置背景颜色
|
||||||
ctx.fillStyle = '#FFFFFF';
|
ctx.fillStyle = background;
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
return writeBarcode(content, writerOptions).then((result) => {
|
return writeBarcode(content, writerOptions).then((result) => {
|
||||||
|
|
||||||
console.debug('生成二维码', result);
|
console.debug(PREFIX, '生成二维码', result);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.error(`生成二维码失败:${result.error}`);
|
console.error(PREFIX, `生成二维码失败:${result.error}`);
|
||||||
return '';
|
return '';
|
||||||
} else {
|
} else {
|
||||||
return result.svg;
|
return result.svg;
|
||||||
@@ -227,6 +323,8 @@ export function writeQrCodeImage(options = {}) {
|
|||||||
return renderSvgToCanvas({
|
return renderSvgToCanvas({
|
||||||
canvas: canvas,
|
canvas: canvas,
|
||||||
svgString: svgString,
|
svgString: svgString,
|
||||||
|
background: background,
|
||||||
|
foreground: foreground,
|
||||||
drawLeft: 0,
|
drawLeft: 0,
|
||||||
drawTop: 0,
|
drawTop: 0,
|
||||||
drawWidth: width,
|
drawWidth: width,
|
||||||
@@ -244,7 +342,7 @@ export function writeQrCodeImage(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('生成二维码失败:');
|
console.error(PREFIX, '生成二维码失败:');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
@@ -3,6 +3,14 @@
|
|||||||
|
|
||||||
<!-- 解析二维码 -->
|
<!-- 解析二维码 -->
|
||||||
<n-card size="small" title="解析二维码">
|
<n-card size="small" title="解析二维码">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
:disabled="!readerData.dataURL"
|
||||||
|
:text="true"
|
||||||
|
@click="handleClearReader"
|
||||||
|
>清空</n-button>
|
||||||
|
</template>
|
||||||
<n-form
|
<n-form
|
||||||
class="form-no-feedback"
|
class="form-no-feedback"
|
||||||
label-align="left"
|
label-align="left"
|
||||||
@@ -58,13 +66,94 @@
|
|||||||
</n-card>
|
</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
|
<n-form
|
||||||
class="form-no-feedback"
|
class="form-no-feedback writer-config"
|
||||||
label-align="right"
|
label-align="left"
|
||||||
label-placement="top"
|
label-placement="top"
|
||||||
label-width="auto"
|
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>
|
</n-card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -72,9 +161,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
NCard, NFlex, NForm, NFormItem,
|
NButton, NCard, NColorPicker, NFlex,
|
||||||
NImage, NLi, NOl,
|
NForm, NFormItem, NInput, NInputNumber,
|
||||||
NUpload, NUploadDragger,
|
NImage, NLi, NOl, NUpload, NUploadDragger,
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -86,7 +175,7 @@ import {
|
|||||||
} from '@/assets/js/naive-ui';
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
readQrCodeImage,
|
readQrCodeImage, writeQrCodeImage,
|
||||||
} from '@/assets/js/qr-code';
|
} 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 处理选择图片
|
* @description 处理选择图片
|
||||||
@@ -162,4 +317,27 @@ function handleSelectQrImage(options) {
|
|||||||
user-select: text;
|
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>
|
</style>
|
||||||
|
Reference in New Issue
Block a user