From 4c279e8309282699ba70e2ff96a5e5960b13a334 Mon Sep 17 00:00:00 2001 From: fofolee Date: Fri, 10 Jan 2025 01:37:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=9B=BE=E7=89=87=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=88=86=E7=B1=BB=EF=BC=8C=E6=94=AF=E6=8C=81=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=A4=A7=E5=B0=8F=E3=80=81=E6=97=8B=E8=BD=AC=E3=80=81?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B0=B4=E5=8D=B0=E3=80=81=E8=A3=81=E5=89=AA?= =?UTF-8?q?=E3=80=81=E6=A0=BC=E5=BC=8F=E8=BD=AC=E6=8D=A2=E3=80=81=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=BF=A1=E6=81=AF=E3=80=81png2ico?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/quickcomposer.js | 1 + plugin/lib/quickcomposer/image/image.js | 492 +++++++++++++++ plugin/lib/quickcomposer/image/index.js | 7 + plugin/lib/quickcomposer/image/png2icon.js | 36 ++ plugin/package-lock.json | 27 + plugin/package.json | 2 + .../composer/common/NumberInput.vue | 36 +- src/js/composer/commands/dataCommands.js | 6 + src/js/composer/commands/imageCommands.js | 572 ++++++++++++++++++ src/js/composer/commands/index.js | 3 +- src/js/composer/commands/otherCommands.js | 2 + src/js/composer/commands/simulateCommands.js | 8 + src/js/composer/commands/systemCommands.js | 2 + src/js/composer/commands/uiCommands.js | 4 + src/js/composer/commands/utoolsCommand.js | 2 + 15 files changed, 1195 insertions(+), 5 deletions(-) create mode 100644 plugin/lib/quickcomposer/image/image.js create mode 100644 plugin/lib/quickcomposer/image/index.js create mode 100644 plugin/lib/quickcomposer/image/png2icon.js create mode 100644 src/js/composer/commands/imageCommands.js diff --git a/plugin/lib/quickcomposer.js b/plugin/lib/quickcomposer.js index bdc6083..c705bf8 100644 --- a/plugin/lib/quickcomposer.js +++ b/plugin/lib/quickcomposer.js @@ -8,6 +8,7 @@ const quickcomposer = { math: require("./quickcomposer/math"), ui: require("./quickcomposer/ui"), audio: require("./quickcomposer/audio"), + image: require("./quickcomposer/image"), }; module.exports = quickcomposer; diff --git a/plugin/lib/quickcomposer/image/image.js b/plugin/lib/quickcomposer/image/image.js new file mode 100644 index 0000000..215640b --- /dev/null +++ b/plugin/lib/quickcomposer/image/image.js @@ -0,0 +1,492 @@ +const fs = require("fs"); +const exif = require("exif-reader"); + +/** + * 加载图片 + * @param {string} file 图片文件路径 + * @returns {Promise} 图片元素 + */ +async function loadImage(file) { + if (!fs.existsSync(file)) { + throw new Error(`图片文件不存在: ${file}`); + } + + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = `file://${file}`; + }); +} + +/** + * 读取EXIF信息 + * @param {string} file 图片文件路径 + * @returns {Promise} EXIF信息 + */ +async function readExif(file) { + try { + const buffer = fs.readFileSync(file); + + // 检查是否是JPEG文件 + if (buffer[0] !== 0xff || buffer[1] !== 0xd8) { + console.warn("不是JPEG文件"); + return null; + } + + // 查找APP1段 + let offset = 2; + while (offset < buffer.length) { + if (buffer[offset] === 0xff && buffer[offset + 1] === 0xe1) { + // 获取段长度 + const segmentLength = buffer[offset + 2] * 256 + buffer[offset + 3]; + + // 提取EXIF数据 + const exifData = buffer.slice(offset + 4, offset + 2 + segmentLength); + + // 检查是否是EXIF数据 + if (exifData.slice(0, 6).toString() === "Exif\0\0") { + try { + // 提取实际的EXIF数据(跳过Exif\0\0) + const metadata = exif(exifData.slice(6)); + return metadata; + } catch (e) { + console.warn("解析EXIF数据失败:", e); + return null; + } + } + } + offset++; + } + + console.warn("未找到EXIF数据"); + return null; + } catch (error) { + console.warn("读取EXIF信息失败:", error); + return null; + } +} + +/** + * 获取图片颜色信息 + * @param {HTMLImageElement} img 图片元素 + * @returns {Object} 颜色信息 + */ +function getColorInfo(img) { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + let r = 0, + g = 0, + b = 0, + a = 0; + + // 计算平均颜色 + for (let i = 0; i < data.length; i += 4) { + r += data[i]; + g += data[i + 1]; + b += data[i + 2]; + a += data[i + 3]; + } + + const pixels = data.length / 4; + return { + averageColor: { + r: Math.round(r / pixels), + g: Math.round(g / pixels), + b: Math.round(b / pixels), + a: Math.round(a / pixels) / 255, + }, + isTransparent: Math.round(a / pixels) < 255, + hasAlphaChannel: true, // Canvas总是包含alpha通道 + }; +} + +/** + * 分析图片信息 + * @param {string} file 图片文件路径 + * @returns {Promise} 图片信息 + */ +async function analyze(file) { + const img = await loadImage(file); + const stats = fs.statSync(file); + const ext = file.split(".").pop().toLowerCase(); + const exifData = await readExif(file); + const colorInfo = getColorInfo(img); + + // 计算图片大小的可读格式 + const formatSize = (bytes) => { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB"; + return (bytes / (1024 * 1024)).toFixed(2) + " MB"; + }; + + // 获取图片类型 + const getImageType = (ext) => { + const types = { + jpg: "JPEG图片", + jpeg: "JPEG图片", + png: "PNG图片", + webp: "WebP图片", + gif: "GIF图片", + bmp: "BMP图片", + }; + return types[ext] || "未知类型"; + }; + + // 格式化EXIF信息 + const formatExif = (exif) => { + if (!exif) return null; + return { + 相机信息: { + 制造商: exif.Image?.Make, + 型号: exif.Image?.Model, + 软件: exif.Image?.Software, + 方向: exif.Image?.Orientation, + 分辨率: { + X: exif.Image?.XResolution, + Y: exif.Image?.YResolution, + 单位: exif.Image?.ResolutionUnit, + }, + }, + 拍摄信息: { + 拍摄时间: exif.Photo?.DateTimeOriginal, + 曝光时间: exif.Photo?.ExposureTime, + 光圈值: exif.Photo?.FNumber, + ISO感光度: exif.Photo?.ISOSpeedRatings, + 焦距: exif.Photo?.FocalLength, + 焦距35mm: exif.Photo?.FocalLengthIn35mmFilm, + 闪光灯: exif.Photo?.Flash, + 白平衡: exif.Photo?.WhiteBalance, + 曝光程序: exif.Photo?.ExposureProgram, + 曝光补偿: exif.Photo?.ExposureBiasValue, + 测光模式: exif.Photo?.MeteringMode, + 亮度值: exif.Photo?.BrightnessValue, + 场景类型: exif.Photo?.SceneCaptureType, + 镜头信息: { + 制造商: exif.Photo?.LensMake, + 型号: exif.Photo?.LensModel, + }, + }, + GPS信息: exif.GPSInfo + ? { + 纬度: { + 参考: exif.GPSInfo.GPSLatitudeRef, + 值: exif.GPSInfo.GPSLatitude, + }, + 经度: { + 参考: exif.GPSInfo.GPSLongitudeRef, + 值: exif.GPSInfo.GPSLongitude, + }, + 海拔: { + 参考: exif.GPSInfo.GPSAltitudeRef, + 值: exif.GPSInfo.GPSAltitude, + }, + 时间戳: exif.GPSInfo.GPSTimeStamp, + 日期戳: exif.GPSInfo.GPSDateStamp, + 方向: { + 参考: exif.GPSInfo.GPSImgDirectionRef, + 值: exif.GPSInfo.GPSImgDirection, + }, + } + : null, + 缩略图信息: exif.Thumbnail + ? { + 宽度: exif.Thumbnail.ImageWidth, + 高度: exif.Thumbnail.ImageLength, + 压缩: exif.Thumbnail.Compression, + 方向: exif.Thumbnail.Orientation, + 分辨率: { + X: exif.Thumbnail.XResolution, + Y: exif.Thumbnail.YResolution, + 单位: exif.Thumbnail.ResolutionUnit, + }, + } + : null, + 其他信息: { + 描述: exif.Image?.ImageDescription, + 作者: exif.Image?.Artist, + 版权: exif.Image?.Copyright, + 创建时间: exif.Photo?.DateTimeDigitized, + 修改时间: exif.Image?.DateTime, + 色彩空间: exif.Photo?.ColorSpace, + 图像处理: exif.Photo?.CustomRendered, + 对比度: exif.Photo?.Contrast, + 饱和度: exif.Photo?.Saturation, + 锐度: exif.Photo?.Sharpness, + }, + }; + }; + + return { + // 基本信息 + width: img.width, // 宽度(像素) + height: img.height, // 高度(像素) + aspectRatio: img.width / img.height, // 宽高比 + resolution: img.width * img.height, // 分辨率(总像素) + + // 文件信息 + type: getImageType(ext), // 图片类型 + format: ext.toUpperCase(), // 文件格式 + size: formatSize(stats.size), // 文件大小 + bytes: stats.size, // 字节数 + + // 时间信息 + createTime: stats.birthtime, // 创建时间 + modifyTime: stats.mtime, // 修改时间 + accessTime: stats.atime, // 访问时间 + + // 路径信息 + path: file, // 完整路径 + filename: file.split("/").pop(), // 文件名 + + // 颜色信息 + colorInfo: { + averageColor: colorInfo.averageColor, // 平均颜色 + isTransparent: colorInfo.isTransparent, // 是否包含透明 + hasAlphaChannel: colorInfo.hasAlphaChannel, // 是否有Alpha通道 + }, + + // EXIF信息 + exif: formatExif(exifData), + rawExif: exifData, // 原始EXIF数据 + + // 其他信息 + naturalWidth: img.naturalWidth, // 原始宽度 + naturalHeight: img.naturalHeight, // 原始高度 + }; +} + +/** + * 调整图片大小 + * @param {string} inputFile 输入文件路径 + * @param {string} outputFile 输出文件路径 + * @param {number} width 宽度 + * @param {number} height 高度 + * @param {boolean} keepAspectRatio 保持宽高比 + * @param {number} quality 图片质量 (0-1) + */ +async function resize( + inputFile, + outputFile, + width = null, + height = null, + keepAspectRatio = true, + quality = 0.92 +) { + const img = await loadImage(inputFile); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + if (keepAspectRatio) { + if (width && !height) { + height = width * (img.height / img.width); + } else if (height && !width) { + width = height * (img.width / img.height); + } + } + + canvas.width = width || img.width; + canvas.height = height || img.height; + + // 绘制调整大小后的图片 + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + // 获取图片数据 + const format = outputFile.split(".").pop() || "jpeg"; + const dataURL = canvas.toDataURL(`image/${format}`, quality); + + // 保存文件 + const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, ""); + fs.writeFileSync(outputFile, Buffer.from(base64Data, "base64")); +} + +/** + * 旋转图片 + * @param {string} inputFile 输入文件路径 + * @param {string} outputFile 输出文件路径 + * @param {number} angle 旋转角度 + * @param {number} quality 图片质量 + */ +async function rotate(inputFile, outputFile, angle = 90, quality = 0.92) { + const img = await loadImage(inputFile); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + const angleRad = (angle * Math.PI) / 180; + const sin = Math.abs(Math.sin(angleRad)); + const cos = Math.abs(Math.cos(angleRad)); + + // 计算旋转后的画布大小 + canvas.width = img.width * cos + img.height * sin; + canvas.height = img.width * sin + img.height * cos; + + // 移动到画布中心并旋转 + ctx.translate(canvas.width / 2, canvas.height / 2); + ctx.rotate(angleRad); + ctx.drawImage(img, -img.width / 2, -img.height / 2); + + // 保存文件 + const format = outputFile.split(".").pop() || "jpeg"; + const dataURL = canvas.toDataURL(`image/${format}`, quality); + + const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, ""); + fs.writeFileSync(outputFile, Buffer.from(base64Data, "base64")); +} + +/** + * 裁剪图片 + * @param {string} inputFile 输入文件路径 + * @param {string} outputFile 输出文件路径 + * @param {number} x 起始X坐标 + * @param {number} y 起始Y坐标 + * @param {number} width 裁剪宽度 + * @param {number} height 裁剪高度 + * @param {number} quality 图片质量 + */ +async function crop( + inputFile, + outputFile, + x = 0, + y = 0, + width = null, + height = null, + quality = 0.92 +) { + const img = await loadImage(inputFile); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + width = width || img.width; + height = height || img.height; + + canvas.width = width; + canvas.height = height; + + // 绘制裁剪区域 + ctx.drawImage(img, x, y, width, height, 0, 0, width, height); + + // 保存文件 + const format = outputFile.split(".").pop() || "jpeg"; + const dataURL = canvas.toDataURL(`image/${format}`, quality); + + const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, ""); + fs.writeFileSync(outputFile, Buffer.from(base64Data, "base64")); +} + +/** + * 添加水印 + * @param {string} inputFile 输入文件路径 + * @param {string} outputFile 输出文件路径 + * @param {string} text 水印文字 + * @param {string} font 字体设置 + * @param {string} color 文字颜色 + * @param {string} position 位置(topLeft/topRight/bottomLeft/bottomRight/center) + * @param {number} margin 边距 + * @param {number} opacity 不透明度 + * @param {number} quality 图片质量 + */ +async function watermark( + inputFile, + outputFile, + text = "水印文字", + font = "24px Arial", + color = "rgba(255, 255, 255, 0.5)", + position = "bottomRight", + margin = 20, + opacity = 0.5, + quality = 0.92 +) { + const img = await loadImage(inputFile); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + canvas.width = img.width; + canvas.height = img.height; + + // 绘制原图 + ctx.drawImage(img, 0, 0); + + // 设置水印样式 + ctx.font = font; + ctx.fillStyle = color; + ctx.globalAlpha = opacity; + + const metrics = ctx.measureText(text); + const textWidth = metrics.width; + const textHeight = parseInt(ctx.font); // 近似值 + + // 计算水印位置 + let x, y; + switch (position) { + case "topLeft": + x = margin; + y = margin + textHeight; + break; + case "topRight": + x = canvas.width - textWidth - margin; + y = margin + textHeight; + break; + case "bottomLeft": + x = margin; + y = canvas.height - margin; + break; + case "bottomRight": + x = canvas.width - textWidth - margin; + y = canvas.height - margin; + break; + case "center": + default: + x = (canvas.width - textWidth) / 2; + y = (canvas.height + textHeight) / 2; + } + + // 绘制水印 + ctx.fillText(text, x, y); + + // 保存文件 + const format = outputFile.split(".").pop() || "jpeg"; + const dataURL = canvas.toDataURL(`image/${format}`, quality); + + const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, ""); + fs.writeFileSync(outputFile, Buffer.from(base64Data, "base64")); +} + +/** + * 转换图片格式 + * @param {string} inputFile 输入文件路径 + * @param {string} outputFile 输出文件路径 + * @param {string} format 输出格式 + * @param {number} quality 图片质量 + */ +async function convert(inputFile, outputFile, format = "jpeg", quality = 0.92) { + const img = await loadImage(inputFile); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + canvas.width = img.width; + canvas.height = img.height; + + // 绘制图片 + ctx.drawImage(img, 0, 0); + + // 保存为新格式 + const dataURL = canvas.toDataURL(`image/${format}`, quality); + + const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, ""); + fs.writeFileSync(outputFile, Buffer.from(base64Data, "base64")); +} + +module.exports = { + analyze, + resize, + rotate, + crop, + watermark, + convert, +}; diff --git a/plugin/lib/quickcomposer/image/index.js b/plugin/lib/quickcomposer/image/index.js new file mode 100644 index 0000000..0cc779c --- /dev/null +++ b/plugin/lib/quickcomposer/image/index.js @@ -0,0 +1,7 @@ +const image = require("./image"); +const png2icon = require("./png2icon"); + +module.exports = { + ...image, + ...png2icon, +}; diff --git a/plugin/lib/quickcomposer/image/png2icon.js b/plugin/lib/quickcomposer/image/png2icon.js new file mode 100644 index 0000000..d8f2945 --- /dev/null +++ b/plugin/lib/quickcomposer/image/png2icon.js @@ -0,0 +1,36 @@ +const png2icons = require("png2icons"); +const fs = require("fs"); +const path = require("path"); + +const pngToIcon = (input, outputDir, type = "ico") => { + if (input instanceof Array) { + input.forEach((input) => { + pngToIcon(input, outputDir, type); + }); + return; + } + let icon, outputFile, basename; + console.log(input); + if (input.startsWith("data:image/png;base64,")) { + input = Buffer.from(input.split(",")[1], "base64"); + basename = new Date().getTime().toString(); + } else { + basename = path.basename(input, ".png"); + input = fs.readFileSync(input); + } + if (type == "ico") { + icon = png2icons.createICO(input, png2icons.BICUBIC, 0, false); + outputFile = path.join(outputDir, basename + ".ico"); + } else { + icon = png2icons.createICNS(input, png2icons.BILINEAR, 0); + outputFile = path.join(outputDir, basename + ".icns"); + } + if (!icon) return; + fs.writeFile(outputFile, icon, (err) => { + if (err) throw err; + }); +}; + +module.exports = { + pngToIcon, +}; diff --git a/plugin/package-lock.json b/plugin/package-lock.json index fd2c200..4aae12c 100644 --- a/plugin/package-lock.json +++ b/plugin/package-lock.json @@ -7,8 +7,10 @@ "dependencies": { "axios": "^1.7.9", "crypto-js": "^4.2.0", + "exif-reader": "^2.0.1", "iconv-lite": "^0.6.3", "node-forge": "^1.3.1", + "png2icons": "^2.0.1", "ses": "^1.10.0", "sm-crypto": "^0.3.13", "tree-kill": "^1.2.2" @@ -64,6 +66,12 @@ "node": ">=0.4.0" } }, + "node_modules/exif-reader": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/exif-reader/-/exif-reader-2.0.1.tgz", + "integrity": "sha512-gCQ/86RiAWSjeSlalj1G99IC6XnxbwkvB91HLqhh8somj/YBtC/2xuplvyjDjlfO7NsmYREPPElu/Syuy/H52g==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -145,6 +153,15 @@ "node": ">= 6.13.0" } }, + "node_modules/png2icons": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/png2icons/-/png2icons-2.0.1.tgz", + "integrity": "sha512-GDEQJr8OG4e6JMp7mABtXFSEpgJa1CCpbQiAR+EjhkHJHnUL9zPPtbOrjsMD8gUbikgv3j7x404b0YJsV3aVFA==", + "license": "MIT", + "bin": { + "png2icons": "png2icons-cli.js" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -222,6 +239,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "exif-reader": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/exif-reader/-/exif-reader-2.0.1.tgz", + "integrity": "sha512-gCQ/86RiAWSjeSlalj1G99IC6XnxbwkvB91HLqhh8somj/YBtC/2xuplvyjDjlfO7NsmYREPPElu/Syuy/H52g==" + }, "follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -268,6 +290,11 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, + "png2icons": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/png2icons/-/png2icons-2.0.1.tgz", + "integrity": "sha512-GDEQJr8OG4e6JMp7mABtXFSEpgJa1CCpbQiAR+EjhkHJHnUL9zPPtbOrjsMD8gUbikgv3j7x404b0YJsV3aVFA==" + }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", diff --git a/plugin/package.json b/plugin/package.json index 620ae80..8a1b620 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -2,8 +2,10 @@ "dependencies": { "axios": "^1.7.9", "crypto-js": "^4.2.0", + "exif-reader": "^2.0.1", "iconv-lite": "^0.6.3", "node-forge": "^1.3.1", + "png2icons": "^2.0.1", "ses": "^1.10.0", "sm-crypto": "^0.3.13", "tree-kill": "^1.2.2" diff --git a/src/components/composer/common/NumberInput.vue b/src/components/composer/common/NumberInput.vue index c8e070d..c917771 100644 --- a/src/components/composer/common/NumberInput.vue +++ b/src/components/composer/common/NumberInput.vue @@ -6,6 +6,8 @@ filled :label="label" :placeholder="placeholder" + :max="max" + :min="min" class="number-input" > @@ -64,6 +66,18 @@ export default defineComponent({ type: String, default: "", }, + max: { + type: Number, + default: 1000000000, + }, + min: { + type: Number, + default: -1000000000, + }, + step: { + type: Number, + default: 1, + }, }, emits: ["update:modelValue"], computed: { @@ -75,14 +89,28 @@ export default defineComponent({ if (value === null || value === undefined || value === "") { this.$emit("update:modelValue", null); } else { - this.$emit("update:modelValue", value); + const numValue = Number(value); + if (numValue > this.max) { + this.$emit("update:modelValue", this.max); + } else if (numValue < this.min) { + this.$emit("update:modelValue", this.min); + } else { + this.$emit("update:modelValue", numValue); + } } }, }, }, methods: { updateNumber(delta) { - this.$emit("update:modelValue", (this.localValue || 0) + delta); + const newValue = (this.localValue || 0) + delta; + if (newValue > this.max) { + this.$emit("update:modelValue", this.max); + } else if (newValue < this.min) { + this.$emit("update:modelValue", this.min); + } else { + this.$emit("update:modelValue", newValue); + } }, }, }); diff --git a/src/js/composer/commands/dataCommands.js b/src/js/composer/commands/dataCommands.js index c269fd7..22597d3 100644 --- a/src/js/composer/commands/dataCommands.js +++ b/src/js/composer/commands/dataCommands.js @@ -63,6 +63,8 @@ export const dataCommands = { key: "start", label: "起始位置", type: "numInput", + min: 0, + step: 1, icon: "first_page", width: 3, }, @@ -71,6 +73,8 @@ export const dataCommands = { label: "结束位置", type: "numInput", icon: "last_page", + min: 0, + step: 1, width: 3, }, ], @@ -155,6 +159,7 @@ export const dataCommands = { { label: "起始位置", type: "numInput", + step: 1, icon: "first_page", width: 4, }, @@ -162,6 +167,7 @@ export const dataCommands = { label: "结束位置", type: "numInput", icon: "last_page", + step: 1, width: 4, }, ], diff --git a/src/js/composer/commands/imageCommands.js b/src/js/composer/commands/imageCommands.js new file mode 100644 index 0000000..18a7ace --- /dev/null +++ b/src/js/composer/commands/imageCommands.js @@ -0,0 +1,572 @@ +import { newVarInputVal } from "js/composer/varInputValManager"; + +// 图片格式选项 +const IMAGE_FORMATS = [ + { label: "JPEG", value: "jpeg" }, + { label: "PNG", value: "png" }, + { label: "WebP", value: "webp" }, +]; + +// 水印位置选项 +const WATERMARK_POSITIONS = [ + { label: "左上角", value: "topLeft" }, + { label: "右上角", value: "topRight" }, + { label: "左下角", value: "bottomLeft" }, + { label: "右下角", value: "bottomRight" }, + { label: "居中", value: "center" }, +]; + +export const imageCommands = { + label: "图片操作", + icon: "image", + defaultOpened: false, + commands: [ + { + value: "quickcomposer.image.analyze", + label: "图片信息", + desc: "分析图片基本信息", + icon: "analytics", + isAsync: true, + config: [ + { + key: "file", + label: "图片文件", + type: "varInput", + icon: "image", + width: 12, + options: { + dialog: { + type: "open", + options: { + title: "选择图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + properties: ["openFile", "showHiddenFiles"], + }, + }, + }, + }, + ], + }, + { + value: "quickcomposer.image.resize", + label: "调整大小", + desc: "调整图片尺寸", + icon: "aspect_ratio", + isAsync: true, + config: [ + { + key: "inputFile", + label: "输入文件", + type: "varInput", + icon: "image", + width: 12, + options: { + dialog: { + type: "open", + options: { + title: "选择图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + properties: ["openFile", "showHiddenFiles"], + }, + }, + }, + }, + { + key: "outputFile", + label: "输出文件", + type: "varInput", + icon: "save", + width: 12, + options: { + dialog: { + type: "save", + options: { + title: "保存图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + }, + }, + }, + }, + { + key: "width", + label: "宽度(像素)", + type: "numInput", + icon: "compare_arrows", + width: 6, + min: 1, + step: 10, + defaultValue: "", + }, + { + key: "height", + label: "高度(像素)", + type: "numInput", + icon: "height", + width: 6, + min: 1, + step: 10, + defaultValue: "", + }, + { + key: "keepAspectRatio", + label: "保持宽高比", + type: "select", + icon: "aspect_ratio", + width: 6, + defaultValue: "true", + options: [ + { label: "是", value: "true" }, + { label: "否", value: "false" }, + ], + }, + { + key: "quality", + label: "图片质量(0-1)", + type: "numInput", + icon: "high_quality", + width: 6, + max: 1, + min: 0, + step: 0.05, + defaultValue: 0.92, + }, + ], + }, + { + value: "quickcomposer.image.rotate", + label: "旋转图片", + desc: "旋转图片角度", + icon: "rotate_right", + isAsync: true, + config: [ + { + key: "inputFile", + label: "输入文件", + type: "varInput", + icon: "image", + width: 12, + options: { + dialog: { + type: "open", + options: { + title: "选择图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + properties: ["openFile", "showHiddenFiles"], + }, + }, + }, + }, + { + key: "outputFile", + label: "输出文件", + type: "varInput", + icon: "save", + width: 12, + options: { + dialog: { + type: "save", + options: { + title: "保存图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + }, + }, + }, + }, + { + key: "angle", + label: "旋转角度", + type: "numInput", + icon: "rotate_right", + width: 6, + step: 90, + defaultValue: 90, + }, + { + key: "quality", + label: "图片质量(0-1)", + type: "numInput", + icon: "high_quality", + width: 6, + max: 1, + min: 0, + step: 0.05, + defaultValue: 0.92, + }, + ], + }, + { + value: "quickcomposer.image.crop", + label: "裁剪图片", + desc: "裁剪图片区域", + icon: "crop", + isAsync: true, + config: [ + { + key: "inputFile", + label: "输入文件", + type: "varInput", + icon: "image", + width: 12, + options: { + dialog: { + type: "open", + options: { + title: "选择图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + properties: ["openFile", "showHiddenFiles"], + }, + }, + }, + }, + { + key: "outputFile", + label: "输出文件", + type: "varInput", + icon: "save", + width: 12, + options: { + dialog: { + type: "save", + options: { + title: "保存图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + }, + }, + }, + }, + { + key: "x", + label: "起始X坐标", + type: "numInput", + icon: "arrow_right", + width: 6, + min: 0, + step: 10, + defaultValue: 0, + }, + { + key: "y", + label: "起始Y坐标", + type: "numInput", + icon: "arrow_downward", + width: 6, + min: 0, + step: 10, + defaultValue: 0, + }, + { + key: "width", + label: "裁剪宽度", + type: "numInput", + icon: "compare_arrows", + width: 6, + min: 1, + step: 10, + defaultValue: "", + }, + { + key: "height", + label: "裁剪高度", + type: "numInput", + icon: "height", + width: 6, + min: 1, + step: 10, + defaultValue: "", + }, + { + key: "quality", + label: "图片质量(0-1)", + type: "numInput", + icon: "high_quality", + width: 6, + max: 1, + min: 0, + step: 0.05, + defaultValue: 0.92, + }, + ], + }, + { + value: "quickcomposer.image.watermark", + label: "添加水印", + desc: "添加文字水印", + icon: "format_color_text", + isAsync: true, + config: [ + { + key: "inputFile", + label: "输入文件", + type: "varInput", + icon: "image", + width: 12, + options: { + dialog: { + type: "open", + options: { + title: "选择图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + properties: ["openFile", "showHiddenFiles"], + }, + }, + }, + }, + { + key: "outputFile", + label: "输出文件", + type: "varInput", + icon: "save", + width: 12, + options: { + dialog: { + type: "save", + options: { + title: "保存图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + }, + }, + }, + }, + { + key: "text", + label: "水印文字", + type: "varInput", + icon: "text_fields", + width: 12, + defaultValue: newVarInputVal("var", "水印文字"), + }, + { + key: "font", + label: "字体设置", + type: "varInput", + icon: "font_download", + width: 6, + defaultValue: newVarInputVal("var", "24px Arial"), + }, + { + key: "color", + label: "文字颜色", + type: "varInput", + icon: "format_color_text", + width: 6, + defaultValue: newVarInputVal("var", "rgba(255, 255, 255, 0.5)"), + }, + { + key: "position", + label: "位置", + type: "select", + icon: "place", + width: 6, + defaultValue: "bottomRight", + options: WATERMARK_POSITIONS, + }, + { + key: "margin", + label: "边距", + type: "numInput", + icon: "space_bar", + min: 0, + step: 10, + width: 6, + defaultValue: 20, + }, + { + key: "opacity", + label: "不透明度(0-1)", + type: "numInput", + icon: "opacity", + max: 1, + min: 0, + step: 0.05, + width: 6, + defaultValue: 0.5, + }, + { + key: "quality", + label: "图片质量(0-1)", + type: "numInput", + icon: "high_quality", + max: 1, + min: 0, + step: 0.05, + width: 6, + defaultValue: 0.92, + }, + ], + }, + { + value: "quickcomposer.image.convert", + label: "格式转换", + desc: "转换图片格式", + icon: "transform", + isAsync: true, + config: [ + { + key: "inputFile", + label: "输入文件", + type: "varInput", + icon: "image", + width: 12, + options: { + dialog: { + type: "open", + options: { + title: "选择图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + properties: ["openFile", "showHiddenFiles"], + }, + }, + }, + }, + { + key: "outputFile", + label: "输出文件", + type: "varInput", + icon: "save", + width: 12, + options: { + dialog: { + type: "save", + options: { + title: "保存图片", + filters: [ + { + name: "图片文件", + extensions: ["jpg", "jpeg", "png", "webp"], + }, + ], + }, + }, + }, + }, + { + key: "format", + label: "输出格式", + type: "select", + icon: "transform", + width: 6, + defaultValue: "jpeg", + options: IMAGE_FORMATS, + }, + { + key: "quality", + label: "图片质量(0-1)", + type: "numInput", + icon: "high_quality", + width: 6, + max: 1, + min: 0, + step: 0.05, + defaultValue: 0.92, + }, + ], + }, + { + value: "quickcomposer.image.pngToIcon", + label: "PNG转图标", + desc: "将PNG图片转换为图标", + icon: "transform", + config: [ + { + key: "inputFile", + label: "PNG路径/Base64", + type: "varInput", + icon: "image", + width: 12, + options: { + dialog: { + type: "open", + options: { + title: "选择图片", + filters: [ + { + name: "图片文件", + extensions: ["png"], + }, + ], + properties: ["openFile", "multiSelections"], + }, + }, + }, + }, + { + key: "outputDir", + label: "输出目录", + type: "varInput", + icon: "save", + width: 9, + defaultValue: newVarInputVal("str", window.utools.getPath("desktop")), + options: { + dialog: { + type: "save", + options: { + title: "选择输出目录", + defaultPath: ".", + properties: ["openDirectory"], + }, + }, + }, + }, + { + key: "type", + label: "输出格式", + type: "select", + icon: "transform", + width: 3, + defaultValue: "ico", + options: ["ico", "icns"], + }, + ], + }, + ], +}; diff --git a/src/js/composer/commands/index.js b/src/js/composer/commands/index.js index a52abaa..5c67119 100644 --- a/src/js/composer/commands/index.js +++ b/src/js/composer/commands/index.js @@ -13,13 +13,14 @@ import { userdataCommands } from "./userdataCommands"; import { utoolsCommands } from "./utoolsCommand"; import { screenCommands } from "./screenCommands"; import { audioCommands } from "./audioCommands"; -console.log(audioCommands); +import { imageCommands } from "./imageCommands"; export const commandCategories = [ fileCommands, networkCommands, systemCommands, audioCommands, + imageCommands, notifyCommands, utoolsCommands, dataCommands, diff --git a/src/js/composer/commands/otherCommands.js b/src/js/composer/commands/otherCommands.js index b7edf86..937b7b1 100644 --- a/src/js/composer/commands/otherCommands.js +++ b/src/js/composer/commands/otherCommands.js @@ -11,6 +11,8 @@ export const otherCommands = { key: "ms", label: "延迟的毫秒数", type: "numInput", + min: 0, + step: 100, icon: "schedule", defaultValue: 500, }, diff --git a/src/js/composer/commands/simulateCommands.js b/src/js/composer/commands/simulateCommands.js index 2057ea6..f5a6831 100644 --- a/src/js/composer/commands/simulateCommands.js +++ b/src/js/composer/commands/simulateCommands.js @@ -121,12 +121,16 @@ export const simulateCommands = { label: "X坐标(留空则原地点击)", icon: "drag_handle", type: "numInput", + min: 0, + step: 10, width: 6, }, { label: "Y坐标(留空则原地点击)", icon: "drag_handle", type: "numInput", + min: 0, + step: 10, width: 6, }, ], @@ -162,6 +166,8 @@ export const simulateCommands = { icon: "drag_handle", defaultValue: 0, type: "numInput", + min: 0, + step: 10, width: 6, }, { @@ -169,6 +175,8 @@ export const simulateCommands = { icon: "drag_handle", defaultValue: 0, type: "numInput", + min: 0, + step: 10, width: 6, }, ], diff --git a/src/js/composer/commands/systemCommands.js b/src/js/composer/commands/systemCommands.js index f5ee53b..a214201 100644 --- a/src/js/composer/commands/systemCommands.js +++ b/src/js/composer/commands/systemCommands.js @@ -370,6 +370,8 @@ export const systemCommands = { { label: "进程ID", type: "numInput", + min: 0, + step: 100, icon: "developer_board", width: 7, }, diff --git a/src/js/composer/commands/uiCommands.js b/src/js/composer/commands/uiCommands.js index 5619488..0d44043 100644 --- a/src/js/composer/commands/uiCommands.js +++ b/src/js/composer/commands/uiCommands.js @@ -118,6 +118,8 @@ export const uiCommands = { { label: "显示时间(ms)", type: "numInput", + min: 0, + step: 100, width: 6, placeholder: "0为手动关闭,留空按文本长度调整", }, @@ -152,6 +154,8 @@ export const uiCommands = { { label: "宽度", type: "numInput", + min: 0, + step: 100, defaultValue: 450, width: 3, placeholder: "对话框宽度", diff --git a/src/js/composer/commands/utoolsCommand.js b/src/js/composer/commands/utoolsCommand.js index a7d0267..42d8915 100644 --- a/src/js/composer/commands/utoolsCommand.js +++ b/src/js/composer/commands/utoolsCommand.js @@ -26,6 +26,8 @@ export const utoolsCommands = { key: "height", label: "高度", type: "numInput", + min: 0, + step: 100, icon: "straighten", width: 12, },