Compare commits
20 Commits
V3.1.3
...
855695bc13
Author | SHA1 | Date | |
---|---|---|---|
855695bc13 | |||
d9869fcb87 | |||
2ecddb17aa | |||
73837d517d | |||
8e6a00f610 | |||
ffbf926c9f | |||
93fea94c3a | |||
eeb72097e5 | |||
c2a496b228 | |||
f27a869d6a | |||
084afc0cef | |||
abb1fed1ef | |||
85a7f66af4 | |||
d616564a55 | |||
ba238a44d9 | |||
55f3e74cbf | |||
173cada6a4 | |||
fb655552b3 | |||
9aa47a6b3b | |||
b338b91e5a |
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,5 +1,25 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## [3.1.5] - 2025-04-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `工具箱` 添加“二维码解析和生成”工具。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `工具箱` 调整工具列表项顺序。
|
||||||
|
|
||||||
|
## [3.1.4] - 2025-02-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `工具箱` 添加在新窗口中打开当前工具功能。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `工具箱/JSON 格式化` 优化“输出内容”显示样式,解决内容较多时行号显示不全的问题。
|
||||||
|
|
||||||
## [3.1.3] - 2025-02-07
|
## [3.1.3] - 2025-02-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frost-navigation",
|
"name": "frost-navigation",
|
||||||
"description": "Frost Navigation",
|
"description": "Frost Navigation",
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
2
public/wasm/README.txt
Normal file
2
public/wasm/README.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
zxing_full.wasm
|
||||||
|
zxing-wasm v2.0.1
|
BIN
public/wasm/zxing_full.wasm
Normal file
BIN
public/wasm/zxing_full.wasm
Normal file
Binary file not shown.
31
src/App.vue
31
src/App.vue
@@ -53,19 +53,36 @@ const themeVars = useThemeVars();
|
|||||||
*/
|
*/
|
||||||
function handleContextMenu(event) {
|
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 键时
|
// 排除按住 Ctrl 键时
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 排除输入框元素
|
for (let i = 0; i < elements.length; i++) {
|
||||||
if (
|
|
||||||
element instanceof HTMLInputElement &&
|
let element = elements[i];
|
||||||
['password', 'text', 'textarea'].includes(element.type)
|
|
||||||
) {
|
// 获取元素 class 信息
|
||||||
return;
|
if (element instanceof HTMLElement) {
|
||||||
|
classValue = element.classList.value;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除输入框元素
|
||||||
|
if (element instanceof HTMLInputElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除指定元素
|
||||||
|
if (classValue && classRegExp.test(classValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
412
src/assets/js/qr-code.js
Normal file
412
src/assets/js/qr-code.js
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
import {
|
||||||
|
prepareZXingModule,
|
||||||
|
readBarcodes,
|
||||||
|
writeBarcode,
|
||||||
|
} from 'zxing-wasm/full';
|
||||||
|
|
||||||
|
/** 默认背景颜色 */
|
||||||
|
const DEFAULT_BGC = 'transparent';
|
||||||
|
|
||||||
|
/** 默认前景颜色 */
|
||||||
|
const DEFAULT_FGC = '#000000';
|
||||||
|
|
||||||
|
/** 模块名称 */
|
||||||
|
const PREFIX = '[qr-code]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 配置 wasm 文件路径
|
||||||
|
prepareZXingModule({
|
||||||
|
overrides: {
|
||||||
|
locateFile: (path, prefix) => {
|
||||||
|
if (path.endsWith('.wasm')) {
|
||||||
|
return `./wasm/${path}`;
|
||||||
|
} else {
|
||||||
|
return (prefix + path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 在图片上绘制矩形,返回 DataURL
|
||||||
|
* @param {Blob} blob 图片二进制
|
||||||
|
* @param {Rect[]} rects 矩形位置信息列表
|
||||||
|
*/
|
||||||
|
function drawRectsOnImage(blob, rects) {
|
||||||
|
|
||||||
|
/** @typedef {{ x: number; y: number; w: number; h: number }} Rect */
|
||||||
|
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
return renderImageToCanvas(blob, canvas).then((success) => {
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
rects.forEach((rect, index) => {
|
||||||
|
|
||||||
|
let { x, y, w, h } = rect;
|
||||||
|
|
||||||
|
let text = String(index + 1);
|
||||||
|
|
||||||
|
// 绘制矩形
|
||||||
|
ctx.fillStyle = 'rgba(0, 255, 0, 0.25)';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.strokeStyle = 'rgba(0, 255, 0, 1)';
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
ctx.strokeRect(x, y, w, h);
|
||||||
|
|
||||||
|
// 绘制文本
|
||||||
|
ctx.font = `bold ${Math.round(w / 2)}px sans-serif`;
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
|
||||||
|
ctx.lineWidth = Number((w / 100).toFixed(4));
|
||||||
|
ctx.strokeStyle = 'rgba(0, 0, 0, 1)';
|
||||||
|
ctx.fillText(text, x, y + h);
|
||||||
|
ctx.strokeText(text, x, y + h);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片 Blob 渲染到 Canvas
|
||||||
|
* @param {Blob} blob
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
function renderImageToCanvas(blob, canvas) {
|
||||||
|
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (window.createImageBitmap) {
|
||||||
|
return createImageBitmap(blob).then((bitmap) => {
|
||||||
|
canvas.width = bitmap.width;
|
||||||
|
canvas.height = bitmap.height;
|
||||||
|
ctx.drawImage(bitmap, 0, 0);
|
||||||
|
return true;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('渲染图片失败:');
|
||||||
|
console.error(error);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
|
||||||
|
let image = new Image();
|
||||||
|
let url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
image.onload = () => {
|
||||||
|
canvas.width = image.naturalWidth;
|
||||||
|
canvas.height = image.naturalHeight;
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.onerror = () => {
|
||||||
|
console.error(PREFIX, '渲染图片失败:加载图片失败');
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.src = url;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
* @param {number} options.drawHeight
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
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 ctx = canvas.getContext('2d');
|
||||||
|
let image = new Image();
|
||||||
|
let svgBlob = new Blob([svgInfo.svgString], {
|
||||||
|
type: 'image/svg+xml;charset=utf-8',
|
||||||
|
});
|
||||||
|
let svgUrl = URL.createObjectURL(svgBlob);
|
||||||
|
|
||||||
|
image.onerror = () => {
|
||||||
|
console.error(PREFIX, '加载 SVG 失败');
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.onload = () => {
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
svgInfo.offsetX, svgInfo.offsetY, svgInfo.sizeW, svgInfo.sizeH,
|
||||||
|
drawLeft, drawTop, drawWidth, drawHeight,
|
||||||
|
);
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
image.src = svgUrl;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 解析二维码图片
|
||||||
|
* @param {Blob} blob 图片二进制
|
||||||
|
*/
|
||||||
|
export function readQrCodeImage(blob) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @desc 返回结果
|
||||||
|
* @type {{ error: string; image: string; textList: string[]; }}
|
||||||
|
*/
|
||||||
|
let result = {
|
||||||
|
error: '',
|
||||||
|
image: '',
|
||||||
|
textList: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return readBarcodes(blob, readerOptions).then((codeList) => {
|
||||||
|
|
||||||
|
let rectList = [];
|
||||||
|
let textList = result.textList;
|
||||||
|
|
||||||
|
if (codeList.length === 0) {
|
||||||
|
console.warn(PREFIX, '解析二维码失败:未识别到内容');
|
||||||
|
} else {
|
||||||
|
console.debug(PREFIX, '解析二维码成功:', codeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < codeList.length; i++) {
|
||||||
|
|
||||||
|
let item = codeList[i];
|
||||||
|
let position = item.position;
|
||||||
|
let posX0 = position.topLeft.x;
|
||||||
|
let posX1 = position.bottomRight.x;
|
||||||
|
let posY0 = position.topLeft.y;
|
||||||
|
let posY1 = position.bottomRight.y;
|
||||||
|
|
||||||
|
// 记录二维码坐标
|
||||||
|
rectList.push({
|
||||||
|
x: posX0,
|
||||||
|
y: posY0,
|
||||||
|
w: posX1 - posX0,
|
||||||
|
h: posY1 - posY0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 记录二维码文本
|
||||||
|
textList.push(item.text);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 框选二维码区域
|
||||||
|
return drawRectsOnImage(blob, rectList);
|
||||||
|
|
||||||
|
}).then((dataURL) => {
|
||||||
|
|
||||||
|
if (dataURL) {
|
||||||
|
result.image = dataURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(PREFIX, '解析二维码失败:');
|
||||||
|
console.error(error);
|
||||||
|
result.error = String(error);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 = '',
|
||||||
|
background = DEFAULT_BGC, foreground = DEFAULT_FGC,
|
||||||
|
width = 256, height = 256,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// 更新画布大小
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
// 设置背景颜色
|
||||||
|
ctx.fillStyle = background;
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
return writeBarcode(content, writerOptions).then((result) => {
|
||||||
|
|
||||||
|
console.debug(PREFIX, '生成二维码', result);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error(PREFIX, `生成二维码失败:${result.error}`);
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return result.svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
}).then((svgString) => {
|
||||||
|
if (svgString) {
|
||||||
|
return renderSvgToCanvas({
|
||||||
|
canvas: canvas,
|
||||||
|
svgString: svgString,
|
||||||
|
background: background,
|
||||||
|
foreground: foreground,
|
||||||
|
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(PREFIX, '生成二维码失败:');
|
||||||
|
console.error(error);
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
@@ -13,22 +13,22 @@ export const toolList = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'calc-download-time',
|
id: 'calc-ratio',
|
||||||
component: 'Calculation/CalcDownloadTime',
|
component: 'Calculation/CalcRatio',
|
||||||
title: '下载用时计算',
|
title: '比例计算',
|
||||||
iconClass: 'mdi mdi-calculator-variant-outline',
|
iconClass: 'mdi mdi-calculator-variant-outline',
|
||||||
desc: '根据设定的文件大小和下载速度简单计算大约下载完成所需的时间。',
|
desc: '按设定的比例计算给出的数值所对应的数值。',
|
||||||
createdAt: '2024-09-08',
|
createdAt: '2024-09-08',
|
||||||
updatedAt: '2024-09-08',
|
updatedAt: '2024-09-08',
|
||||||
version: '1',
|
version: '1',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'calc-ratio',
|
id: 'calc-download-time',
|
||||||
component: 'Calculation/CalcRatio',
|
component: 'Calculation/CalcDownloadTime',
|
||||||
title: '比例计算',
|
title: '下载用时计算',
|
||||||
iconClass: 'mdi mdi-calculator-variant-outline',
|
iconClass: 'mdi mdi-calculator-variant-outline',
|
||||||
desc: '按设定的比例计算给出的数值所对应的数值。',
|
desc: '根据设定的文件大小和下载速度简单计算大约下载完成所需的时间。',
|
||||||
createdAt: '2024-09-08',
|
createdAt: '2024-09-08',
|
||||||
updatedAt: '2024-09-08',
|
updatedAt: '2024-09-08',
|
||||||
version: '1',
|
version: '1',
|
||||||
@@ -42,27 +42,49 @@ export const toolList = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'convert-html-entities',
|
id: 'base64-encode-decode',
|
||||||
component: 'Conversion/ConvertHtmlEntities',
|
component: 'Conversion/Base64StringEncodeDecode',
|
||||||
title: '转换 HTML 实体',
|
title: 'Base64 字符串编码 / 解码',
|
||||||
iconClass: 'mdi mdi-swap-horizontal',
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
desc: '',
|
desc: '处理 Base64 编码的字符串。',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
version: '0',
|
version: '0',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'convert-timestamp',
|
||||||
|
component: 'Conversion/ConvertTimestamp',
|
||||||
|
title: 'Unix 时间戳转换',
|
||||||
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
|
desc: '时间戳转时间字符串 / 时间字符串转时间戳',
|
||||||
|
createdAt: '2025-02-05',
|
||||||
|
updatedAt: '2025-02-05',
|
||||||
|
version: '1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'url-encode-decode',
|
id: 'url-encode-decode',
|
||||||
component: 'Conversion/UrlEncodeDecode',
|
component: 'Conversion/UrlEncodeDecode',
|
||||||
title: 'URL 编码 / 解码',
|
title: 'URL 编码 / 解码',
|
||||||
iconClass: 'mdi mdi-swap-horizontal',
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
desc: '',
|
desc: '处理 URL 编码的字符串。',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
version: '0',
|
version: '0',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'qrcode-reader-and-generator',
|
||||||
|
component: 'Conversion/QrcodeReaderAndGenerator',
|
||||||
|
title: '二维码解析和生成',
|
||||||
|
iconClass: 'mdi mdi-qrcode',
|
||||||
|
desc: '解析二维码 / 生成二维码',
|
||||||
|
createdAt: '2025-02-21',
|
||||||
|
updatedAt: '2025-02-23',
|
||||||
|
version: '2',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'convert-text-structure',
|
id: 'convert-text-structure',
|
||||||
component: 'Conversion/ConvertTextStructure',
|
component: 'Conversion/ConvertTextStructure',
|
||||||
@@ -75,15 +97,15 @@ export const toolList = [
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'convert-timestamp',
|
id: 'convert-html-entities',
|
||||||
component: 'Conversion/ConvertTimestamp',
|
component: 'Conversion/ConvertHtmlEntities',
|
||||||
title: 'Unix 时间戳转换',
|
title: '转换 HTML 实体',
|
||||||
iconClass: 'mdi mdi-swap-horizontal',
|
iconClass: 'mdi mdi-swap-horizontal',
|
||||||
desc: '时间戳转时间 / 时间转时间戳',
|
desc: '',
|
||||||
createdAt: '2025-02-05',
|
createdAt: '',
|
||||||
updatedAt: '2025-02-05',
|
updatedAt: '',
|
||||||
version: '1',
|
version: '0',
|
||||||
enabled: true,
|
enabled: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -110,8 +132,8 @@ export const toolList = [
|
|||||||
iconClass: 'mdi mdi-code-json',
|
iconClass: 'mdi mdi-code-json',
|
||||||
desc: '格式化 / 美化 JSON 字符串',
|
desc: '格式化 / 美化 JSON 字符串',
|
||||||
createdAt: '2025-02-04',
|
createdAt: '2025-02-04',
|
||||||
updatedAt: '2025-02-04',
|
updatedAt: '2025-02-07',
|
||||||
version: '1',
|
version: '2',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -132,6 +154,17 @@ export const toolList = [
|
|||||||
version: '0',
|
version: '0',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'frp-config-generator',
|
||||||
|
component: 'Generator/UuidGenerator',
|
||||||
|
title: 'UUID 生成器',
|
||||||
|
iconClass: 'mdi mdi-identifier',
|
||||||
|
desc: '生成 UUID 列表。',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
version: '0',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'generate-urls',
|
id: 'generate-urls',
|
||||||
component: 'Generator/GenerateUrls',
|
component: 'Generator/GenerateUrls',
|
||||||
@@ -162,22 +195,11 @@ export const toolList = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'calc-minecraft-chunk-location',
|
id: 'minecraft-uuid-converter',
|
||||||
component: 'Minecraft/CalcChunkLocation',
|
component: 'Minecraft/UuidConverter',
|
||||||
title: 'Minecraft 区块位置计算',
|
title: 'Minecraft UUID 转换',
|
||||||
iconClass: 'mdi mdi-calculator-variant-outline',
|
iconClass: 'mdi mdi-identifier',
|
||||||
desc: '',
|
desc: '随机生成或转换 Minecraft 的 UUID。',
|
||||||
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: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
version: '0',
|
version: '0',
|
||||||
@@ -195,11 +217,22 @@ export const toolList = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'minecraft-uuid-converter',
|
id: 'calc-minecraft-chunk-location',
|
||||||
component: 'Minecraft/UuidConverter',
|
component: 'Minecraft/CalcChunkLocation',
|
||||||
title: 'Minecraft UUID 转换',
|
title: 'Minecraft 区块位置计算',
|
||||||
iconClass: 'mdi mdi-identifier',
|
iconClass: 'mdi mdi-calculator-variant-outline',
|
||||||
desc: '随机生成或转换 Minecraft 的 UUID。',
|
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: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
version: '0',
|
version: '0',
|
||||||
@@ -230,23 +263,12 @@ export const toolList = [
|
|||||||
title: '其他',
|
title: '其他',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
items: [
|
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',
|
id: 'keep-screen-on',
|
||||||
component: 'Other/KeepScreenOn',
|
component: 'Other/KeepScreenOn',
|
||||||
title: '保持亮屏',
|
title: '保持亮屏',
|
||||||
iconClass: 'mdi mdi-monitor',
|
iconClass: 'mdi mdi-monitor',
|
||||||
desc: '保持屏幕开启,不息屏,不休眠',
|
desc: '保持屏幕开启,不息屏,不休眠。',
|
||||||
createdAt: '2024-10-11',
|
createdAt: '2024-10-11',
|
||||||
updatedAt: '2024-10-13',
|
updatedAt: '2024-10-13',
|
||||||
version: '2',
|
version: '2',
|
||||||
@@ -257,7 +279,7 @@ export const toolList = [
|
|||||||
component: 'Other/OpenNewWindow',
|
component: 'Other/OpenNewWindow',
|
||||||
title: '新窗口中打开',
|
title: '新窗口中打开',
|
||||||
iconClass: 'mdi mdi-window-maximize',
|
iconClass: 'mdi mdi-window-maximize',
|
||||||
desc: '从新的小窗口中打开指定的链接(仅支持 PC 端浏览器)',
|
desc: '从新的小窗口中打开指定的链接(仅支持 PC 端浏览器)。',
|
||||||
createdAt: '2025-02-04',
|
createdAt: '2025-02-04',
|
||||||
updatedAt: '2025-02-04',
|
updatedAt: '2025-02-04',
|
||||||
version: '1',
|
version: '1',
|
||||||
@@ -268,12 +290,23 @@ export const toolList = [
|
|||||||
component: 'Other/RunJavaScript',
|
component: 'Other/RunJavaScript',
|
||||||
title: '执行 JavaScript',
|
title: '执行 JavaScript',
|
||||||
iconClass: 'mdi mdi-code-braces',
|
iconClass: 'mdi mdi-code-braces',
|
||||||
desc: '执行简单的 JavaScript 代码片段',
|
desc: '执行简单的 JavaScript 代码片段。',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
version: '0',
|
version: '0',
|
||||||
enabled: false,
|
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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
@@ -57,7 +57,6 @@
|
|||||||
label-align="right"
|
label-align="right"
|
||||||
label-placement="left"
|
label-placement="left"
|
||||||
label-width="9em"
|
label-width="9em"
|
||||||
@contextmenu.stop
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<n-form-item label="本地时间:">
|
<n-form-item label="本地时间:">
|
||||||
@@ -88,7 +87,6 @@
|
|||||||
label-align="right"
|
label-align="right"
|
||||||
label-placement="left"
|
label-placement="left"
|
||||||
label-width="9em"
|
label-width="9em"
|
||||||
@contextmenu.stop
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<n-form-item label="本地时间:">
|
<n-form-item label="本地时间:">
|
||||||
@@ -120,9 +118,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
NButton, NCard, NCode, NFlex,
|
NButton, NCard, NFlex,
|
||||||
NForm, NFormItem,
|
NForm, NFormItem,
|
||||||
NInput, NInputNumber, NP, NSelect, NSwitch,
|
NInput, NP, NSelect, NSwitch,
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
421
src/views/ToolboxView/Conversion/QrcodeReaderAndGenerator.vue
Normal file
421
src/views/ToolboxView/Conversion/QrcodeReaderAndGenerator.vue
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page">
|
||||||
|
|
||||||
|
<!-- 解析二维码 -->
|
||||||
|
<n-card size="small" title="解析二维码">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-flex>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:text="true"
|
||||||
|
@click="handlePasteImage"
|
||||||
|
>粘贴</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
:disabled="!readerData.dataURL"
|
||||||
|
:text="true"
|
||||||
|
@click="handleClearReader"
|
||||||
|
>清空</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
<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="false"
|
||||||
|
: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 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 writer-config"
|
||||||
|
label-align="left"
|
||||||
|
label-placement="top"
|
||||||
|
label-width="auto"
|
||||||
|
>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
NButton, NCard, NColorPicker, NFlex,
|
||||||
|
NForm, NFormItem, NInput, NInputNumber,
|
||||||
|
NImage, NLi, NOl, NUpload, NUploadDragger,
|
||||||
|
} from 'naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
reactive,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$message,
|
||||||
|
} from '@/assets/js/naive-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
readQrCodeImage, writeQrCodeImage,
|
||||||
|
} from '@/assets/js/qr-code';
|
||||||
|
|
||||||
|
/** 二维码解析相关数据 */
|
||||||
|
const readerData = reactive({
|
||||||
|
|
||||||
|
/** 图片 DataURL */
|
||||||
|
dataURL: '',
|
||||||
|
|
||||||
|
/** 选择文件列表 */
|
||||||
|
fileList: [],
|
||||||
|
|
||||||
|
/** 解析结果 */
|
||||||
|
results: [],
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 二维码生成相关数据 */
|
||||||
|
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;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从剪贴板中获取图片 */
|
||||||
|
function handlePasteImage() {
|
||||||
|
|
||||||
|
let cb = navigator.clipboard;
|
||||||
|
|
||||||
|
if (!cb) {
|
||||||
|
$message.error('当前浏览器不支持该操作');
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb.read().then((items) => {
|
||||||
|
|
||||||
|
console.debug('剪贴板内容', items);
|
||||||
|
|
||||||
|
let imageItem = null;
|
||||||
|
let imageType = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
|
||||||
|
let item = items[i];
|
||||||
|
let types = item.types;
|
||||||
|
|
||||||
|
for (let j = 0; j < types.length; j++) {
|
||||||
|
let type = types[j];
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
imageItem = item;
|
||||||
|
imageType = type;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageItem) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageItem) {
|
||||||
|
return imageItem.getType(imageType);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}).then((blob) => {
|
||||||
|
|
||||||
|
if (blob) {
|
||||||
|
return handleParseQrCode(blob);
|
||||||
|
} else {
|
||||||
|
$message.warning('未在剪贴板中找到图片');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('读取剪贴板失败:');
|
||||||
|
console.error(error);
|
||||||
|
$message.error('读取剪贴板失败,可能是没有权限。');
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 处理解析二维码图片
|
||||||
|
* @param {File} file
|
||||||
|
*/
|
||||||
|
function handleParseQrCode(file) {
|
||||||
|
return readQrCodeImage(file).then((result) => {
|
||||||
|
|
||||||
|
let { error, image, textList } = result;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
$message.error(error);
|
||||||
|
readerData.dataURL = '';
|
||||||
|
readerData.results = [];
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (textList.length === 0) {
|
||||||
|
$message.warning('未识别到有效的二维码');
|
||||||
|
} else {
|
||||||
|
$message.success('识别成功');
|
||||||
|
}
|
||||||
|
readerData.dataURL = image;
|
||||||
|
readerData.results = textList;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 处理选择图片
|
||||||
|
* @type { import('naive-ui').UploadOnChange }
|
||||||
|
*/
|
||||||
|
function handleSelectQrImage(options) {
|
||||||
|
return handleParseQrCode(options.file.file);
|
||||||
|
}
|
||||||
|
</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;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-result__text-list {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 256px;
|
||||||
|
width: 0;
|
||||||
|
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;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -61,7 +61,6 @@
|
|||||||
placeholder="请输入 JSON 字符串"
|
placeholder="请输入 JSON 字符串"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="8"
|
:rows="8"
|
||||||
@contextmenu.stop
|
|
||||||
></n-input>
|
></n-input>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
@@ -72,7 +71,6 @@
|
|||||||
language="json"
|
language="json"
|
||||||
:code="data.jsonOutput"
|
:code="data.jsonOutput"
|
||||||
:show-line-numbers="true"
|
:show-line-numbers="true"
|
||||||
@contextmenu.stop
|
|
||||||
/>
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
@@ -225,7 +223,9 @@ function sortObjectKeys(obj) {
|
|||||||
.json-output {
|
.json-output {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
|
||||||
:deep(pre) {
|
:deep(.__code__) {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
src/views/ToolboxView/Generator/UuidGenerator.vue
Normal file
9
src/views/ToolboxView/Generator/UuidGenerator.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tool-detail-page"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
</style>
|
@@ -14,7 +14,6 @@
|
|||||||
label-align="left"
|
label-align="left"
|
||||||
label-placement="top"
|
label-placement="top"
|
||||||
label-width="auto"
|
label-width="auto"
|
||||||
@contextmenu.stop
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<n-form-item label="连接地址">
|
<n-form-item label="连接地址">
|
||||||
|
@@ -6,38 +6,36 @@
|
|||||||
<n-form
|
<n-form
|
||||||
class="form-no-feedback config-inputs"
|
class="form-no-feedback config-inputs"
|
||||||
label-align="left"
|
label-align="left"
|
||||||
label-placement="left"
|
label-placement="top"
|
||||||
label-width="auto"
|
label-width="auto"
|
||||||
@contextmenu.stop
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<n-form-item label="目标链接:">
|
<n-form-item label="目标链接">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="data.url"
|
v-model:value="data.url"
|
||||||
placeholder="请输入需要打开的 URL,需要包含协议部分(https://)"
|
placeholder="请输入 URL,需要包含协议部分(https://)"
|
||||||
type="text"
|
type="text"
|
||||||
></n-input>
|
></n-input>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item label="窗口大小:">
|
<n-form-item label="窗口宽度">
|
||||||
<n-flex align="center">
|
<n-input-number
|
||||||
<span>宽度</span>
|
v-model:value="data.width"
|
||||||
<n-input-number
|
:min="0"
|
||||||
v-model:value="data.width"
|
:max="9999999"
|
||||||
:min="0"
|
:precision="0"
|
||||||
:max="9999999"
|
:step="1"
|
||||||
:precision="0"
|
></n-input-number>
|
||||||
:step="1"
|
</n-form-item>
|
||||||
></n-input-number>
|
|
||||||
<span>高度</span>
|
<n-form-item label="窗口高度">
|
||||||
<n-input-number
|
<n-input-number
|
||||||
v-model:value="data.height"
|
v-model:value="data.height"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="9999999"
|
:max="9999999"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
:step="1"
|
:step="1"
|
||||||
></n-input-number>
|
></n-input-number>
|
||||||
</n-flex>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
</n-form>
|
</n-form>
|
||||||
@@ -45,12 +43,10 @@
|
|||||||
|
|
||||||
<!-- 操作 -->
|
<!-- 操作 -->
|
||||||
<n-card size="small" title="操作">
|
<n-card size="small" title="操作">
|
||||||
<n-flex>
|
<n-button
|
||||||
<n-button
|
type="primary"
|
||||||
type="primary"
|
@click="openWindow"
|
||||||
@click="openWindow"
|
>打开窗口</n-button>
|
||||||
>打开窗口</n-button>
|
|
||||||
</n-flex>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +54,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
NButton, NCard, NFlex,
|
NButton, NCard,
|
||||||
NForm, NFormItem, NInput, NInputNumber,
|
NForm, NFormItem, NInput, NInputNumber,
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
|
|
||||||
@@ -76,10 +72,10 @@ const data = reactive({
|
|||||||
/** 打开窗口 */
|
/** 打开窗口 */
|
||||||
function openWindow() {
|
function openWindow() {
|
||||||
|
|
||||||
var link = data.url || 'https://github.com/Frost-ZX';
|
let link = data.url || 'https://github.com/Frost-ZX';
|
||||||
var width = data.width ?? 400;
|
let width = data.width ?? 400;
|
||||||
var height = data.height ?? 300;
|
let height = data.height ?? 300;
|
||||||
var features = `height=${height}, width=${width}, toolbar=no, menubar=no, scrollbars=yes, resizable=yes, location=yes, status=yes`;
|
let features = `height=${height}, width=${width}, toolbar=no, menubar=no, scrollbars=yes, resizable=yes, location=yes, status=yes`;
|
||||||
|
|
||||||
window.open(link, '_blank', features);
|
window.open(link, '_blank', features);
|
||||||
|
|
||||||
@@ -87,18 +83,7 @@ function openWindow() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.config-inputs {
|
.config-inputs .n-form-item {
|
||||||
.n-input-number {
|
max-width: 480px;
|
||||||
flex-grow: 1;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-flex {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-form-item-blank) {
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -15,6 +15,19 @@
|
|||||||
<!-- 标题 -->
|
<!-- 标题 -->
|
||||||
<span>{{ routeTitle }}</span>
|
<span>{{ routeTitle }}</span>
|
||||||
|
|
||||||
|
<!-- 占位 -->
|
||||||
|
<div class="placeholder"></div>
|
||||||
|
|
||||||
|
<!-- 新窗口打开 -->
|
||||||
|
<n-button
|
||||||
|
v-show="isToolDetail"
|
||||||
|
class="back-button"
|
||||||
|
:text="true"
|
||||||
|
@click="handleOpenNewWindow"
|
||||||
|
>
|
||||||
|
<span class="mdi mdi-open-in-new"></span>
|
||||||
|
</n-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="app-view-content is-transparent">
|
<div class="app-view-content is-transparent">
|
||||||
|
|
||||||
@@ -120,6 +133,18 @@ function handleCloseTool() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 在新窗口中打开当前工具 */
|
||||||
|
function handleOpenNewWindow() {
|
||||||
|
|
||||||
|
let width = window.innerWidth ?? 400;
|
||||||
|
let height = window.innerHeight ?? 300;
|
||||||
|
let url = location.href;
|
||||||
|
let features = `height=${height}, width=${width}, toolbar=no, menubar=no, scrollbars=yes, resizable=yes, location=yes, status=yes`;
|
||||||
|
|
||||||
|
window.open(url, '_blank', features);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 打开工具
|
* @description 打开工具
|
||||||
* @param {ToolboxItem} data
|
* @param {ToolboxItem} data
|
||||||
@@ -136,6 +161,12 @@ function handleOpenTool(data) {
|
|||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-window-button {
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.tool-list {
|
.tool-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
Reference in New Issue
Block a user