Compare commits
35 Commits
V3.1.5
...
760a457410
| Author | SHA1 | Date | |
|---|---|---|---|
| 760a457410 | |||
| 58116ff311 | |||
| b2d715a1b1 | |||
| f31020d31c | |||
| 7e5100fc5c | |||
| 9ca0619373 | |||
| 02237e3e30 | |||
| 2d805cf02d | |||
| 266a1764be | |||
| 52d154e1a3 | |||
| 7deefc8ea3 | |||
| 31b6a6ce2d | |||
| 7b76410df8 | |||
| c836302f99 | |||
| ecadb6ce2a | |||
| 1ff42aa911 | |||
| a666679ebd | |||
| 6cbbbe8541 | |||
| dcd46b9afe | |||
| 13ed3a8fe4 | |||
| 69e64fb08c | |||
| e6c35d25a5 | |||
| 26947ee678 | |||
| fe8f1606fa | |||
| b8113725bf | |||
| 66e1170c58 | |||
| 359acb41d1 | |||
| 6883e54a1b | |||
| 84273c3689 | |||
| 383280031c | |||
| bca8602a3a | |||
| 04c76dc347 | |||
| d75232e5e0 | |||
| ca27b2bbcc | |||
| 6486a90812 |
3
.trae/rules/project_rules.md
Normal file
3
.trae/rules/project_rules.md
Normal file
@@ -0,0 +1,3 @@
|
||||
1. 项目采用 Vite + Vue 3 + Naive UI 组件库开发,图标库使用 Material Design Icons(@mdi/font)。
|
||||
2. 项目使用 pnpm 作为包管理器。
|
||||
3. 项目开发服务已启动,首页访问地址是 http://localhost:9000/,不需要重复启动。
|
||||
@@ -1,5 +1,12 @@
|
||||
# 更新日志
|
||||
|
||||
## [3.1.6] - 2025-10-26
|
||||
|
||||
### Added
|
||||
|
||||
- `工具箱` 添加工具信息和工具更新日志显示。
|
||||
- `工具箱` 添加“JSON 编辑器”工具。
|
||||
|
||||
## [3.1.5] - 2025-04-06
|
||||
|
||||
### Added
|
||||
|
||||
10
README.md
10
README.md
@@ -2,7 +2,15 @@
|
||||
|
||||
## 简介
|
||||
|
||||
一个多功能的网址导航,绿色无广告。
|
||||
一个使用 Vue 3 开发的网站导航和工具箱,绿色无广告。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 使用方法
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@package-json": ["./package.json"],
|
||||
}
|
||||
},
|
||||
"types": [
|
||||
"@types/w3c-web-serial"
|
||||
]
|
||||
},
|
||||
"exclude": [],
|
||||
"include": [
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
"types": [
|
||||
"@types/node"
|
||||
]
|
||||
},
|
||||
"exclude": [],
|
||||
"include": [
|
||||
|
||||
41
package.json
41
package.json
@@ -1,41 +1,46 @@
|
||||
{
|
||||
"name": "frost-navigation",
|
||||
"description": "Frost Navigation",
|
||||
"version": "3.1.5",
|
||||
"version": "3.1.6",
|
||||
"license": "GPL-3.0-only",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev-host": "vite dev --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"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",
|
||||
"monaco-editor": "^0.54.0",
|
||||
"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",
|
||||
"@types/w3c-web-serial": "^1.0.8",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
2543
pnpm-lock.yaml
generated
2543
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 |
12
src/App.vue
12
src/App.vue
@@ -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;
|
||||
@@ -275,4 +281,8 @@ html {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.n-flex {
|
||||
gap: 10px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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', '必应'),
|
||||
|
||||
};
|
||||
|
||||
21
src/assets/js/monaco-editor.js
Normal file
21
src/assets/js/monaco-editor.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
||||
|
||||
/** 初始化 MonacoEnvironment */
|
||||
export function initMonacoEnvironment() {
|
||||
if (!self.MonacoEnvironment) {
|
||||
// 配置编辑器环境(Service Worker 等)
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(workerId, label) {
|
||||
if (label === 'javascript' || label === 'typescript') {
|
||||
return new tsWorker();
|
||||
} else if (label === 'json') {
|
||||
return new jsonWorker();
|
||||
} else {
|
||||
return new editorWorker();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
207
src/assets/js/png-to-ico.js
Normal file
207
src/assets/js/png-to-ico.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/** PNG 转 ICO 图标,支持生成多尺寸 ICO */
|
||||
class PNGtoICOConverter {
|
||||
|
||||
/**
|
||||
* @description 将 PNG 图像转换为 ICO 格式
|
||||
* @param {File|Blob|string} source PNG 文件、Blob 或 DataURL
|
||||
* @param {number[]} sizes 需要生成的尺寸数组,默认 `[32, 128, 256]`
|
||||
* @returns {Promise<Blob>} ICO 文件的 Blob 对象
|
||||
*/
|
||||
static async convert(source, sizes = [32, 128, 256]) {
|
||||
try {
|
||||
|
||||
// 1. 加载 PNG 图像
|
||||
let pngBlob = await this._preparePNG(source);
|
||||
let imageBitmap = await createImageBitmap(pngBlob);
|
||||
|
||||
// 2. 创建各尺寸的图像
|
||||
let imageDatas = await this._createMultiSizeImages(imageBitmap, sizes);
|
||||
|
||||
// 3. 生成 ICO 文件
|
||||
let icoBlob = await this._generateICO(imageDatas);
|
||||
|
||||
return icoBlob;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`PNG 转 ICO 失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 准备 PNG 源数据
|
||||
* @private
|
||||
*/
|
||||
static async _preparePNG(source) {
|
||||
|
||||
if (source instanceof File || source instanceof Blob) {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (typeof source === 'string') {
|
||||
if (source.startsWith('data:image/png')) {
|
||||
const response = await fetch(source);
|
||||
return await response.blob();
|
||||
} else {
|
||||
throw new Error('不支持的数据格式,仅支持 PNG 文件、Blob 或 DataURL');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('无效的输入类型');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建多尺寸图像数据
|
||||
* @private
|
||||
*/
|
||||
static async _createMultiSizeImages(imageBitmap, sizes) {
|
||||
|
||||
let results = [];
|
||||
|
||||
for (let size of sizes) {
|
||||
|
||||
// 按尺寸创建 Canvas
|
||||
let canvas = document.createElement('canvas');
|
||||
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
let ctx = canvas.getContext('2d');
|
||||
|
||||
// 设置高质量缩放
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
|
||||
// 绘制缩放后的图像
|
||||
ctx.drawImage(imageBitmap, 0, 0, size, size);
|
||||
|
||||
// 获取 PNG 数据
|
||||
let pngBlob = await new Promise(resolve => {
|
||||
canvas.toBlob(resolve, 'image/png');
|
||||
});
|
||||
|
||||
let arrayBuffer = await pngBlob.arrayBuffer();
|
||||
|
||||
results.push({
|
||||
size,
|
||||
width: size,
|
||||
height: size,
|
||||
data: new Uint8Array(arrayBuffer),
|
||||
pngSize: arrayBuffer.byteLength
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成 ICO 文件
|
||||
* @private
|
||||
*/
|
||||
static async _generateICO(imageDatas) {
|
||||
|
||||
// ICO 文件结构:
|
||||
// 1. ICONDIR 头 (6 字节)
|
||||
// 2. ICONDIRENTRY 数组 (每个 16 字节)
|
||||
// 3. PNG 数据
|
||||
|
||||
let numImages = imageDatas.length;
|
||||
|
||||
// 计算总大小
|
||||
let totalSize = 6; // ICONDIR 头
|
||||
totalSize += numImages * 16; // ICONDIRENTRY 数组
|
||||
totalSize += imageDatas.reduce((sum, img) => sum + img.pngSize, 0);
|
||||
|
||||
// 创建 ArrayBuffer
|
||||
let buffer = new ArrayBuffer(totalSize);
|
||||
let view = new DataView(buffer);
|
||||
let offset = 0;
|
||||
|
||||
// 1. 写入 ICONDIR 头
|
||||
view.setUint16(offset, 0, true); // Reserved (0)
|
||||
offset += 2;
|
||||
view.setUint16(offset, 1, true); // Type (1 for ICO)
|
||||
offset += 2;
|
||||
view.setUint16(offset, numImages, true); // Number of images
|
||||
offset += 2;
|
||||
|
||||
// 2. 写入 ICONDIRENTRY 数组
|
||||
let dataOffset = 6 + (numImages * 16);
|
||||
|
||||
for (let img of imageDatas) {
|
||||
|
||||
// 宽度和高度 (0 表示 256 像素)
|
||||
view.setUint8(offset, img.width === 256 ? 0 : img.width);
|
||||
offset += 1;
|
||||
view.setUint8(offset, img.height === 256 ? 0 : img.height);
|
||||
offset += 1;
|
||||
|
||||
view.setUint8(offset, 0); // Color palette (0 for no palette)
|
||||
offset += 1;
|
||||
view.setUint8(offset, 0); // Reserved
|
||||
offset += 1;
|
||||
|
||||
view.setUint16(offset, 1, true); // Color planes
|
||||
offset += 2;
|
||||
view.setUint16(offset, 32, true); // Bits per pixel
|
||||
offset += 2;
|
||||
|
||||
view.setUint32(offset, img.pngSize, true); // PNG data size
|
||||
offset += 4;
|
||||
view.setUint32(offset, dataOffset, true); // PNG data offset
|
||||
offset += 4;
|
||||
|
||||
dataOffset += img.pngSize;
|
||||
|
||||
}
|
||||
|
||||
// 3. 写入 PNG 数据
|
||||
for (let img of imageDatas) {
|
||||
let uint8View = new Uint8Array(buffer, offset, img.pngSize);
|
||||
uint8View.set(img.data);
|
||||
offset += img.pngSize;
|
||||
}
|
||||
|
||||
return new Blob([buffer], { type: 'image/x-icon' });
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 下载 ICO 文件
|
||||
* @param {Blob} icoBlob ICO 文件的 Blob 对象
|
||||
* @param {string} filename 下载的文件名
|
||||
*/
|
||||
static download(icoBlob, filename = 'icon.ico') {
|
||||
|
||||
let url = URL.createObjectURL(icoBlob);
|
||||
let a = document.createElement('a');
|
||||
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 将 PNG 转换为 ICO 并下载
|
||||
* @param {File|Blob|string} source PNG 文件、Blob 或 DataURL
|
||||
* @param {number[]} sizes 需要生成的尺寸数组
|
||||
* @param {string} filename 下载的文件名
|
||||
*/
|
||||
export async function convertPNGtoICO(source, sizes, filename) {
|
||||
try {
|
||||
let blob = await PNGtoICOConverter.convert(source, sizes);
|
||||
PNGtoICOConverter.download(blob, filename);
|
||||
return { blob: blob, error: '', success: true };
|
||||
} catch (error) {
|
||||
console.error('转换失败:', error);
|
||||
return { blob: null, error: error.message, success: false };
|
||||
}
|
||||
}
|
||||
20
src/assets/js/toolbox-changelogs.js
Normal file
20
src/assets/js/toolbox-changelogs.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/** 工具更新日志 */
|
||||
export const CHANGE_LOGS = {
|
||||
'json-formatter': [
|
||||
'[2] - 2025-02-07\n优化“输出内容”显示样式,解决内容较多时行号显示不全的问题。',
|
||||
'[1] - 2025-02-04\n初始版本。',
|
||||
],
|
||||
'keep-screen-on': [
|
||||
'[3] - 2026-01-03\n调整界面样式,与其他模块统一。',
|
||||
'[2] - 2024-10-13\n优化界面样式,背景添加圆角。',
|
||||
'[1] - 2024-10-11\n初始版本。',
|
||||
],
|
||||
'msu2-usb-monitor-controller': [
|
||||
'[2] - 2026-02-24\n完善功能:添加旧算法;支持设置显示方向;支持配置渲染间隔和发送间隔。',
|
||||
'[1] - 2026-02-21\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');
|
||||
|
||||
/**
|
||||
@@ -52,6 +54,17 @@ export const toolList = [
|
||||
version: '0',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'png-to-ico',
|
||||
component: 'Conversion/PngToIco',
|
||||
title: 'PNG 转 ICO 图标',
|
||||
iconClass: 'mdi mdi-image-outline',
|
||||
desc: '将 PNG 图片转换为 ICO 图标。',
|
||||
createdAt: '2026-01-26',
|
||||
updatedAt: '2026-01-26',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'convert-timestamp',
|
||||
component: 'Conversion/ConvertTimestamp',
|
||||
@@ -84,6 +97,7 @@ export const toolList = [
|
||||
updatedAt: '2025-02-23',
|
||||
version: '2',
|
||||
enabled: true,
|
||||
changelogs: CHANGE_LOGS['qrcode-reader-and-generator'],
|
||||
},
|
||||
{
|
||||
id: 'convert-text-structure',
|
||||
@@ -125,6 +139,17 @@ export const toolList = [
|
||||
version: '0',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'json-editor',
|
||||
component: 'Edit/JsonEditor',
|
||||
title: 'JSON 编辑器',
|
||||
iconClass: 'mdi mdi-code-json',
|
||||
desc: '基于 Monaco Editor 实现的 JSON 编辑器。',
|
||||
createdAt: '2025-10-26',
|
||||
updatedAt: '2025-10-26',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'json-formatter',
|
||||
component: 'Edit/JsonFormatter',
|
||||
@@ -135,6 +160,18 @@ export const toolList = [
|
||||
updatedAt: '2025-02-07',
|
||||
version: '2',
|
||||
enabled: true,
|
||||
changelogs: CHANGE_LOGS['json-formatter'],
|
||||
},
|
||||
{
|
||||
id: 'text-editor',
|
||||
component: 'Edit/TextEditor',
|
||||
title: '文本编辑器',
|
||||
iconClass: 'mdi mdi-text-long',
|
||||
desc: '基于 Monaco Editor 实现的文本编辑器。',
|
||||
createdAt: '2025-12-21',
|
||||
updatedAt: '2025-12-21',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -155,15 +192,15 @@ export const toolList = [
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'frp-config-generator',
|
||||
id: 'uuid-generator',
|
||||
component: 'Generator/UuidGenerator',
|
||||
title: 'UUID 生成器',
|
||||
iconClass: 'mdi mdi-identifier',
|
||||
desc: '生成 UUID 列表。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
createdAt: '2026-01-03',
|
||||
updatedAt: '2026-01-03',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'generate-urls',
|
||||
@@ -171,10 +208,10 @@ export const toolList = [
|
||||
title: '生成批量下载链接',
|
||||
iconClass: 'mdi mdi-link-variant',
|
||||
desc: '根据设置,生成有一定规律的用于批量下载的链接。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
createdAt: '2026-01-03',
|
||||
updatedAt: '2026-01-03',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'generate-random-string',
|
||||
@@ -270,8 +307,31 @@ export const toolList = [
|
||||
iconClass: 'mdi mdi-monitor',
|
||||
desc: '保持屏幕开启,不息屏,不休眠。',
|
||||
createdAt: '2024-10-11',
|
||||
updatedAt: '2024-10-13',
|
||||
version: '2',
|
||||
updatedAt: '2026-01-03',
|
||||
version: '3',
|
||||
enabled: true,
|
||||
changelogs: CHANGE_LOGS['keep-screen-on'],
|
||||
},
|
||||
{
|
||||
id: 'visualized-working-hours',
|
||||
component: 'Other/VisualizedWorkingHours/VisualizedWorkingHours',
|
||||
title: '工作时间可视化',
|
||||
iconClass: 'mdi mdi-clock-digital',
|
||||
desc: '用趣味化的方式呈现工作收益与时间进度,让薪资进度和下班期待看得见。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'timer-tool',
|
||||
component: 'Other/TimerTool',
|
||||
title: '计时器',
|
||||
iconClass: 'mdi mdi-timer-outline',
|
||||
desc: '正计时、倒计时工具。',
|
||||
createdAt: '2025-12-29',
|
||||
updatedAt: '2025-12-29',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
@@ -291,10 +351,22 @@ export const toolList = [
|
||||
title: '执行 JavaScript',
|
||||
iconClass: 'mdi mdi-code-braces',
|
||||
desc: '执行简单的 JavaScript 代码片段。',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
createdAt: '2026-01-03',
|
||||
updatedAt: '2026-01-03',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'msu2-usb-monitor-controller',
|
||||
component: 'Other/Msu2UsbMonitorController/Msu2UsbMonitorController',
|
||||
title: 'MSU2 USB 小屏幕控制',
|
||||
iconClass: 'mdi mdi-usb',
|
||||
desc: 'MSU2 USB 小屏幕控制工具,参考了原 Python 程序的代码。',
|
||||
createdAt: '2026-02-21',
|
||||
updatedAt: '2026-02-24',
|
||||
version: '2',
|
||||
enabled: true,
|
||||
changelogs: CHANGE_LOGS['msu2-usb-monitor-controller'],
|
||||
},
|
||||
{
|
||||
id: 'genshin-impact-clock',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 模块显示配置
|
||||
|
||||
import { IS_DEV } from './env';
|
||||
// import { IS_DEV } from './env';
|
||||
|
||||
/** 启用模块 */
|
||||
export const ABOUT_MODULE_ENABLED = true;
|
||||
@@ -9,7 +9,7 @@ export const ABOUT_MODULE_ENABLED = true;
|
||||
export const ABOUT_MODULE_TITLE = '关于';
|
||||
|
||||
/** 启用模块 */
|
||||
export const MC_CTRL_MODULE_ENABLED = IS_DEV;
|
||||
export const MC_CTRL_MODULE_ENABLED = false;
|
||||
|
||||
/** 模块标题 */
|
||||
export const MC_CTRL_MODULE_TITLE = 'MINECRAFT 联动控制';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 参数 -->
|
||||
<n-card size="small" title="参数">
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="配置参数">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 控制 -->
|
||||
<n-card size="small" title="控制">
|
||||
<!-- 操作 -->
|
||||
<n-card size="small" title="操作">
|
||||
<n-form
|
||||
class="form-no-feedback form-data"
|
||||
label-align="right"
|
||||
@@ -80,8 +80,8 @@
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 转换 -->
|
||||
<n-card size="small" title="转换">
|
||||
<!-- 结果 -->
|
||||
<n-card size="small" title="结果">
|
||||
<n-form
|
||||
class="form-no-feedback form-data"
|
||||
label-align="right"
|
||||
|
||||
216
src/views/ToolboxView/Conversion/PngToIco.vue
Normal file
216
src/views/ToolboxView/Conversion/PngToIco.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 文件选择 -->
|
||||
<n-card size="small" title="文件选择">
|
||||
<div class="file-description">
|
||||
<p>支持格式:PNG、JPEG、GIF、BMP</p>
|
||||
<p>建议图片大小:小于 8MB</p>
|
||||
<p>建议图片比例:1:1(正方形)</p>
|
||||
</div>
|
||||
<n-upload
|
||||
:max="1"
|
||||
:on-before-upload="handleFileSelect"
|
||||
:on-error="handleFileError"
|
||||
:on-remove="handleFileRemove"
|
||||
:show-file-list="false">
|
||||
<n-button type="primary">选择文件</n-button>
|
||||
</n-upload>
|
||||
<div class="file-status">
|
||||
<span>已选择文件:</span>
|
||||
<span v-if="formInfo.selectedFile">{{ formInfo.selectedFile.name }}</span>
|
||||
<span v-else>无</span>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 分辨率配置 -->
|
||||
<n-card size="small" title="分辨率配置">
|
||||
<div class="section-description">
|
||||
<p>一个 ICO 文件可以存储多种分辨率的图像。</p>
|
||||
</div>
|
||||
<div class="selection-items">
|
||||
<p class="senection-item">
|
||||
<n-checkbox
|
||||
v-model:checked="formInfo.sizes[16]"
|
||||
>16px</n-checkbox>
|
||||
</p>
|
||||
<p class="senection-item">
|
||||
<n-checkbox
|
||||
v-model:checked="formInfo.sizes[32]"
|
||||
>32px</n-checkbox>
|
||||
</p>
|
||||
<p class="senection-item">
|
||||
<n-checkbox
|
||||
v-model:checked="formInfo.sizes[48]"
|
||||
>48px</n-checkbox>
|
||||
</p>
|
||||
<p class="senection-item">
|
||||
<n-checkbox
|
||||
v-model:checked="formInfo.sizes[64]"
|
||||
>64px</n-checkbox>
|
||||
</p>
|
||||
<p class="senection-item">
|
||||
<n-checkbox
|
||||
v-model:checked="formInfo.sizes[128]"
|
||||
>128px</n-checkbox>
|
||||
</p>
|
||||
<p class="senection-item">
|
||||
<n-checkbox
|
||||
v-model:checked="formInfo.sizes[256]"
|
||||
>256px(仅 32 位深度支持)</n-checkbox>
|
||||
</p>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 位深度配置 -->
|
||||
<n-card size="small" title="位深度配置">
|
||||
<n-radio-group v-model:value="formInfo.bitDepth">
|
||||
<p>
|
||||
<n-radio :value="8">8 位(256 色,调色板)</n-radio>
|
||||
</p>
|
||||
<p>
|
||||
<n-radio :value="32">32 位(1670 万色 & 透明度)</n-radio>
|
||||
</p>
|
||||
</n-radio-group>
|
||||
</n-card>
|
||||
|
||||
<!-- 操作 -->
|
||||
<n-card size="small" title="操作">
|
||||
<n-flex>
|
||||
<n-button
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="handleConvert"
|
||||
>转换为 ICO 图标</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NFlex, NForm, NFormItem,
|
||||
NCheckbox, NRadio, NRadioGroup, NUpload,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
reactive,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
$message, $notification,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import {
|
||||
convertPNGtoICO,
|
||||
} from '@/assets/js/png-to-ico';
|
||||
|
||||
/** 数据 */
|
||||
const formInfo = reactive({
|
||||
|
||||
/** 选择的文件 */
|
||||
selectedFile: null,
|
||||
|
||||
/** 选择的分辨率 */
|
||||
sizes: {
|
||||
16: true,
|
||||
32: true,
|
||||
48: true,
|
||||
64: false,
|
||||
128: false,
|
||||
256: false
|
||||
},
|
||||
|
||||
/** 选择的位深度 */
|
||||
bitDepth: 32,
|
||||
|
||||
});
|
||||
|
||||
/** 文件上传错误时的处理 */
|
||||
function handleFileError() {
|
||||
$message.error('文件上传失败,请重试');
|
||||
}
|
||||
|
||||
/** 文件选择时的处理 */
|
||||
function handleFileSelect(uploadData) {
|
||||
formInfo.selectedFile = uploadData?.file?.file;
|
||||
// 返回 false 阻止自动上传(只需要获取文件对象)
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 文件移除时的处理 */
|
||||
function handleFileRemove() {
|
||||
formInfo.selectedFile = null;
|
||||
}
|
||||
|
||||
/** 转换按钮点击处理 */
|
||||
function handleConvert() {
|
||||
|
||||
if (!formInfo.selectedFile) {
|
||||
$message.warning('请先选择文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否选择了至少一个分辨率
|
||||
let selectedSizes = Object.keys(formInfo.sizes).filter((size) => {
|
||||
return formInfo.sizes[size];
|
||||
}).map((value) => {
|
||||
return Number(value);
|
||||
});
|
||||
|
||||
if (selectedSizes.length === 0) {
|
||||
$message.warning('请至少选择一个分辨率');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查如果选择了 256 像素,是否位深度为 32 位
|
||||
if (formInfo.sizes[256] && formInfo.bitDepth !== 32) {
|
||||
$message.warning('256px 分辨率仅支持 32 位深度');
|
||||
return;
|
||||
}
|
||||
|
||||
convertPNGtoICO(
|
||||
formInfo.selectedFile,
|
||||
selectedSizes,
|
||||
'icon.ico'
|
||||
).then((result) => {
|
||||
|
||||
console.log('转换结果', result);
|
||||
|
||||
if (result.success) {
|
||||
$message.success('文件转换成功');
|
||||
} else {
|
||||
$message.error('文件转换失败');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-description {
|
||||
margin-bottom: 12px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.file-status {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
margin-bottom: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:deep(.n-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
137
src/views/ToolboxView/Edit/JsonEditor.vue
Normal file
137
src/views/ToolboxView/Edit/JsonEditor.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
<n-card size="small" title="编辑器">
|
||||
<div ref="editorContainer" class="editor-container"></div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NCard,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
ref, shallowRef,
|
||||
onBeforeUnmount, onMounted,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
initMonacoEnvironment,
|
||||
} from '@/assets/js/monaco-editor';
|
||||
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
/** 模块名称 */
|
||||
const PREFIX = '[JsonEditor]';
|
||||
|
||||
/**
|
||||
* @desc 编辑器实例(注:内部不能为响应式,防止出现部分操作会导致界面卡死等异常)
|
||||
* @type {VueRef<monaco.editor.IStandaloneCodeEditor>}
|
||||
*/
|
||||
const editorInstance = shallowRef(null);
|
||||
|
||||
/** @type {VueRef<HTMLElement>} */
|
||||
const editorContainer = ref(null);
|
||||
|
||||
/** 防抖定时器 */
|
||||
const resizeTimer = ref(null);
|
||||
|
||||
/** 处理窗口大小变化 */
|
||||
function handleWindowResize() {
|
||||
clearTimeout(resizeTimer.value);
|
||||
resizeTimer.value = setTimeout(() => {
|
||||
if (editorInstance.value) {
|
||||
editorInstance.value.layout();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/** 初始化编辑器 */
|
||||
function initEditor() {
|
||||
|
||||
console.log(PREFIX, 'initEditor');
|
||||
|
||||
let container = editorContainer.value;
|
||||
let editor = null;
|
||||
let valueStr = '';
|
||||
|
||||
if (!container) {
|
||||
console.error(PREFIX, '初始化失败:元素不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
editor = monaco.editor.create(container, {
|
||||
autoDetectHighContrast: false,
|
||||
automaticLayout: false,
|
||||
contextmenu: true,
|
||||
find: {
|
||||
cursorMoveOnType: false,
|
||||
},
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
language: 'json',
|
||||
minimap: {
|
||||
enabled: true,
|
||||
renderCharacters: false,
|
||||
},
|
||||
mouseWheelScrollSensitivity: 2,
|
||||
stickyScroll: {
|
||||
enabled: false,
|
||||
},
|
||||
tabSize: 2,
|
||||
theme: 'vs',
|
||||
value: valueStr,
|
||||
});
|
||||
|
||||
// 注:
|
||||
// 若使用自定义字体,且字体的加载时间比编辑器加载时间长,
|
||||
// 需要在字体加载完成后调用 monaco.editor.remeasureFonts(),
|
||||
// 防止光标位置异常。
|
||||
|
||||
editorInstance.value = editor;
|
||||
|
||||
}
|
||||
|
||||
/** 销毁编辑器 */
|
||||
function resetEditor() {
|
||||
|
||||
console.log(PREFIX, 'resetEditor');
|
||||
|
||||
try {
|
||||
if (editorInstance.value) {
|
||||
editorInstance.value.getModel().dispose()
|
||||
editorInstance.value.dispose();
|
||||
editorInstance.value = null;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(PREFIX, '销毁失败:');
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initMonacoEnvironment();
|
||||
initEditor();
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetEditor();
|
||||
window.removeEventListener('resize', handleWindowResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-card, .editor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
outline: 1px solid var(--n-border-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 设置 -->
|
||||
<n-card size="small" title="设置">
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="配置参数">
|
||||
<n-flex>
|
||||
<!-- 缩进空格 -->
|
||||
<div class="config-item">
|
||||
|
||||
245
src/views/ToolboxView/Edit/TextEditor.vue
Normal file
245
src/views/ToolboxView/Edit/TextEditor.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 操作 -->
|
||||
<n-card size="small" title="操作">
|
||||
<n-flex>
|
||||
<n-flex align="center">
|
||||
<span>文件格式:</span>
|
||||
<n-select
|
||||
v-model:value="textEncoding"
|
||||
:options="[
|
||||
{ label: 'GBK', value: 'gbk' },
|
||||
{ label: 'UTF-8', value: 'utf-8' },
|
||||
]"
|
||||
style="width: 160px;"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="handleOpenFile"
|
||||
>打开文件</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="handleClearContent"
|
||||
>清空内容</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<n-card size="small" title="编辑器">
|
||||
<div ref="editorContainer" class="editor-container"></div>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NFlex, NSelect,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
ref, shallowRef,
|
||||
onBeforeUnmount, onMounted,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
useFileDialog,
|
||||
} from '@vueuse/core';
|
||||
|
||||
import {
|
||||
initMonacoEnvironment,
|
||||
} from '@/assets/js/monaco-editor';
|
||||
|
||||
import {
|
||||
$dialog, $message,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
/** 模块名称 */
|
||||
const PREFIX = '[TextEditor]';
|
||||
|
||||
const {
|
||||
onChange: fileOnChange,
|
||||
open: fileOpen,
|
||||
reset: fileReset,
|
||||
} = useFileDialog({
|
||||
accept: '*',
|
||||
directory: false,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* @desc 编辑器实例(注:内部不能为响应式,防止出现部分操作会导致界面卡死等异常)
|
||||
* @type {VueRef<monaco.editor.IStandaloneCodeEditor>}
|
||||
*/
|
||||
const editorInstance = shallowRef(null);
|
||||
|
||||
/** @type {VueRef<HTMLElement>} */
|
||||
const editorContainer = ref(null);
|
||||
|
||||
/** 防抖定时器 */
|
||||
const resizeTimer = ref(null);
|
||||
|
||||
/** 文件编码 */
|
||||
const textEncoding = ref('utf-8');
|
||||
|
||||
/** 处理清空内容操作 */
|
||||
function handleClearContent() {
|
||||
$dialog.create({
|
||||
content: '确定要清空编辑器内容吗?',
|
||||
negativeText: '取消',
|
||||
positiveText: '确定',
|
||||
title: '确认',
|
||||
type: 'default',
|
||||
onPositiveClick: () => {
|
||||
updateEditorContent('');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理打开文件操作 */
|
||||
function handleOpenFile() {
|
||||
fileReset();
|
||||
fileOpen();
|
||||
}
|
||||
|
||||
/** 处理窗口大小变化 */
|
||||
function handleWindowResize() {
|
||||
clearTimeout(resizeTimer.value);
|
||||
resizeTimer.value = setTimeout(() => {
|
||||
if (editorInstance.value) {
|
||||
editorInstance.value.layout();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/** 初始化编辑器 */
|
||||
function initEditor() {
|
||||
|
||||
console.log(PREFIX, 'initEditor');
|
||||
|
||||
let container = editorContainer.value;
|
||||
let editor = null;
|
||||
let valueStr = '';
|
||||
|
||||
if (!container) {
|
||||
console.error(PREFIX, '初始化失败:元素不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
editor = monaco.editor.create(container, {
|
||||
autoDetectHighContrast: false,
|
||||
automaticLayout: false,
|
||||
contextmenu: true,
|
||||
find: {
|
||||
cursorMoveOnType: false,
|
||||
},
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
language: 'plaintext',
|
||||
minimap: {
|
||||
enabled: true,
|
||||
renderCharacters: false,
|
||||
},
|
||||
mouseWheelScrollSensitivity: 2,
|
||||
stickyScroll: {
|
||||
enabled: false,
|
||||
},
|
||||
tabSize: 2,
|
||||
theme: 'vs',
|
||||
value: valueStr,
|
||||
});
|
||||
|
||||
// 注:
|
||||
// 若使用自定义字体,且字体的加载时间比编辑器加载时间长,
|
||||
// 需要在字体加载完成后调用 monaco.editor.remeasureFonts(),
|
||||
// 防止光标位置异常。
|
||||
|
||||
editorInstance.value = editor;
|
||||
|
||||
}
|
||||
|
||||
/** 销毁编辑器 */
|
||||
function resetEditor() {
|
||||
|
||||
console.log(PREFIX, 'resetEditor');
|
||||
|
||||
try {
|
||||
if (editorInstance.value) {
|
||||
editorInstance.value.getModel().dispose()
|
||||
editorInstance.value.dispose();
|
||||
editorInstance.value = null;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(PREFIX, '销毁失败:');
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** 更新编辑器内容 */
|
||||
function updateEditorContent(text = '') {
|
||||
if (editorInstance.value) {
|
||||
editorInstance.value.setValue(text);
|
||||
}
|
||||
}
|
||||
|
||||
fileOnChange((files) => {
|
||||
|
||||
let file = files ? files[0] : null;
|
||||
|
||||
file && file.arrayBuffer().then((buffer) => {
|
||||
try {
|
||||
|
||||
let decoder = new TextDecoder(textEncoding.value);
|
||||
let text = decoder.decode(buffer);
|
||||
|
||||
updateEditorContent(text);
|
||||
|
||||
} catch (error) {
|
||||
console.error('解码失败:');
|
||||
console.error(error);
|
||||
$message.error('打开文件失败:解码失败');
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('打开文件失败:');
|
||||
console.error(error);
|
||||
$message.error('打开文件失败:读取失败');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initMonacoEnvironment();
|
||||
initEditor();
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetEditor();
|
||||
window.removeEventListener('resize', handleWindowResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.n-card:nth-child(1) {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.n-card:nth-child(2) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.n-card, .editor-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
height: 100%;
|
||||
outline: 1px solid var(--n-border-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 配置选项 -->
|
||||
<n-card size="small" title="配置选项">
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="配置参数">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
|
||||
@@ -1,9 +1,488 @@
|
||||
<template>
|
||||
<div class="tool-detail-page"></div>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 链接模板 -->
|
||||
<n-card size="small" title="链接模板">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
<n-form-item label="链接模板:">
|
||||
<n-input
|
||||
v-model:value="linkBase"
|
||||
placeholder="使用 {n} 表示变量,例如:https://example.com/file_{n}.jpg"
|
||||
clearable
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="参数配置">
|
||||
<n-radio-group v-model:value="mode">
|
||||
|
||||
<!-- 等差数列 -->
|
||||
<div class="mode-item">
|
||||
<n-flex vertical>
|
||||
<n-radio value="as">等差数列</n-radio>
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
<n-flex>
|
||||
<n-form-item label="首项:">
|
||||
<n-input-number
|
||||
v-model:value="modes.as.first"
|
||||
:min="0"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item label="公差:">
|
||||
<n-input-number
|
||||
v-model:value="modes.as.diff"
|
||||
:min="1"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item label="项数:">
|
||||
<n-input-number
|
||||
v-model:value="modes.as.count"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
</n-flex>
|
||||
<n-flex>
|
||||
<n-form-item label="选项:">
|
||||
<n-checkbox
|
||||
v-model:checked="modes.as.zero"
|
||||
>补零</n-checkbox>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-checkbox
|
||||
v-model:checked="modes.as.reverse"
|
||||
>倒序</n-checkbox>
|
||||
</n-form-item>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<!-- 等比数列 -->
|
||||
<div class="mode-item">
|
||||
<n-flex vertical>
|
||||
<n-radio value="ps">等比数列</n-radio>
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
<n-flex>
|
||||
<n-form-item label="首项:">
|
||||
<n-input-number
|
||||
v-model:value="modes.ps.first"
|
||||
:min="1"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item label="公比:">
|
||||
<n-input-number
|
||||
v-model:value="modes.ps.diff"
|
||||
:min="2"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item label="项数:">
|
||||
<n-input-number
|
||||
v-model:value="modes.ps.count"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
</n-flex>
|
||||
<n-flex>
|
||||
<n-form-item label="选项:">
|
||||
<n-checkbox
|
||||
v-model:checked="modes.ps.zero"
|
||||
>补零</n-checkbox>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-checkbox
|
||||
v-model:checked="modes.ps.reverse"
|
||||
>倒序</n-checkbox>
|
||||
</n-form-item>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<!-- 字母变化 -->
|
||||
<div class="mode-item">
|
||||
<n-flex vertical>
|
||||
<n-radio value="lc">字母变化</n-radio>
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
<n-flex align-items="center">
|
||||
<n-form-item label="从:">
|
||||
<n-input
|
||||
v-model:value="modes.lc.start"
|
||||
:maxlength="1"
|
||||
placeholder="a"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item label="到:">
|
||||
<n-input
|
||||
v-model:value="modes.lc.end"
|
||||
:maxlength="1"
|
||||
placeholder="z"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-checkbox
|
||||
v-model:checked="modes.lc.reverse"
|
||||
>倒序</n-checkbox>
|
||||
</n-form-item>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
</n-radio-group>
|
||||
</n-card>
|
||||
|
||||
<!-- 操作 -->
|
||||
<n-card size="small" title="操作">
|
||||
<n-flex>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="handleGenerate"
|
||||
>生成链接</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!linkResult"
|
||||
@click="handleCopy"
|
||||
>复制结果</n-button>
|
||||
<n-button
|
||||
@click="handleClear"
|
||||
>清空结果</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<!-- 生成结果 -->
|
||||
<n-card size="small" title="生成结果">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
<n-form-item>
|
||||
<n-input
|
||||
v-model:value="linkResult"
|
||||
placeholder="生成的链接将显示在这里"
|
||||
type="textarea"
|
||||
:readonly="true"
|
||||
:rows="10"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NCheckbox, NFlex,
|
||||
NForm, NFormItem, NInput, NInputNumber,
|
||||
NRadio, NRadioGroup,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
reactive, ref,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
$message,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import {
|
||||
useClipboard,
|
||||
} from '@vueuse/core';
|
||||
|
||||
// 链接模板
|
||||
const linkBase = ref('');
|
||||
|
||||
// 生成结果
|
||||
const linkResult = ref('');
|
||||
|
||||
// 正则,匹配链接变量
|
||||
const linkRegExp = /\{n\}/g;
|
||||
|
||||
// 当前模式
|
||||
const mode = ref('as');
|
||||
|
||||
// 模式配置
|
||||
const modes = reactive({
|
||||
// 等差数列(Arithmetic Sequence)
|
||||
as: {
|
||||
first: 0,
|
||||
diff: 1,
|
||||
count: 10,
|
||||
zero: false,
|
||||
reverse: false,
|
||||
},
|
||||
// 等比数列(Proportional Sequence)
|
||||
ps: {
|
||||
first: 1,
|
||||
diff: 2,
|
||||
count: 10,
|
||||
zero: false,
|
||||
reverse: false,
|
||||
},
|
||||
// 字母变化(Letter Change)
|
||||
lc: {
|
||||
start: 'a',
|
||||
end: 'z',
|
||||
reverse: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 剪贴板功能
|
||||
const clipboard = useClipboard({
|
||||
legacy: true,
|
||||
read: false,
|
||||
});
|
||||
|
||||
/** 处理生成链接 */
|
||||
function handleGenerate() {
|
||||
|
||||
if (!linkBase.value) {
|
||||
$message.warning('请输入链接模板');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mode.value) {
|
||||
case 'as':
|
||||
linkResult.value = generateSeq('as');
|
||||
break;
|
||||
case 'ps':
|
||||
linkResult.value = generateSeq('ps');
|
||||
break;
|
||||
case 'lc':
|
||||
linkResult.value = generateLetter();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成数列
|
||||
* @param {string} type 类型(as - 等差数列,ps - 等比数列)
|
||||
*/
|
||||
function generateSeq(type) {
|
||||
|
||||
let linkBaseVal = linkBase.value;
|
||||
|
||||
if (!linkBaseVal) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let data = modes[type];
|
||||
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let nFirst = data.first;
|
||||
let nCount = data.count;
|
||||
let nDiff = data.diff;
|
||||
|
||||
let nResult = {
|
||||
digits: 0, // 最大位数
|
||||
numbers: [], // 生成的数值
|
||||
};
|
||||
|
||||
let links = [];
|
||||
|
||||
// 等差数列公式
|
||||
let expAS = (i) => {
|
||||
return (nFirst + (i - 1) * nDiff);
|
||||
};
|
||||
|
||||
// 等比数列公式
|
||||
let expPS = (i) => {
|
||||
return (nFirst * Math.pow(nDiff, (i - 1)));
|
||||
};
|
||||
|
||||
// 实际使用的公式
|
||||
let exp = (type === 'as' ? expAS : expPS);
|
||||
|
||||
// 生成数值
|
||||
for (let i = 1; i <= nCount; i++) {
|
||||
|
||||
// 等差数列 / 等比数列
|
||||
let n = exp(i);
|
||||
let digits = Math.abs(n).toString().length;
|
||||
|
||||
(digits > nResult.digits) && (nResult.digits = digits);
|
||||
nResult.numbers.push(n);
|
||||
|
||||
}
|
||||
|
||||
// 补零
|
||||
if (data.zero) {
|
||||
|
||||
let digits = nResult.digits;
|
||||
let base = Math.pow(10, digits);
|
||||
let numbers = nResult.numbers;
|
||||
|
||||
for (let i = 0; i < numbers.length; i++) {
|
||||
let n = numbers[i];
|
||||
if (n >= 0) {
|
||||
numbers[i] = (n / base).toFixed(digits).substr(2);
|
||||
} else {
|
||||
numbers[i] = '-' + (n / base).toFixed(digits).substr(3);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 倒序
|
||||
if (data.reverse) {
|
||||
nResult.numbers.reverse();
|
||||
}
|
||||
|
||||
// 生成链接
|
||||
nResult.numbers.forEach((n) => {
|
||||
links.push(linkBaseVal.replace(linkRegExp, n));
|
||||
});
|
||||
|
||||
return links.join('\n');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成字母变化链接
|
||||
* @returns {string} 生成结果
|
||||
*/
|
||||
function generateLetter() {
|
||||
|
||||
let linkBaseVal = linkBase.value;
|
||||
|
||||
if (!linkBaseVal) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let data = modes.lc;
|
||||
|
||||
// 编码数值
|
||||
let cStart = data.start.charCodeAt(0);
|
||||
let cEnd = data.end.charCodeAt(0);
|
||||
|
||||
let chars = [];
|
||||
let links = [];
|
||||
|
||||
// 生成字母
|
||||
if (cStart >= 65 && cStart <= 122 && cEnd >= 65 && cEnd <= 122) {
|
||||
if (cStart < cEnd) {
|
||||
for (let i = cStart; i <= cEnd; i++) {
|
||||
// 跳过符号 [ \ ] ^ _ `
|
||||
if (i >= 91 && i <= 96) {
|
||||
continue;
|
||||
}
|
||||
chars.push(String.fromCharCode(i));
|
||||
}
|
||||
} else if (cStart > cEnd) {
|
||||
$message.warning('字母先后顺序有误。注意:大写字母需要在前。');
|
||||
return '';
|
||||
} else if (cStart === cEnd) {
|
||||
$message.warning('仅有 1 条链接,无需生成。');
|
||||
return '';
|
||||
} else {
|
||||
$message.error('未知错误。');
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
$message.warning('输入有误,请检查字母范围。');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 倒序
|
||||
if (data.reverse) {
|
||||
chars.reverse();
|
||||
}
|
||||
|
||||
// 生成链接
|
||||
chars.forEach((c) => {
|
||||
links.push(linkBaseVal.replace(linkRegExp, c));
|
||||
});
|
||||
|
||||
return links.join('\n');
|
||||
|
||||
}
|
||||
|
||||
/** 处理复制结果 */
|
||||
function handleCopy() {
|
||||
if (clipboard.isSupported) {
|
||||
return clipboard.copy(linkResult.value).then(() => {
|
||||
$message.success('复制成功');
|
||||
}).catch((error) => {
|
||||
console.error('复制失败:');
|
||||
console.error(error);
|
||||
$message.error('复制失败:异常');
|
||||
});
|
||||
} else {
|
||||
$message.error('复制失败:当前浏览器不支持该操作');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理清空结果 */
|
||||
function handleClear() {
|
||||
linkResult.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tool-detail-page {
|
||||
:deep(.n-input-number) {
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
max-width: 48em;
|
||||
}
|
||||
|
||||
:deep(.n-input--textarea) {
|
||||
max-width: 48em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.n-form) {
|
||||
margin-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,153 @@
|
||||
<template>
|
||||
<div class="tool-detail-page"></div>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="配置参数">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
|
||||
<n-form-item label="生成数量:">
|
||||
<n-input-number
|
||||
v-model:value="info.count"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="选项:">
|
||||
<n-flex>
|
||||
<n-checkbox
|
||||
v-model:checked="info.options.withSplit"
|
||||
>是否包含分隔符“-”</n-checkbox>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 操作 -->
|
||||
<n-card size="small" title="操作">
|
||||
<n-flex>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="handleGenerate"
|
||||
>生成 UUID V4</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!info.result"
|
||||
@click="handleCopy"
|
||||
>复制结果</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<!-- 结果 -->
|
||||
<n-card size="small" title="结果">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
|
||||
<n-form-item>
|
||||
<n-input
|
||||
v-model:value="info.result"
|
||||
placeholder="生成的 UUID 将显示在这里"
|
||||
type="textarea"
|
||||
:readonly="true"
|
||||
:rows="8"
|
||||
></n-input>
|
||||
</n-form-item>
|
||||
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NCheckbox,
|
||||
NFlex, NForm, NFormItem,
|
||||
NInput, NInputNumber,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
reactive,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
$message,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import {
|
||||
useClipboard,
|
||||
} from '@vueuse/core';
|
||||
|
||||
import {
|
||||
getUuidV4,
|
||||
} from '@/assets/js/utils';
|
||||
|
||||
const clipboard = useClipboard({
|
||||
legacy: true,
|
||||
read: false,
|
||||
});
|
||||
|
||||
const info = reactive({
|
||||
count: 1,
|
||||
options: {
|
||||
withSplit: true,
|
||||
},
|
||||
result: '',
|
||||
});
|
||||
|
||||
/** 处理复制操作 */
|
||||
function handleCopy() {
|
||||
if (clipboard.isSupported) {
|
||||
return clipboard.copy(info.result).then(() => {
|
||||
$message.success('复制成功');
|
||||
}).catch((error) => {
|
||||
console.error('复制失败:');
|
||||
console.error(error);
|
||||
$message.error('复制失败:异常');
|
||||
});
|
||||
} else {
|
||||
$message.error('复制失败:当前浏览器不支持该操作');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理生成操作 */
|
||||
function handleGenerate() {
|
||||
|
||||
let count = info.count;
|
||||
let opt = info.options;
|
||||
let results = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let uuid = getUuidV4(!opt.withSplit);
|
||||
results.push(uuid);
|
||||
}
|
||||
|
||||
info.result = results.join('\n');
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tool-detail-page {
|
||||
:deep(.n-input-number) {
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
:deep(.n-input--textarea) {
|
||||
max-width: 48em;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<n-p>若内容出现乱码,请尝试更改“文件编码”后重新打开文件。</n-p>
|
||||
</n-card>
|
||||
|
||||
<!-- 设置 -->
|
||||
<n-card size="small" title="设置">
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="配置参数">
|
||||
<n-flex>
|
||||
<!-- 文件编码 -->
|
||||
<div class="config-item">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 注意 -->
|
||||
<n-card size="small" title="注意">
|
||||
<!-- 注意事项 -->
|
||||
<n-card size="small" title="注意事项">
|
||||
<n-p>由于浏览器限制,通过 HTTPS 访问网站时只能连接带 SSL 的 WebSocket(WSS)。</n-p>
|
||||
<n-p>若需要连接不带 SSL 的 WebSocket(WS),建议下载到本地后使用。</n-p>
|
||||
</n-card>
|
||||
@@ -73,8 +73,8 @@
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<!-- 日志 -->
|
||||
<n-card size="small" title="日志">
|
||||
<!-- 日志内容 -->
|
||||
<n-card size="small" title="日志内容">
|
||||
<div
|
||||
ref="logsContentRef"
|
||||
class="logs-content"
|
||||
@@ -107,8 +107,8 @@
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 设置 -->
|
||||
<n-card size="small" title="设置">
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="配置参数">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="left"
|
||||
|
||||
@@ -13,71 +13,78 @@
|
||||
}"
|
||||
@click="isFaded = false"
|
||||
>
|
||||
<div class="controls">
|
||||
|
||||
<div class="title">开关 / Switch</div>
|
||||
|
||||
<n-switch
|
||||
:value="wakeLock.isActive.value"
|
||||
size="medium"
|
||||
:round="false"
|
||||
@update:value="handleToggleWakeLock"
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="配置参数">
|
||||
<n-form
|
||||
class="form-no-feedback"
|
||||
label-align="left"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
<template #checked>开启</template>
|
||||
<template #unchecked>关闭</template>
|
||||
</n-switch>
|
||||
|
||||
<div class="title">背景颜色 / Background Color</div>
|
||||
<n-form-item label="开关 / Switch">
|
||||
<n-switch
|
||||
:value="wakeLock.isActive.value"
|
||||
size="medium"
|
||||
:round="false"
|
||||
@update:value="handleToggleWakeLock"
|
||||
>
|
||||
<template #checked>开启</template>
|
||||
<template #unchecked>关闭</template>
|
||||
</n-switch>
|
||||
</n-form-item>
|
||||
|
||||
<n-color-picker
|
||||
v-model:value="bgColor"
|
||||
size="medium"
|
||||
:modes="['hex']"
|
||||
:show-preview="true"
|
||||
:swatches="[
|
||||
'#000000',
|
||||
'#252525',
|
||||
'#505050',
|
||||
'#808080',
|
||||
'#FFFFFF',
|
||||
]"
|
||||
/>
|
||||
<n-form-item label="背景颜色 / Background Color">
|
||||
<n-color-picker
|
||||
v-model:value="bgColor"
|
||||
size="medium"
|
||||
:modes="['hex']"
|
||||
:show-preview="true"
|
||||
:swatches="[
|
||||
'#000000',
|
||||
'#252525',
|
||||
'#505050',
|
||||
'#808080',
|
||||
'#FFFFFF',
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<div
|
||||
v-show="fullscreen.isSupported"
|
||||
class="title"
|
||||
>切换全屏 / Toggle Fullscreen</div>
|
||||
<n-form-item
|
||||
v-show="fullscreen.isSupported"
|
||||
label="切换全屏 / Toggle Fullscreen"
|
||||
>
|
||||
<n-switch
|
||||
size="medium"
|
||||
:round="false"
|
||||
:value="fullscreen.isFullscreen.value"
|
||||
@update:value="handleToggleFullscreen"
|
||||
>
|
||||
<template #checked>开启</template>
|
||||
<template #unchecked>关闭</template>
|
||||
</n-switch>
|
||||
</n-form-item>
|
||||
|
||||
<n-switch
|
||||
v-show="fullscreen.isSupported"
|
||||
size="medium"
|
||||
:round="false"
|
||||
:value="fullscreen.isFullscreen.value"
|
||||
@update:value="handleToggleFullscreen"
|
||||
>
|
||||
<template #checked>开启</template>
|
||||
<template #unchecked>关闭</template>
|
||||
</n-switch>
|
||||
<n-form-item label="隐藏界面 / Hide UI">
|
||||
<n-switch
|
||||
v-model:value="isFaded"
|
||||
size="medium"
|
||||
:round="false"
|
||||
@click.stop
|
||||
>
|
||||
<template #checked>隐藏</template>
|
||||
<template #unchecked>显示</template>
|
||||
</n-switch>
|
||||
</n-form-item>
|
||||
|
||||
<div class="title">隐藏界面 / Hide UI</div>
|
||||
|
||||
<n-switch
|
||||
v-model:value="isFaded"
|
||||
size="medium"
|
||||
:round="false"
|
||||
@click.stop
|
||||
>
|
||||
<template #checked>隐藏</template>
|
||||
<template #unchecked>显示</template>
|
||||
</n-switch>
|
||||
|
||||
</div>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NColorPicker, NSwitch,
|
||||
NCard, NColorPicker, NForm, NFormItem, NSwitch,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
@@ -178,7 +185,6 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tool-detail-page {
|
||||
display: flex;
|
||||
background-color: inherit;
|
||||
transition: all 0.25s;
|
||||
user-select: none;
|
||||
@@ -190,26 +196,19 @@ onBeforeUnmount(() => {
|
||||
&.is-on {
|
||||
background-color: var(--bg-color);
|
||||
|
||||
&.is-dark-color {
|
||||
&.is-dark-color * {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
transition: opacity 0.25s;
|
||||
:deep(.n-card) {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin: auto;
|
||||
padding-bottom: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 48px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 24px;
|
||||
:deep(.n-card) {
|
||||
background-color: transparent;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
:deep(.n-color-picker) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 设置 -->
|
||||
<n-card size="small" title="设置">
|
||||
<!-- 配置参数 -->
|
||||
<n-card size="small" title="配置参数">
|
||||
<n-form
|
||||
class="form-no-feedback config-inputs"
|
||||
label-align="left"
|
||||
|
||||
@@ -1,9 +1,336 @@
|
||||
<template>
|
||||
<div class="tool-detail-page"></div>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 操作 -->
|
||||
<n-card class="actions" size="small" title="操作">
|
||||
<n-flex>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="handleRunCode"
|
||||
>执行代码</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="handleClearContent"
|
||||
>清空内容</n-button>
|
||||
<n-button
|
||||
type="default"
|
||||
@click="handleClearOutput"
|
||||
>清空输出</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<!-- 代码和结果 -->
|
||||
<div class="code-and-result">
|
||||
|
||||
<!-- 编辑器 -->
|
||||
<n-card size="small" title="编辑器">
|
||||
<div ref="editorContainer" class="editor-container"></div>
|
||||
</n-card>
|
||||
|
||||
<!-- 执行结果 -->
|
||||
<n-card size="small" title="执行结果">
|
||||
<n-scrollbar class="output-container">
|
||||
<pre class="output-content">{{ outputContent }}</pre>
|
||||
</n-scrollbar>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NFlex, NScrollbar,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
ref, shallowRef,
|
||||
onBeforeUnmount, onMounted,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
initMonacoEnvironment,
|
||||
} from '@/assets/js/monaco-editor';
|
||||
|
||||
import {
|
||||
$dialog, $message,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
/** 模块名称 */
|
||||
const PREFIX = '[RunJavaScript]';
|
||||
|
||||
/**
|
||||
* @desc 编辑器实例(注:内部不能为响应式,防止出现部分操作会导致界面卡死等异常)
|
||||
* @type {VueRef<monaco.editor.IStandaloneCodeEditor>}
|
||||
*/
|
||||
const editorInstance = shallowRef(null);
|
||||
|
||||
/** @type {VueRef<HTMLElement>} */
|
||||
const editorContainer = ref(null);
|
||||
|
||||
/** 防抖定时器 */
|
||||
const resizeTimer = ref(null);
|
||||
|
||||
/** 输出内容 */
|
||||
const outputContent = ref('');
|
||||
|
||||
/** 处理清空内容操作 */
|
||||
function handleClearContent() {
|
||||
$dialog.create({
|
||||
content: '确定要清空编辑器内容吗?',
|
||||
negativeText: '取消',
|
||||
positiveText: '确定',
|
||||
title: '确认',
|
||||
type: 'default',
|
||||
onPositiveClick: () => {
|
||||
updateEditorContent('');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理清空输出操作 */
|
||||
function handleClearOutput() {
|
||||
outputContent.value = '';
|
||||
}
|
||||
|
||||
/** 处理执行代码操作 */
|
||||
function handleRunCode() {
|
||||
|
||||
let code = editorInstance.value?.getValue() || '';
|
||||
|
||||
if (!code.trim()) {
|
||||
$message.warning('请先输入要执行的 JavaScript 代码');
|
||||
return;
|
||||
}
|
||||
|
||||
outputContent.value += '> 开始执行...\n';
|
||||
|
||||
// 创建控制台重定向
|
||||
let originalConsole = {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
info: console.info,
|
||||
};
|
||||
|
||||
// 重定向控制台输出到结果区域
|
||||
console.log = (...args) => {
|
||||
originalConsole.log(...args);
|
||||
outputContent.value += '[LOG] ' + formatConsoleArgs(args) + '\n';
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
originalConsole.error(...args);
|
||||
outputContent.value += '[ERROR] ' + formatConsoleArgs(args) + '\n';
|
||||
};
|
||||
|
||||
console.warn = (...args) => {
|
||||
originalConsole.warn(...args);
|
||||
outputContent.value += '[WARN] ' + formatConsoleArgs(args) + '\n';
|
||||
};
|
||||
|
||||
console.info = (...args) => {
|
||||
originalConsole.info(...args);
|
||||
outputContent.value += '[INFO] ' + formatConsoleArgs(args) + '\n';
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
// 执行代码
|
||||
let result = eval(code);
|
||||
|
||||
// 如果有返回值且不是 undefined,输出结果
|
||||
if (result !== undefined) {
|
||||
outputContent.value += '返回值: ' + JSON.stringify(result, null, 2) + '\n';
|
||||
}
|
||||
|
||||
outputContent.value += '> 执行完成' + '\n';
|
||||
|
||||
} catch (error) {
|
||||
outputContent.value += '错误: ' + error.message + '\n';
|
||||
} finally {
|
||||
// 恢复原控制台
|
||||
Object.assign(console, originalConsole);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** 格式化控制台参数 */
|
||||
function formatConsoleArgs(args) {
|
||||
return args.map(arg => {
|
||||
if (typeof arg === 'object') {
|
||||
return JSON.stringify(arg, null, 2);
|
||||
} else {
|
||||
return String(arg);
|
||||
}
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
/** 处理窗口大小变化 */
|
||||
function handleWindowResize() {
|
||||
clearTimeout(resizeTimer.value);
|
||||
resizeTimer.value = setTimeout(() => {
|
||||
if (editorInstance.value) {
|
||||
editorInstance.value.layout();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/** 初始化编辑器 */
|
||||
function initEditor() {
|
||||
|
||||
console.log(PREFIX, 'initEditor');
|
||||
|
||||
let container = editorContainer.value;
|
||||
let editor = null;
|
||||
let valueStr = `// 在这里输入 JavaScript 代码\nconsole.log('Hello, World!');`;
|
||||
|
||||
if (!container) {
|
||||
console.error(PREFIX, '初始化失败:元素不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
editor = monaco.editor.create(container, {
|
||||
autoDetectHighContrast: false,
|
||||
automaticLayout: false,
|
||||
contextmenu: true,
|
||||
find: {
|
||||
cursorMoveOnType: false,
|
||||
},
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
language: 'javascript',
|
||||
minimap: {
|
||||
enabled: true,
|
||||
renderCharacters: false,
|
||||
},
|
||||
mouseWheelScrollSensitivity: 2,
|
||||
stickyScroll: {
|
||||
enabled: false,
|
||||
},
|
||||
tabSize: 2,
|
||||
theme: 'vs',
|
||||
value: valueStr,
|
||||
});
|
||||
|
||||
editorInstance.value = editor;
|
||||
|
||||
}
|
||||
|
||||
/** 销毁编辑器 */
|
||||
function resetEditor() {
|
||||
|
||||
console.log(PREFIX, 'resetEditor');
|
||||
|
||||
try {
|
||||
if (editorInstance.value) {
|
||||
editorInstance.value.getModel().dispose()
|
||||
editorInstance.value.dispose();
|
||||
editorInstance.value = null;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(PREFIX, '销毁失败:');
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** 更新编辑器内容 */
|
||||
function updateEditorContent(text = '') {
|
||||
if (editorInstance.value) {
|
||||
editorInstance.value.setValue(text);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initMonacoEnvironment();
|
||||
initEditor();
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetEditor();
|
||||
window.removeEventListener('resize', handleWindowResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tool-detail-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.actions {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.code-and-result {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
height: 0;
|
||||
|
||||
.n-card {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.n-card__content) {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.n-card:first-child {
|
||||
flex-grow: 1.5;
|
||||
}
|
||||
|
||||
.n-card:last-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 640px) {
|
||||
.code-and-result {
|
||||
display: block;
|
||||
|
||||
.n-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.n-card:last-child {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: 1px solid var(--n-border-color);
|
||||
}
|
||||
|
||||
.output-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.output-content {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
background-color: #F5F5F5;
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
color: #252525;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
416
src/views/ToolboxView/Other/TimerTool.vue
Normal file
416
src/views/ToolboxView/Other/TimerTool.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div class="tool-detail-page">
|
||||
|
||||
<!-- 计时器 -->
|
||||
<n-card size="small" title="计时器">
|
||||
<div class="timer-display">
|
||||
<span class="time-main">{{ formatTimeMain(data.timeDisplay) }}</span>
|
||||
<span class="time-milliseconds">{{ formatTimeMilliseconds(data.timeDisplay) }}</span>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 操作 -->
|
||||
<n-card size="small" title="操作">
|
||||
<n-flex justify="center">
|
||||
<n-button
|
||||
type="success"
|
||||
:disabled="data.isRunning"
|
||||
@click="startTimer"
|
||||
>开始</n-button>
|
||||
<n-button
|
||||
type="warning"
|
||||
:disabled="!data.isRunning"
|
||||
@click="pauseTimer"
|
||||
>暂停</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="resetTimer"
|
||||
>重置</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="recordTime"
|
||||
>记录</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<!-- 配置参数 -->
|
||||
<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-radio-group v-model:value="data.timerMode">
|
||||
<n-radio-button label="正计时" :value="'countUp'" />
|
||||
<n-radio-button label="倒计时" :value="'countDown'" />
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<!-- 倒计时时间设置 -->
|
||||
<n-form-item label="倒计时设置" v-if="data.timerMode === 'countDown'">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
v-model:value="data.countDownTime.hours"
|
||||
:min="0"
|
||||
:max="99"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
<span class="time-separator">时</span>
|
||||
<n-input-number
|
||||
v-model:value="data.countDownTime.minutes"
|
||||
:min="0"
|
||||
:max="59"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
<span class="time-separator">分</span>
|
||||
<n-input-number
|
||||
v-model:value="data.countDownTime.seconds"
|
||||
:min="0"
|
||||
:max="59"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
></n-input-number>
|
||||
<span class="time-separator">秒</span>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 时间记录 -->
|
||||
<n-card size="small" title="时间记录">
|
||||
<div class="records-content">
|
||||
<div
|
||||
v-if="data.records.length === 0"
|
||||
class="no-records"
|
||||
>暂无记录</div>
|
||||
<div
|
||||
v-for="(record, index) in data.records"
|
||||
:key="record.id"
|
||||
class="record-item"
|
||||
>
|
||||
<span class="record-index">{{ index + 1 }}</span>
|
||||
<span class="record-time">{{ formatTime(record.time) }}</span>
|
||||
<span class="record-timestamp">{{ formatDateTime(record.timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<n-button
|
||||
type="error"
|
||||
size="small"
|
||||
class="clear-records-btn"
|
||||
@click="clearRecords"
|
||||
>清空记录</n-button>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NButton, NCard, NFlex, NForm, NFormItem, NInputGroup, NInputNumber, NRadioButton, NRadioGroup,
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
reactive, onBeforeUnmount, watch,
|
||||
} from 'vue';
|
||||
|
||||
import {
|
||||
getCommonDateTime,
|
||||
} from '@frost-utils/javascript/common/index';
|
||||
|
||||
import {
|
||||
$dialog, $notification,
|
||||
} from '@/assets/js/naive-ui';
|
||||
|
||||
/** 数据 */
|
||||
const data = reactive({
|
||||
/** 计时器模式:countUp(正计时), countDown(倒计时) */
|
||||
timerMode: 'countUp',
|
||||
/** 是否正在运行 */
|
||||
isRunning: false,
|
||||
/** 当前显示时间(毫秒) */
|
||||
timeDisplay: 0,
|
||||
/** 倒计时时间设置 */
|
||||
countDownTime: {
|
||||
hours: 0,
|
||||
minutes: 5,
|
||||
seconds: 0
|
||||
},
|
||||
/** 计时器ID */
|
||||
timerID: null,
|
||||
/** 开始时间戳 */
|
||||
startTime: 0,
|
||||
/** 已运行时间 */
|
||||
elapsedTime: 0,
|
||||
/** 时间记录 */
|
||||
records: [],
|
||||
/** 记录ID */
|
||||
recordID: 0,
|
||||
});
|
||||
|
||||
/** 格式化时间显示(主时间部分:HH:MM:SS) */
|
||||
function formatTimeMain(milliseconds) {
|
||||
|
||||
let totalSeconds = Math.floor(milliseconds / 1000);
|
||||
let hours = Math.floor(totalSeconds / 3600);
|
||||
let minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
let seconds = totalSeconds % 60;
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
}
|
||||
|
||||
/** 格式化时间显示(毫秒部分:.SSS) */
|
||||
function formatTimeMilliseconds(milliseconds) {
|
||||
let ms = milliseconds % 1000;
|
||||
return `.${ms.toString().padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
/** 格式化完整时间显示(用于记录和提示) */
|
||||
function formatTime(milliseconds) {
|
||||
return `${formatTimeMain(milliseconds)}${formatTimeMilliseconds(milliseconds)}`;
|
||||
}
|
||||
|
||||
/** 格式化日期时间 */
|
||||
function formatDateTime(timestamp) {
|
||||
return getCommonDateTime(timestamp);
|
||||
}
|
||||
|
||||
/** 开始计时器 */
|
||||
function startTimer() {
|
||||
|
||||
if (data.timerMode === 'countUp') {
|
||||
|
||||
// 正计时模式
|
||||
data.startTime = Date.now() - data.elapsedTime;
|
||||
data.timerID = setInterval(() => {
|
||||
data.elapsedTime = Date.now() - data.startTime;
|
||||
data.timeDisplay = data.elapsedTime;
|
||||
}, 10); // 每10毫秒更新一次,提高毫秒显示精度
|
||||
|
||||
} else {
|
||||
|
||||
// 倒计时模式
|
||||
if (data.timeDisplay === 0) {
|
||||
// 如果是首次开始或重置后开始,设置倒计时时间
|
||||
const { hours, minutes, seconds } = data.countDownTime;
|
||||
data.timeDisplay = (hours * 3600 + minutes * 60 + seconds) * 1000;
|
||||
}
|
||||
|
||||
data.startTime = Date.now() + data.timeDisplay;
|
||||
data.timerID = setInterval(() => {
|
||||
|
||||
data.timeDisplay = Math.max(0, data.startTime - Date.now());
|
||||
data.elapsedTime = (data.startTime - data.timeDisplay) - data.startTime + data.timeDisplay;
|
||||
|
||||
// 倒计时结束
|
||||
if (data.timeDisplay === 0) {
|
||||
pauseTimer();
|
||||
notify({
|
||||
message: '倒计时结束!',
|
||||
type: 'success',
|
||||
title: '提示'
|
||||
});
|
||||
}
|
||||
|
||||
}, 10); // 每10毫秒更新一次,提高毫秒显示精度
|
||||
|
||||
}
|
||||
|
||||
data.isRunning = true;
|
||||
|
||||
}
|
||||
|
||||
/** 暂停计时器 */
|
||||
function pauseTimer() {
|
||||
if (data.timerID) {
|
||||
clearInterval(data.timerID);
|
||||
data.timerID = null;
|
||||
data.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置计时器 */
|
||||
function resetTimer() {
|
||||
pauseTimer();
|
||||
data.elapsedTime = 0;
|
||||
data.timeDisplay = 0;
|
||||
}
|
||||
|
||||
/** 记录时间 */
|
||||
function recordTime() {
|
||||
|
||||
data.recordID += 1;
|
||||
data.records.push({
|
||||
id: data.recordID,
|
||||
time: data.timeDisplay,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
notify({
|
||||
message: `已记录时间: ${formatTime(data.timeDisplay)}`,
|
||||
type: 'info',
|
||||
title: '记录成功'
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/** 清空记录 */
|
||||
function clearRecords() {
|
||||
$dialog.create({
|
||||
content: '确定要清空所有时间记录吗?',
|
||||
negativeText: '取消',
|
||||
positiveText: '确定',
|
||||
title: '确认',
|
||||
type: 'default',
|
||||
onPositiveClick: () => {
|
||||
data.records = [];
|
||||
data.recordID = 0;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 提示信息
|
||||
* @param {object} options
|
||||
* @param {number} options.duration
|
||||
* @param {string} options.message
|
||||
* @param {string} options.title
|
||||
* @param {string} options.type
|
||||
*/
|
||||
function notify(options) {
|
||||
|
||||
let {
|
||||
duration = 3000,
|
||||
message = '',
|
||||
title = '提示',
|
||||
type = 'info',
|
||||
} = options;
|
||||
|
||||
return $notification.create({
|
||||
content: message,
|
||||
duration: duration,
|
||||
title: title,
|
||||
type: type,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/** 监听计时模式切换 */
|
||||
watch(
|
||||
() => data.timerMode,
|
||||
() => {
|
||||
// 切换模式时重置计时器
|
||||
resetTimer();
|
||||
}
|
||||
);
|
||||
|
||||
/** 监听倒计时时间设置变化 */
|
||||
watch(
|
||||
() => [data.countDownTime.hours, data.countDownTime.minutes, data.countDownTime.seconds],
|
||||
() => {
|
||||
// 只有在倒计时模式且计时器未运行时,才更新显示时间
|
||||
if (data.timerMode === 'countDown' && !data.isRunning && data.timeDisplay === 0) {
|
||||
const { hours, minutes, seconds } = data.countDownTime;
|
||||
data.timeDisplay = (hours * 3600 + minutes * 60 + seconds) * 1000;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (data.timerID) {
|
||||
clearInterval(data.timerID);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.timer-display {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
color: #2196F3;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
.time-main {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.time-milliseconds {
|
||||
font-size: 32px;
|
||||
color: #666;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.records-content {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #F0F0F0;
|
||||
border-radius: 4px;
|
||||
background-color: #FFF;
|
||||
line-height: 1.6;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed #F0F0F0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.record-index {
|
||||
width: 30px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #2196F3;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.record-timestamp {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.no-records {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-records-btn {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
}
|
||||
</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
|
||||
|
||||
@@ -20,6 +20,13 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
envPrefix: 'V_ENV_',
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
// 预构建 Monaco Editor Worker
|
||||
'monaco-editor/esm/vs/editor/editor.worker',
|
||||
'monaco-editor/esm/vs/language/json/json.worker',
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
legacy({
|
||||
polyfills: false,
|
||||
@@ -37,6 +44,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
allowedHosts: true,
|
||||
port: 9000,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user