Compare commits
16 Commits
084afc0cef
...
v3-dev
Author | SHA1 | Date | |
---|---|---|---|
383280031c | |||
bca8602a3a | |||
04c76dc347 | |||
d75232e5e0 | |||
ca27b2bbcc | |||
6486a90812 | |||
855695bc13 | |||
d9869fcb87 | |||
2ecddb17aa | |||
73837d517d | |||
8e6a00f610 | |||
ffbf926c9f | |||
93fea94c3a | |||
eeb72097e5 | |||
c2a496b228 | |||
f27a869d6a |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# 更新日志
|
||||
|
||||
## [3.1.5] - 2025-04-06
|
||||
|
||||
### Added
|
||||
|
||||
- `工具箱` 添加“二维码解析和生成”工具。
|
||||
|
||||
### Changed
|
||||
|
||||
- `工具箱` 调整工具列表项顺序。
|
||||
|
||||
## [3.1.4] - 2025-02-08
|
||||
|
||||
### Added
|
||||
|
10
README.md
10
README.md
@@ -2,7 +2,15 @@
|
||||
|
||||
## 简介
|
||||
|
||||
一个多功能的网址导航,绿色无广告。
|
||||
一个使用 Vue 3 开发的网站导航和工具箱,绿色无广告。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 使用方法
|
||||
|
||||
|
37
package.json
37
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frost-navigation",
|
||||
"description": "Frost Navigation",
|
||||
"version": "3.1.4",
|
||||
"version": "3.1.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -9,33 +9,34 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"dependencies": {
|
||||
"@frost-utils/javascript": "^2.1.3",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"axios": "^1.7.9",
|
||||
"@vueuse/core": "^13.3.0",
|
||||
"axios": "^1.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lunisolar": "^2.5.1",
|
||||
"mathjs": "^14.2.0",
|
||||
"lunisolar": "^2.5.2",
|
||||
"mathjs": "^14.5.2",
|
||||
"naive-ui": "^2.41.0",
|
||||
"radash": "^12.1.0",
|
||||
"uuid": "^11.0.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"zxing-wasm": "^2.0.1"
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^3.5.16",
|
||||
"vue-router": "^4.5.1",
|
||||
"zxing-wasm": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.17.16",
|
||||
"@tsconfig/node20": "^20.1.5",
|
||||
"@types/node": "^20.17.57",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vitejs/plugin-legacy": "^6.1.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vitejs/plugin-vue-jsx": "~4.1.2",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"less": "^4.2.2",
|
||||
"vite": "^6.0.11"
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"less": "^4.3.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
2514
pnpm-lock.yaml
generated
2514
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- core-js
|
||||
- esbuild
|
@@ -1,2 +1,2 @@
|
||||
zxing_full.wasm
|
||||
zxing-wasm v2.0.1
|
||||
zxing-wasm v2.1.2
|
||||
|
Binary file not shown.
BIN
screenshots/image_01.png
Normal file
BIN
screenshots/image_01.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
screenshots/image_02.png
Normal file
BIN
screenshots/image_02.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
BIN
screenshots/image_03.png
Normal file
BIN
screenshots/image_03.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
BIN
screenshots/image_04.png
Normal file
BIN
screenshots/image_04.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
@@ -122,7 +122,8 @@ function initCssVars() {
|
||||
'--color-green': 'var(--color-success)',
|
||||
'--color-blue': 'var(--color-info)',
|
||||
'--color-orange': 'var(--color-warning)',
|
||||
// 滚动条大小
|
||||
// 元素大小
|
||||
'--dialog-content-max-height': 'calc(100vh - 160px)',
|
||||
'--scrollbar-size': '8px',
|
||||
};
|
||||
|
||||
@@ -265,6 +266,11 @@ html {
|
||||
|
||||
// -- Naive UI --
|
||||
|
||||
.n-dialog-content--with-max-height {
|
||||
max-height: var(--dialog-content-max-height);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.n-drawer--right-placement {
|
||||
.n-drawer-body {
|
||||
height: 0;
|
||||
|
@@ -3,19 +3,19 @@
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
|
||||
/** 本地储存 key 前缀 */
|
||||
const PREFIX = 'frost-navigation/';
|
||||
export const KEY_PREFIX = 'frost-navigation/';
|
||||
|
||||
/** NavView 模块 */
|
||||
export const storeNavView = {
|
||||
|
||||
/** 导航链接侧边栏折叠状态 */
|
||||
isAsideCollapsed: useLocalStorage(PREFIX + 'nav-view/is-aside-collapsed', false),
|
||||
isAsideCollapsed: useLocalStorage(KEY_PREFIX + 'nav-view/isAsideCollapsed', false),
|
||||
|
||||
/** 导航链接当前选中分类 */
|
||||
currentCategory: useLocalStorage(PREFIX + 'nav-view/current-category', ''),
|
||||
currentCategory: useLocalStorage(KEY_PREFIX + 'nav-view/currentCategory', ''),
|
||||
|
||||
/** 导航链接搜索类型 */
|
||||
searchType: useLocalStorage(PREFIX + 'nav-view/search-type', 'all'),
|
||||
searchType: useLocalStorage(KEY_PREFIX + 'nav-view/searchType', 'all'),
|
||||
|
||||
};
|
||||
|
||||
@@ -23,6 +23,6 @@ export const storeNavView = {
|
||||
export const storeSearchView = {
|
||||
|
||||
/** 当前使用的搜索引擎名称 */
|
||||
searchEngineName: useLocalStorage(PREFIX + 'search-view/search-engine-name', '必应'),
|
||||
searchEngineName: useLocalStorage(KEY_PREFIX + 'search-view/searchEngineName', '必应'),
|
||||
|
||||
};
|
||||
|
@@ -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 文件路径
|
||||
@@ -42,31 +51,161 @@ prepareZXingModule({
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 转换 Blob 为 DataURL
|
||||
* @param {Blob} blob
|
||||
* @param {Callback} callback
|
||||
* @description 在图片上绘制矩形,返回 DataURL
|
||||
* @param {Blob} blob 图片二进制
|
||||
* @param {Rect[]} rects 矩形位置信息列表
|
||||
*/
|
||||
export function blobToDataURL(blob, callback) {
|
||||
function drawRectsOnImage(blob, rects) {
|
||||
|
||||
/** @typedef {(data: { error: boolean, result: string }) => void} Callback */
|
||||
/** @typedef {{ x: number; y: number; w: number; h: number }} Rect */
|
||||
|
||||
let reader = new FileReader();
|
||||
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);
|
||||
|
||||
reader.onerror = function () {
|
||||
callback({
|
||||
error: true,
|
||||
result: reader.result,
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
reader.onload = function () {
|
||||
callback({
|
||||
error: false,
|
||||
result: reader.result,
|
||||
// 输出处理结果
|
||||
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);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
image.onerror = () => {
|
||||
console.error(PREFIX, '渲染图片失败:加载图片失败');
|
||||
URL.revokeObjectURL(url);
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
image.src = url;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -75,6 +214,8 @@ export function blobToDataURL(blob, callback) {
|
||||
* @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 +226,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 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);
|
||||
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);
|
||||
};
|
||||
@@ -117,74 +273,70 @@ function renderSvgToCanvas(options) {
|
||||
|
||||
/**
|
||||
* @description 解析二维码图片
|
||||
* @param {Blob} image 图片二进制
|
||||
* @param {Blob} blob 图片二进制
|
||||
*/
|
||||
export function readQrCodeImage(image) {
|
||||
export function readQrCodeImage(blob) {
|
||||
|
||||
/**
|
||||
* @desc 返回结果
|
||||
* @type {{ error: string; image: string; textList: string[]; }}
|
||||
*/
|
||||
let returns = {
|
||||
let result = {
|
||||
error: '',
|
||||
image: '',
|
||||
textList: [],
|
||||
};
|
||||
|
||||
/** 读取图片,转换为 DataURL */
|
||||
let fileReader = new FileReader();
|
||||
return readBarcodes(blob, readerOptions).then((codeList) => {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let rectList = [];
|
||||
let textList = result.textList;
|
||||
|
||||
// 处理读取异常
|
||||
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);
|
||||
if (codeList.length === 0) {
|
||||
console.warn(PREFIX, '解析二维码失败:未识别到内容');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).then((resultList) => {
|
||||
|
||||
let textList = returns.textList;
|
||||
|
||||
if (resultList.length === 0) {
|
||||
console.warn('解析二维码失败:未识别到内容');
|
||||
return returns;
|
||||
} else {
|
||||
console.debug('解析二维码成功:', resultList);
|
||||
console.debug(PREFIX, '解析二维码成功:', codeList);
|
||||
}
|
||||
|
||||
for (let i = 0; i < resultList.length; i++) {
|
||||
for (let i = 0; i < codeList.length; i++) {
|
||||
|
||||
let item = resultList[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 returns;
|
||||
// 框选二维码区域
|
||||
return drawRectsOnImage(blob, rectList);
|
||||
|
||||
}).then((dataURL) => {
|
||||
|
||||
if (dataURL) {
|
||||
result.image = dataURL;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}).catch((error) => {
|
||||
console.error('解析二维码失败:');
|
||||
console.error(PREFIX, '解析二维码失败:');
|
||||
console.error(error);
|
||||
returns.error = String(error);
|
||||
return returns;
|
||||
});;
|
||||
result.error = String(error);
|
||||
return result;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -192,13 +344,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 +366,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 +385,8 @@ export function writeQrCodeImage(options = {}) {
|
||||
return renderSvgToCanvas({
|
||||
canvas: canvas,
|
||||
svgString: svgString,
|
||||
background: background,
|
||||
foreground: foreground,
|
||||
drawLeft: 0,
|
||||
drawTop: 0,
|
||||
drawWidth: width,
|
||||
@@ -244,7 +404,7 @@ export function writeQrCodeImage(options = {}) {
|
||||
}
|
||||
|
||||
}).catch((error) => {
|
||||
console.error('生成二维码失败:');
|
||||
console.error(PREFIX, '生成二维码失败:');
|
||||
console.error(error);
|
||||
return '';
|
||||
});
|
||||
|
15
src/assets/js/toolbox-changelogs.js
Normal file
15
src/assets/js/toolbox-changelogs.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/** 工具更新日志 */
|
||||
export const CHANGE_LOGS = {
|
||||
'json-formatter': [
|
||||
'[2] - 2025-02-07\n优化“输出内容”显示样式,解决内容较多时行号显示不全的问题。',
|
||||
'[1] - 2025-02-04\n初始版本。',
|
||||
],
|
||||
'keep-screen-on': [
|
||||
'[2] - 2024-10-13\n优化界面样式,背景添加圆角。',
|
||||
'[1] - 2024-10-11\n初始版本。',
|
||||
],
|
||||
'qrcode-reader-and-generator': [
|
||||
'[2] - 2025-02-23\n支持生成二维码。\n支持解析剪贴板中的二维码图片。\n优化解析功能,在二维码所在位置添加矩形标记。',
|
||||
'[1] - 2025-02-21\n初始版本。',
|
||||
],
|
||||
};
|
@@ -1,5 +1,7 @@
|
||||
// 工具箱
|
||||
|
||||
import { CHANGE_LOGS } from './toolbox-changelogs';
|
||||
|
||||
const MODULES = import.meta.glob('../../views/ToolboxView/**/*.vue');
|
||||
|
||||
/**
|
||||
@@ -41,12 +43,23 @@ export const toolList = [
|
||||
title: '转换',
|
||||
enabled: true,
|
||||
items: [
|
||||
{
|
||||
id: 'base64-encode-decode',
|
||||
component: 'Conversion/Base64StringEncodeDecode',
|
||||
title: 'Base64 字符串编码 / 解码',
|
||||
iconClass: 'mdi mdi-swap-horizontal',
|
||||
desc: '处理 Base64 编码的字符串。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'convert-timestamp',
|
||||
component: 'Conversion/ConvertTimestamp',
|
||||
title: 'Unix 时间戳转换',
|
||||
iconClass: 'mdi mdi-swap-horizontal',
|
||||
desc: '时间戳转时间 / 时间转时间戳',
|
||||
desc: '时间戳转时间字符串 / 时间字符串转时间戳',
|
||||
createdAt: '2025-02-05',
|
||||
updatedAt: '2025-02-05',
|
||||
version: '1',
|
||||
@@ -57,7 +70,7 @@ export const toolList = [
|
||||
component: 'Conversion/UrlEncodeDecode',
|
||||
title: 'URL 编码 / 解码',
|
||||
iconClass: 'mdi mdi-swap-horizontal',
|
||||
desc: '',
|
||||
desc: '处理 URL 编码的字符串。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
@@ -68,11 +81,12 @@ export const toolList = [
|
||||
component: 'Conversion/QrcodeReaderAndGenerator',
|
||||
title: '二维码解析和生成',
|
||||
iconClass: 'mdi mdi-qrcode',
|
||||
desc: '解析二维码、生成二维码',
|
||||
desc: '解析二维码 / 生成二维码',
|
||||
createdAt: '2025-02-21',
|
||||
updatedAt: '2025-02-21',
|
||||
version: '1',
|
||||
updatedAt: '2025-02-23',
|
||||
version: '2',
|
||||
enabled: true,
|
||||
changelogs: CHANGE_LOGS['qrcode-reader-and-generator'],
|
||||
},
|
||||
{
|
||||
id: 'convert-text-structure',
|
||||
@@ -124,6 +138,7 @@ export const toolList = [
|
||||
updatedAt: '2025-02-07',
|
||||
version: '2',
|
||||
enabled: true,
|
||||
changelogs: CHANGE_LOGS['json-formatter'],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -143,6 +158,17 @@ export const toolList = [
|
||||
version: '0',
|
||||
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',
|
||||
component: 'Generator/GenerateUrls',
|
||||
@@ -246,18 +272,19 @@ export const toolList = [
|
||||
component: 'Other/KeepScreenOn',
|
||||
title: '保持亮屏',
|
||||
iconClass: 'mdi mdi-monitor',
|
||||
desc: '保持屏幕开启,不息屏,不休眠',
|
||||
desc: '保持屏幕开启,不息屏,不休眠。',
|
||||
createdAt: '2024-10-11',
|
||||
updatedAt: '2024-10-13',
|
||||
version: '2',
|
||||
enabled: true,
|
||||
changelogs: CHANGE_LOGS['keep-screen-on'],
|
||||
},
|
||||
{
|
||||
id: 'open-new-window',
|
||||
component: 'Other/OpenNewWindow',
|
||||
title: '新窗口中打开',
|
||||
iconClass: 'mdi mdi-window-maximize',
|
||||
desc: '从新的小窗口中打开指定的链接(仅支持 PC 端浏览器)',
|
||||
desc: '从新的小窗口中打开指定的链接(仅支持 PC 端浏览器)。',
|
||||
createdAt: '2025-02-04',
|
||||
updatedAt: '2025-02-04',
|
||||
version: '1',
|
||||
@@ -268,7 +295,7 @@ export const toolList = [
|
||||
component: 'Other/RunJavaScript',
|
||||
title: '执行 JavaScript',
|
||||
iconClass: 'mdi mdi-code-braces',
|
||||
desc: '执行简单的 JavaScript 代码片段',
|
||||
desc: '执行简单的 JavaScript 代码片段。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
@@ -279,12 +306,23 @@ export const toolList = [
|
||||
component: 'Other/GenshinImpactClock/GenshinImpactClock',
|
||||
title: '《原神》时钟',
|
||||
iconClass: 'mdi mdi-clock-outline',
|
||||
desc: '在网页上实现的《原神》时钟效果',
|
||||
desc: '在网页上实现的《原神》时钟效果。',
|
||||
createdAt: '2024-10-13',
|
||||
updatedAt: '2024-10-13',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'visualized-working-hours',
|
||||
component: 'Other/VisualizedWorkingHours/VisualizedWorkingHours',
|
||||
title: '工作时间可视化',
|
||||
iconClass: 'mdi mdi-clock-digital',
|
||||
desc: '用趣味化的方式呈现工作收益与时间进度,让薪资进度和下班期待看得见。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="tool-detail-page"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
@@ -3,6 +3,21 @@
|
||||
|
||||
<!-- 解析二维码 -->
|
||||
<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"
|
||||
@@ -35,7 +50,7 @@
|
||||
object-fit="contain"
|
||||
width="100%"
|
||||
height="100%"
|
||||
:preview-disabled="true"
|
||||
:preview-disabled="false"
|
||||
:src="readerData.dataURL"
|
||||
/>
|
||||
</div>
|
||||
@@ -58,13 +73,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 +168,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 +182,7 @@ import {
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import {
|
||||
readQrCodeImage,
|
||||
readQrCodeImage, writeQrCodeImage,
|
||||
} from '@/assets/js/qr-code';
|
||||
|
||||
/** 二维码解析相关数据 */
|
||||
@@ -103,18 +199,142 @@ 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;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/** 从剪贴板中获取图片 */
|
||||
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 处理选择图片
|
||||
* @type { import('naive-ui').UploadOnChange }
|
||||
* @description 处理解析二维码图片
|
||||
* @param {File} file
|
||||
*/
|
||||
function handleSelectQrImage(options) {
|
||||
|
||||
let file = options.file.file;
|
||||
|
||||
function handleParseQrCode(file) {
|
||||
return readQrCodeImage(file).then((result) => {
|
||||
|
||||
let { error, image, textList } = result;
|
||||
@@ -123,6 +343,7 @@ function handleSelectQrImage(options) {
|
||||
$message.error(error);
|
||||
readerData.dataURL = '';
|
||||
readerData.results = [];
|
||||
return false;
|
||||
} else {
|
||||
if (textList.length === 0) {
|
||||
$message.warning('未识别到有效的二维码');
|
||||
@@ -131,10 +352,18 @@ function handleSelectQrImage(options) {
|
||||
}
|
||||
readerData.dataURL = image;
|
||||
readerData.results = textList;
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理选择图片
|
||||
* @type { import('naive-ui').UploadOnChange }
|
||||
*/
|
||||
function handleSelectQrImage(options) {
|
||||
return handleParseQrCode(options.file.file);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -152,6 +381,8 @@ function handleSelectQrImage(options) {
|
||||
|
||||
.n-image {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,4 +393,29 @@ 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;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
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>
|
@@ -6,21 +6,19 @@
|
||||
<n-form
|
||||
class="form-no-feedback config-inputs"
|
||||
label-align="left"
|
||||
label-placement="left"
|
||||
label-placement="top"
|
||||
label-width="auto"
|
||||
>
|
||||
|
||||
<n-form-item label="目标链接:">
|
||||
<n-form-item label="目标链接">
|
||||
<n-input
|
||||
v-model:value="data.url"
|
||||
placeholder="请输入需要打开的 URL,需要包含协议部分(https://)"
|
||||
placeholder="请输入 URL,需要包含协议部分(https://)"
|
||||
type="text"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="窗口大小:">
|
||||
<n-flex align="center">
|
||||
<span>宽度</span>
|
||||
<n-form-item label="窗口宽度">
|
||||
<n-input-number
|
||||
v-model:value="data.width"
|
||||
:min="0"
|
||||
@@ -28,7 +26,9 @@
|
||||
:precision="0"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
<span>高度</span>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="窗口高度">
|
||||
<n-input-number
|
||||
v-model:value="data.height"
|
||||
:min="0"
|
||||
@@ -36,7 +36,6 @@
|
||||
:precision="0"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
@@ -44,12 +43,10 @@
|
||||
|
||||
<!-- 操作 -->
|
||||
<n-card size="small" title="操作">
|
||||
<n-flex>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="openWindow"
|
||||
>打开窗口</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
@@ -57,7 +54,7 @@
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NFlex,
|
||||
NButton, NCard,
|
||||
NForm, NFormItem, NInput, NInputNumber,
|
||||
} from 'naive-ui';
|
||||
|
||||
@@ -86,18 +83,7 @@ function openWindow() {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.config-inputs {
|
||||
.n-input-number {
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.n-flex {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.n-form-item-blank) {
|
||||
.config-inputs .n-form-item {
|
||||
max-width: 480px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 配置选项 -->
|
||||
<n-card size="small" style="--color: #2196F3;">
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span class="card-title__icon mdi mdi-cog-outline"></span>
|
||||
<span class="card-title__label">配置选项</span>
|
||||
</div>
|
||||
</template>
|
||||
<n-form
|
||||
class="form-no-feedback config-inputs"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
|
||||
<n-form-item label="日薪">
|
||||
<n-input-number
|
||||
v-model:value="configData.dailyWage.value"
|
||||
:min="0"
|
||||
:max="99999999"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="收入币种">
|
||||
<n-input
|
||||
v-model:value="configData.currencyOfIncome.value"
|
||||
placeholder="用于显示,例如:¥"
|
||||
type="text"
|
||||
:maxlength="8"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="工作时间">
|
||||
<n-input-group>
|
||||
<n-time-picker
|
||||
v-model:formatted-value="configData.workTimeStart.value"
|
||||
format="HH:mm"
|
||||
/>
|
||||
<n-time-picker
|
||||
v-model:formatted-value="configData.workTimeStop.value"
|
||||
format="HH:mm"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 实时进度 -->
|
||||
<n-card size="small" style="--color: #F44336;">
|
||||
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span class="card-title__icon mdi mdi-chart-line"></span>
|
||||
<span class="card-title__label">实时进度</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="progress-item">
|
||||
<div class="progress-item__row">
|
||||
<span>本月已赚</span>
|
||||
<span>1000 ¥</span>
|
||||
</div>
|
||||
<div class="progress-item__row">
|
||||
<n-progress
|
||||
color="var(--color)"
|
||||
type="line"
|
||||
:height="16"
|
||||
:percentage="35"
|
||||
:processing="true"
|
||||
:show-indicator="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="progress-item__row">
|
||||
<span>35%</span>
|
||||
<span>剩余 20.5 天</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-item">
|
||||
<div class="progress-item__row">
|
||||
<span>今日已赚</span>
|
||||
<span>100 ¥</span>
|
||||
</div>
|
||||
<div class="progress-item__row">
|
||||
<n-progress
|
||||
color="var(--color)"
|
||||
type="line"
|
||||
:height="16"
|
||||
:percentage="60"
|
||||
:processing="true"
|
||||
:show-indicator="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="progress-item__row">
|
||||
<span>60%</span>
|
||||
<span>剩余 5.6 小时</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</n-card>
|
||||
|
||||
<!-- 下班冲刺 -->
|
||||
<n-card size="small" style="--color: #4CAF50;">
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span class="card-title__icon mdi mdi-clock-outline"></span>
|
||||
<span class="card-title__label">下班冲刺</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="time-info">
|
||||
<div class="time-info__row">
|
||||
<span>距离下班还剩</span>
|
||||
</div>
|
||||
<div class="time-info__row">
|
||||
<span>05:30:20</span>
|
||||
</div>
|
||||
<div class="time-info__row">
|
||||
<span>漫长的一天才过一半,加油吧!</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 获得成就 -->
|
||||
<!-- <n-card size="small" style="--color: #FF9800;">
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span class="card-title__icon mdi mdi-trophy-outline"></span>
|
||||
<span class="card-title__label">获得成就</span>
|
||||
</div>
|
||||
</template>
|
||||
</n-card> -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NForm, NFormItem,
|
||||
NInput, NInputGroup, NInputNumber,
|
||||
NProgress, NTimePicker,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
onBeforeMount,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
configData, initData,
|
||||
} from './data';
|
||||
|
||||
onBeforeMount(() => {
|
||||
initData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-title > span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.card-title__icon {
|
||||
color: var(--color, #252525);
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.card-title__label {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.config-inputs {
|
||||
.n-form-item {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
:deep(.n-form-item-blank) > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.n-input-group) > * {
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-item :not(:first-child) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.progress-item__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&:first-child, &:last-child {
|
||||
color: #505050;
|
||||
}
|
||||
|
||||
&:first-child span:last-child {
|
||||
color: var(--color);
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.time-info {
|
||||
padding: 16px 0;
|
||||
background-color: var(--color-action);
|
||||
}
|
||||
|
||||
.time-info__row {
|
||||
text-align: center;
|
||||
|
||||
&:first-child, &:last-child {
|
||||
color: #505050;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
</style>
|
50
src/views/ToolboxView/Other/VisualizedWorkingHours/data.js
Normal file
50
src/views/ToolboxView/Other/VisualizedWorkingHours/data.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { KEY_PREFIX } from '@/assets/js/local-storage';
|
||||
|
||||
/** 模块名称 */
|
||||
const STORAGE_PREFIX = KEY_PREFIX + 'visualized-working-hours/';
|
||||
|
||||
/** 配置选项 */
|
||||
export const configData = {
|
||||
|
||||
/** 收入币种 */
|
||||
currencyOfIncome: useLocalStorage(STORAGE_PREFIX + 'currencyOfIncome', ''),
|
||||
|
||||
/** 日薪 */
|
||||
dailyWage: useLocalStorage(STORAGE_PREFIX + 'dailyWage', 100),
|
||||
|
||||
/** 午休时长 */
|
||||
lunchBreakDuration: useLocalStorage(STORAGE_PREFIX + 'lunchBreakDuration', 1),
|
||||
|
||||
/** 工作开始时间 */
|
||||
workTimeStart: useLocalStorage(STORAGE_PREFIX + 'workTimeStart', ''),
|
||||
|
||||
/** 工作结束时间 */
|
||||
workTimeStop: useLocalStorage(STORAGE_PREFIX + 'workTimeStop', ''),
|
||||
|
||||
};
|
||||
|
||||
/** 初始化数据 */
|
||||
export function initData() {
|
||||
|
||||
let {
|
||||
currencyOfIncome,
|
||||
workTimeStart,
|
||||
workTimeStop,
|
||||
} = configData;
|
||||
|
||||
let timeRegExp = new RegExp(/^\d{2}:\d{2}$/);
|
||||
|
||||
if (!currencyOfIncome.value) {
|
||||
currencyOfIncome.value = '¥';
|
||||
}
|
||||
|
||||
if (!workTimeStart.value.match(timeRegExp)) {
|
||||
workTimeStart.value = '09:00';
|
||||
}
|
||||
|
||||
if (!workTimeStop.value.match(timeRegExp)) {
|
||||
workTimeStop.value = '18:00';
|
||||
}
|
||||
|
||||
}
|
@@ -5,7 +5,7 @@
|
||||
<!-- 返回上一级 -->
|
||||
<n-button
|
||||
v-show="isToolDetail"
|
||||
class="back-button"
|
||||
class="header-button"
|
||||
:text="true"
|
||||
@click="handleCloseTool"
|
||||
>
|
||||
@@ -18,10 +18,20 @@
|
||||
<!-- 占位 -->
|
||||
<div class="placeholder"></div>
|
||||
|
||||
<!-- 查看信息 -->
|
||||
<n-button
|
||||
v-show="isToolDetail"
|
||||
class="header-button"
|
||||
:text="true"
|
||||
@click="showToolItemInfo"
|
||||
>
|
||||
<span class="mdi mdi-information-slab-circle-outline"></span>
|
||||
</n-button>
|
||||
|
||||
<!-- 新窗口打开 -->
|
||||
<n-button
|
||||
v-show="isToolDetail"
|
||||
class="back-button"
|
||||
class="header-button"
|
||||
:text="true"
|
||||
@click="handleOpenNewWindow"
|
||||
>
|
||||
@@ -89,17 +99,47 @@
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
<!-- 工具信息 -->
|
||||
<n-modal
|
||||
v-model:show="toolInfo.show"
|
||||
content-class="n-dialog-content--with-max-height"
|
||||
preset="dialog"
|
||||
title="工具信息"
|
||||
:show-icon="false"
|
||||
>
|
||||
<template v-if="true">
|
||||
<n-h4>版本信息</n-h4>
|
||||
<n-ul>
|
||||
<n-li>创建日期:{{ toolInfo.dateCreated }}</n-li>
|
||||
<n-li>更新日期:{{ toolInfo.dateUpdated }}</n-li>
|
||||
<n-li>当前版本:{{ toolInfo.currVersion }}</n-li>
|
||||
</n-ul>
|
||||
</template>
|
||||
<template v-if="toolInfo.changelogs.length > 0">
|
||||
<n-h4>更新日志</n-h4>
|
||||
<n-ul>
|
||||
<n-li
|
||||
v-for="(text, index) in toolInfo.changelogs"
|
||||
:key="index"
|
||||
class="changelogs-row"
|
||||
>{{ text }}</n-li>
|
||||
</n-ul>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCollapse, NCollapseItem, NEllipsis, NTooltip,
|
||||
NButton, NCollapse, NCollapseItem,
|
||||
NEllipsis, NModal, NTooltip,
|
||||
NH4, NUl, NLi,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
computed,
|
||||
computed, reactive,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
@@ -112,7 +152,7 @@ import {
|
||||
|
||||
/** 是否为工具页面 */
|
||||
const isToolDetail = computed(() => {
|
||||
return route.meta.isToolDetail;
|
||||
return Boolean(route.meta.isToolDetail);
|
||||
});
|
||||
|
||||
/** 路由 */
|
||||
@@ -126,6 +166,15 @@ const routeTitle = computed(() => {
|
||||
return route.meta.title;
|
||||
});
|
||||
|
||||
/** 工具信息 */
|
||||
const toolInfo = reactive({
|
||||
changelogs: [],
|
||||
currVersion: '',
|
||||
dateCreated: '',
|
||||
dateUpdated: '',
|
||||
show: false,
|
||||
});
|
||||
|
||||
/** 关闭工具 */
|
||||
function handleCloseTool() {
|
||||
return router.push({
|
||||
@@ -154,10 +203,53 @@ function handleOpenTool(data) {
|
||||
name: `Toolbox/${data.component}`,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看当前工具信息 */
|
||||
function showToolItemInfo() {
|
||||
|
||||
let routePath = route.path;
|
||||
let toolIdMatch = routePath.match(/\/([^/]*)$/);
|
||||
let toolIdStr = toolIdMatch ? toolIdMatch[1] : null;
|
||||
let toolItem = null;
|
||||
|
||||
if (toolIdStr) {
|
||||
for (let i = 0; i < toolList.length; i++) {
|
||||
|
||||
let category = toolList[i];
|
||||
let list = category.items;
|
||||
|
||||
if (!category.enabled || !list) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < list.length; j++) {
|
||||
let item = list[j];
|
||||
if (item.id === toolIdStr) {
|
||||
toolItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (toolItem) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (toolItem) {
|
||||
toolInfo.changelogs = toolItem.changelogs || [];
|
||||
toolInfo.currVersion = toolItem.version || '';
|
||||
toolInfo.dateCreated = toolItem.createdAt || '';
|
||||
toolInfo.dateUpdated = toolItem.updatedAt || '';
|
||||
toolInfo.show = true;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.back-button {
|
||||
.header-button {
|
||||
margin-right: 0.5em;
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -243,4 +335,8 @@ function handleOpenTool(data) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.changelogs-row {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
2
types/web.d.ts
vendored
2
types/web.d.ts
vendored
@@ -65,6 +65,8 @@ declare global {
|
||||
version: string;
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
/** 更新日志 */
|
||||
changelogs: string[];
|
||||
}
|
||||
|
||||
// window
|
||||
|
Reference in New Issue
Block a user