6 Commits

10 changed files with 512 additions and 71 deletions

2
public/wasm/README.txt Normal file
View File

@@ -0,0 +1,2 @@
zxing_full.wasm
zxing-wasm v2.0.1

BIN
public/wasm/zxing_full.wasm Normal file

Binary file not shown.

View File

@@ -53,19 +53,36 @@ const themeVars = useThemeVars();
*/
function handleContextMenu(event) {
let element = event.target;
let elements = event.composedPath();
let classValue = '';
let classRegExp = /(n-code|n-input|n-input-number|n-ol|n-select)/;
// 排除按住 Ctrl 键时
if (event.ctrlKey) {
return;
}
// 排除输入框元素
if (
element instanceof HTMLInputElement &&
['password', 'text', 'textarea'].includes(element.type)
) {
return;
for (let i = 0; i < elements.length; i++) {
let element = elements[i];
// 获取元素 class 信息
if (element instanceof HTMLElement) {
classValue = element.classList.value;
} else {
continue;
}
// 排除输入框元素
if (element instanceof HTMLInputElement) {
return;
}
// 排除指定元素
if (classValue && classRegExp.test(classValue)) {
return;
}
}
event.preventDefault();

252
src/assets/js/qr-code.js Normal file
View File

@@ -0,0 +1,252 @@
import {
prepareZXingModule,
readBarcodes,
writeBarcode,
} from 'zxing-wasm/full';
/**
* @desc 二维码读取配置选项
* @type { import('zxing-wasm').ReaderOptions }
*/
const readerOptions = {
formats: ['QRCode'],
maxNumberOfSymbols: 8,
textMode: 'Plain',
tryDownscale: true,
tryHarder: true,
tryInvert: true,
tryRotate: true,
};
/**
* @desc 二维码生成配置选项
* @type { import('zxing-wasm').WriterOptions }
*/
const writerOptions = {
ecLevel: '',
format: 'QRCode',
scale: 1,
};
// 配置 wasm 文件路径
prepareZXingModule({
overrides: {
locateFile: (path, prefix) => {
if (path.endsWith('.wasm')) {
return `./wasm/${path}`;
} else {
return (prefix + path);
}
},
},
});
/**
* @description 转换 Blob 为 DataURL
* @param {Blob} blob
* @param {Callback} callback
*/
export function blobToDataURL(blob, callback) {
/** @typedef {(data: { error: boolean, result: string }) => void} Callback */
let reader = new FileReader();
reader.onerror = function () {
callback({
error: true,
result: reader.result,
});
};
reader.onload = function () {
callback({
error: false,
result: reader.result,
});
};
reader.readAsDataURL(blob);
}
/**
* @description 将 SVG 字符串渲染到 Canvas
* @param {object} options
* @param {HTMLCanvasElement} options.canvas
* @param {string} options.svgString
* @param {number} options.drawLeft
* @param {number} options.drawTop
* @param {number} options.drawWidth
* @param {number} options.drawHeight
* @returns {Promise<boolean>}
*/
function renderSvgToCanvas(options) {
let {
canvas, svgString,
drawLeft = 0, drawTop = 0,
drawWidth = 0, drawHeight = 0,
} = options;
return new Promise((resolve) => {
let svgBlob = new Blob([svgString], {
type: 'image/svg+xml;charset=utf-8',
});
let svgUrl = URL.createObjectURL(svgBlob);
let image = new Image();
image.onerror = () => {
console.error('加载 SVG 失败');
URL.revokeObjectURL(svgUrl);
resolve(false);
};
image.onload = () => {
let ctx = canvas.getContext('2d');
ctx.drawImage(image, drawLeft, drawTop, drawWidth, drawHeight);
URL.revokeObjectURL(svgUrl);
resolve(true);
};
image.src = svgUrl;
});
}
/**
* @description 解析二维码图片
* @param {Blob} image 图片二进制
*/
export function readQrCodeImage(image) {
/**
* @desc 返回结果
* @type {{ error: string; image: string; textList: string[]; }}
*/
let returns = {
error: '',
image: '',
textList: [],
};
/** 读取图片,转换为 DataURL */
let fileReader = new FileReader();
return new Promise((resolve) => {
// 处理读取异常
fileReader.onerror = function () {
console.error('解析二维码失败:读取图片失败');
returns.error = '读取图片失败';
resolve('');
};
// 处理读取完成
fileReader.onload = function () {
resolve(fileReader.result);
};
// 开始读取
fileReader.readAsDataURL(image);
}).then((dataURL) => {
if (dataURL) {
returns.image = dataURL;
return readBarcodes(image, readerOptions);
} else {
return null;
}
}).then((resultList) => {
let textList = returns.textList;
if (resultList.length === 0) {
console.warn('解析二维码失败:未识别到内容');
return returns;
} else {
console.debug('解析二维码成功:', resultList);
}
for (let i = 0; i < resultList.length; i++) {
let item = resultList[i];
textList.push(item.text);
}
return returns;
}).catch((error) => {
console.error('解析二维码失败:');
console.error(error);
returns.error = String(error);
return returns;
});;
}
/**
* @description 生成二维码图片
* @param {object} options
* @param {string} options.content
* @param {number} options.width
* @param {number} options.height
* @returns 二维码图片 DataURL
*/
export function writeQrCodeImage(options = {}) {
let { content = '', width = 256, height = 256 } = options;
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
// 更新画布大小
canvas.width = width;
canvas.height = height;
// 设置背景颜色
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, width, height);
return writeBarcode(content, writerOptions).then((result) => {
console.debug('生成二维码', result);
if (result.error) {
console.error(`生成二维码失败:${result.error}`);
return '';
} else {
return result.svg;
}
}).then((svgString) => {
if (svgString) {
return renderSvgToCanvas({
canvas: canvas,
svgString: svgString,
drawLeft: 0,
drawTop: 0,
drawWidth: width,
drawHeight: height,
});
} else {
return false;
}
}).then((success) => {
if (success) {
return canvas.toDataURL('image/png');
} else {
return '';
}
}).catch((error) => {
console.error('生成二维码失败:');
console.error(error);
return '';
});
}

View File

@@ -13,22 +13,22 @@ export const toolList = [
enabled: true,
items: [
{
id: 'calc-download-time',
component: 'Calculation/CalcDownloadTime',
title: '下载用时计算',
id: 'calc-ratio',
component: 'Calculation/CalcRatio',
title: '比例计算',
iconClass: 'mdi mdi-calculator-variant-outline',
desc: '根据设定的文件大小和下载速度简单计算大约下载完成所需的时间。',
desc: '设定的比例计算给出的数值所对应的数值。',
createdAt: '2024-09-08',
updatedAt: '2024-09-08',
version: '1',
enabled: true,
},
{
id: 'calc-ratio',
component: 'Calculation/CalcRatio',
title: '比例计算',
id: 'calc-download-time',
component: 'Calculation/CalcDownloadTime',
title: '下载用时计算',
iconClass: 'mdi mdi-calculator-variant-outline',
desc: '设定的比例计算给出的数值所对应的数值。',
desc: '根据设定的文件大小和下载速度简单计算大约下载完成所需的时间。',
createdAt: '2024-09-08',
updatedAt: '2024-09-08',
version: '1',
@@ -42,15 +42,15 @@ export const toolList = [
enabled: true,
items: [
{
id: 'convert-html-entities',
component: 'Conversion/ConvertHtmlEntities',
title: '转换 HTML 实体',
id: 'convert-timestamp',
component: 'Conversion/ConvertTimestamp',
title: 'Unix 时间戳转换',
iconClass: 'mdi mdi-swap-horizontal',
desc: '',
createdAt: '',
updatedAt: '',
version: '0',
enabled: false,
desc: '时间戳转时间 / 时间转时间戳',
createdAt: '2025-02-05',
updatedAt: '2025-02-05',
version: '1',
enabled: true,
},
{
id: 'url-encode-decode',
@@ -63,6 +63,17 @@ export const toolList = [
version: '0',
enabled: false,
},
{
id: 'qrcode-reader-and-generator',
component: 'Conversion/QrcodeReaderAndGenerator',
title: '二维码解析和生成',
iconClass: 'mdi mdi-qrcode',
desc: '解析二维码、生成二维码',
createdAt: '2025-02-21',
updatedAt: '2025-02-21',
version: '1',
enabled: true,
},
{
id: 'convert-text-structure',
component: 'Conversion/ConvertTextStructure',
@@ -75,15 +86,15 @@ export const toolList = [
enabled: false,
},
{
id: 'convert-timestamp',
component: 'Conversion/ConvertTimestamp',
title: 'Unix 时间戳转换',
id: 'convert-html-entities',
component: 'Conversion/ConvertHtmlEntities',
title: '转换 HTML 实体',
iconClass: 'mdi mdi-swap-horizontal',
desc: '时间戳转时间 / 时间转时间戳',
createdAt: '2025-02-05',
updatedAt: '2025-02-05',
version: '1',
enabled: true,
desc: '',
createdAt: '',
updatedAt: '',
version: '0',
enabled: false,
},
]
},
@@ -162,22 +173,11 @@ export const toolList = [
enabled: true,
items: [
{
id: 'calc-minecraft-chunk-location',
component: 'Minecraft/CalcChunkLocation',
title: 'Minecraft 区块位置计算',
iconClass: 'mdi mdi-calculator-variant-outline',
desc: '',
createdAt: '',
updatedAt: '',
version: '0',
enabled: false,
},
{
id: 'generate-minecraft-dynmap-renderdata',
component: 'Minecraft/GenerateDynmapRenderdata',
title: '生成 Dynmap renderdata',
iconClass: 'mdi mdi-file-outline',
desc: '生成用于 Minecraft Dynmap 插件或模组的 renderdata 数据。',
id: 'minecraft-uuid-converter',
component: 'Minecraft/UuidConverter',
title: 'Minecraft UUID 转换',
iconClass: 'mdi mdi-identifier',
desc: '随机生成或转换 Minecraft 的 UUID。',
createdAt: '',
updatedAt: '',
version: '0',
@@ -195,11 +195,22 @@ export const toolList = [
enabled: true,
},
{
id: 'minecraft-uuid-converter',
component: 'Minecraft/UuidConverter',
title: 'Minecraft UUID 转换',
iconClass: 'mdi mdi-identifier',
desc: '随机生成或转换 Minecraft 的 UUID。',
id: 'calc-minecraft-chunk-location',
component: 'Minecraft/CalcChunkLocation',
title: 'Minecraft 区块位置计算',
iconClass: 'mdi mdi-calculator-variant-outline',
desc: '',
createdAt: '',
updatedAt: '',
version: '0',
enabled: false,
},
{
id: 'generate-minecraft-dynmap-renderdata',
component: 'Minecraft/GenerateDynmapRenderdata',
title: '生成 Dynmap renderdata',
iconClass: 'mdi mdi-file-outline',
desc: '生成用于 Minecraft Dynmap 插件或模组的 renderdata 数据。',
createdAt: '',
updatedAt: '',
version: '0',
@@ -230,17 +241,6 @@ export const toolList = [
title: '其他',
enabled: true,
items: [
{
id: 'genshin-impact-clock',
component: 'Other/GenshinImpactClock/GenshinImpactClock',
title: '《原神》时钟',
iconClass: 'mdi mdi-clock-outline',
desc: '在网页上实现的《原神》时钟效果',
createdAt: '2024-10-13',
updatedAt: '2024-10-13',
version: '1',
enabled: true,
},
{
id: 'keep-screen-on',
component: 'Other/KeepScreenOn',
@@ -274,6 +274,17 @@ export const toolList = [
version: '0',
enabled: false,
},
{
id: 'genshin-impact-clock',
component: 'Other/GenshinImpactClock/GenshinImpactClock',
title: '《原神》时钟',
iconClass: 'mdi mdi-clock-outline',
desc: '在网页上实现的《原神》时钟效果',
createdAt: '2024-10-13',
updatedAt: '2024-10-13',
version: '1',
enabled: true,
},
],
},
];

View File

@@ -57,7 +57,6 @@
label-align="right"
label-placement="left"
label-width="9em"
@contextmenu.stop
>
<n-form-item label="本地时间:">
@@ -88,7 +87,6 @@
label-align="right"
label-placement="left"
label-width="9em"
@contextmenu.stop
>
<n-form-item label="本地时间:">
@@ -120,9 +118,9 @@
<script setup>
import {
NButton, NCard, NCode, NFlex,
NButton, NCard, NFlex,
NForm, NFormItem,
NInput, NInputNumber, NP, NSelect, NSwitch,
NInput, NP, NSelect, NSwitch,
} from 'naive-ui';
import {

View File

@@ -0,0 +1,165 @@
<template>
<div class="tool-detail-page">
<!-- 解析二维码 -->
<n-card size="small" title="解析二维码">
<n-form
class="form-no-feedback"
label-align="left"
label-placement="top"
label-width="auto"
>
<n-form-item label="选择图片">
<n-upload
accept="image/jpeg,image/png"
:default-upload="false"
:file-list="readerData.fileList"
:show-file-list="false"
@change="handleSelectQrImage"
>
<n-upload-dragger>
<span>点击选择图片 / 拖拽图片到此区域</span>
</n-upload-dragger>
</n-upload>
</n-form-item>
<n-form-item label="解析结果">
<n-flex
class="reader-result"
:wrap="true"
>
<div class="reader-result__image-preview">
<n-image
v-show="readerData.dataURL"
object-fit="contain"
width="100%"
height="100%"
:preview-disabled="true"
:src="readerData.dataURL"
/>
</div>
<n-flex
class="reader-result__text-list"
:vertical="true"
>
<n-ol v-if="readerData.results.length > 0">
<n-li
v-for="(value, index) in readerData.results"
:key="index"
>{{ value }}</n-li>
</n-ol>
<span v-else>请选择二维码图片以进行解析</span>
</n-flex>
</n-flex>
</n-form-item>
</n-form>
</n-card>
<!-- 生成二维码 -->
<n-card v-if="false" size="small" title="生成二维码">
<n-form
class="form-no-feedback"
label-align="right"
label-placement="top"
label-width="auto"
></n-form>
</n-card>
</div>
</template>
<script setup>
import {
NCard, NFlex, NForm, NFormItem,
NImage, NLi, NOl,
NUpload, NUploadDragger,
} from 'naive-ui';
import {
reactive,
} from 'vue';
import {
$message,
} from '@/assets/js/naive-ui';
import {
readQrCodeImage,
} from '@/assets/js/qr-code';
/** 二维码解析相关数据 */
const readerData = reactive({
/** 图片 DataURL */
dataURL: '',
/** 选择文件列表 */
fileList: [],
/** 解析结果 */
results: [],
});
// /** 二维码生成相关数据 */
// const writerData = reactive({
// });
/**
* @description 处理选择图片
* @type { import('naive-ui').UploadOnChange }
*/
function handleSelectQrImage(options) {
let file = options.file.file;
return readQrCodeImage(file).then((result) => {
let { error, image, textList } = result;
if (error) {
$message.error(error);
readerData.dataURL = '';
readerData.results = [];
} else {
if (textList.length === 0) {
$message.warning('未识别到有效的二维码');
} else {
$message.success('识别成功');
}
readerData.dataURL = image;
readerData.results = textList;
}
});
}
</script>
<style lang="less" scoped>
.reader-result {
width: 100%;
.reader-result__image-preview {
display: flex;
padding: 16px;
width: 256px;
height: 256px;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
.n-image {
margin: auto;
}
}
.reader-result__text-list {
flex-grow: 1;
min-width: 256px;
width: 0;
user-select: text;
}
}
</style>

View File

@@ -61,7 +61,6 @@
placeholder="请输入 JSON 字符串"
type="textarea"
:rows="8"
@contextmenu.stop
></n-input>
</n-card>
@@ -72,7 +71,6 @@
language="json"
:code="data.jsonOutput"
:show-line-numbers="true"
@contextmenu.stop
/>
</n-card>

View File

@@ -14,7 +14,6 @@
label-align="left"
label-placement="top"
label-width="auto"
@contextmenu.stop
>
<n-form-item label="连接地址">

View File

@@ -8,7 +8,6 @@
label-align="left"
label-placement="left"
label-width="auto"
@contextmenu.stop
>
<n-form-item label="目标链接:">