From dcaa00823b4090018aff63eda80c7d5e9f59a2d1 Mon Sep 17 00:00:00 2001 From: fofolee Date: Fri, 3 Jan 2025 15:31:56 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=96=E6=8E=92=E6=96=B0=E5=A2=9E=E6=96=87?= =?UTF-8?q?=E4=BB=B6/=E6=96=87=E4=BB=B6=E5=A4=B9=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=EF=BC=8C=E8=8E=B7=E5=8F=96=E6=96=87=E4=BB=B6=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/quickcomposer.js | 1 + plugin/lib/quickcomposer/file/index.js | 5 + plugin/lib/quickcomposer/file/operation.js | 320 ++++++++ src/components/composer/CommandComposer.vue | 10 + src/components/composer/ComposerList.vue | 2 +- .../crypto/AsymmetricCryptoEditor.vue | 4 - .../composer/crypto/SymmetricCryptoEditor.vue | 5 - .../composer/file/FileOperationEditor.vue | 699 ++++++++++++++++++ .../composer/http/AxiosConfigEditor.vue | 6 - .../composer/ubrowser/UBrowserEditor.vue | 5 - src/js/composer/cardComponents.js | 5 + src/js/composer/commands/fileCommands.js | 28 +- src/js/composer/commands/networkCommands.js | 4 +- src/js/composer/customComponentGuide.js | 133 ++++ 项目说明.json | 32 + 15 files changed, 1232 insertions(+), 27 deletions(-) create mode 100644 plugin/lib/quickcomposer/file/index.js create mode 100644 plugin/lib/quickcomposer/file/operation.js create mode 100644 src/components/composer/file/FileOperationEditor.vue create mode 100644 src/js/composer/customComponentGuide.js diff --git a/plugin/lib/quickcomposer.js b/plugin/lib/quickcomposer.js index f7d8ec8..9748dd6 100644 --- a/plugin/lib/quickcomposer.js +++ b/plugin/lib/quickcomposer.js @@ -1,6 +1,7 @@ const quickcomposer = { textProcessor: require("./quickcomposer/textProcessor"), simulate: require("./quickcomposer/simulate"), + file: require("./quickcomposer/file"), }; module.exports = quickcomposer; diff --git a/plugin/lib/quickcomposer/file/index.js b/plugin/lib/quickcomposer/file/index.js new file mode 100644 index 0000000..34e535d --- /dev/null +++ b/plugin/lib/quickcomposer/file/index.js @@ -0,0 +1,5 @@ +const operation = require("./operation"); + +module.exports = { + operation: operation.operation, +}; diff --git a/plugin/lib/quickcomposer/file/operation.js b/plugin/lib/quickcomposer/file/operation.js new file mode 100644 index 0000000..0524abe --- /dev/null +++ b/plugin/lib/quickcomposer/file/operation.js @@ -0,0 +1,320 @@ +const fs = require("fs").promises; +const path = require("path"); + +/** + * 文件读取操作 + * @param {Object} config 配置对象 + * @param {string} config.filePath 文件路径 + * @param {string} config.encoding 编码方式 + * @param {string} config.readMode 读取模式 + * @param {string} config.flag 读取标志 + * @param {number} [config.start] 起始位置 + * @param {number} [config.length] 读取长度 + * @returns {Promise} 文件内容 + */ +async function read(config) { + const { filePath, encoding, readMode, flag, start, length } = config; + + if (readMode === "all") { + return await fs.readFile(filePath, { encoding, flag }); + } else { + // 指定位置读取 + const fileHandle = await fs.open(filePath, flag); + try { + const buffer = Buffer.alloc(length); + await fileHandle.read(buffer, 0, length, start); + await fileHandle.close(); + return encoding ? buffer.toString(encoding) : buffer; + } catch (error) { + await fileHandle.close(); + throw error; + } + } +} + +/** + * 文件写入操作 + * @param {Object} config 配置对象 + * @param {string} config.filePath 文件路径 + * @param {string} config.content 写入内容 + * @param {string} config.encoding 编码方式 + * @param {string} config.flag 写入标志 + * @param {string|number} config.mode 文件权限 + * @returns {Promise} + */ +async function write(config) { + const { filePath, content, encoding, flag, mode } = config; + + // 确保目录存在 + await fs.mkdir(path.dirname(filePath), { recursive: true }); + // 将字符串模式转换为八进制数字 + const modeNum = parseInt(mode, 8); + await fs.writeFile(filePath, content, { encoding, flag, mode: modeNum }); +} + +/** + * 文件删除操作 + */ +async function delete_(config) { + const { filePath, recursive, force, targetType } = config; + + // 检查文件是否存在 + try { + const stats = await fs.lstat(filePath); + + // 检查目标类型 + if (targetType === "file" && !stats.isFile()) { + throw new Error("目标不是文件"); + } + if (targetType === "directory" && !stats.isDirectory()) { + throw new Error("目标不是目录"); + } + + // 执行删除操作 + if (stats.isDirectory()) { + await fs.rm(filePath, { recursive, force }); + } else { + await fs.unlink(filePath); + } + } catch (error) { + if (error.code === "ENOENT") { + if (!force) throw new Error("文件或目录不存在"); + } else { + throw error; + } + } +} + +/** + * 文件管理操作 + */ +async function manage(config) { + const { + filePath, + manageOperation, + newPath, + mode, + uid, + gid, + recursive, + targetType, + } = config; + + // 检查文件是否存在 + const stats = await fs.lstat(filePath); + + // 检查目标类型 + if (targetType === "file" && !stats.isFile()) { + throw new Error("目标不是文件"); + } + if (targetType === "directory" && !stats.isDirectory()) { + throw new Error("目标不是目录"); + } + + switch (manageOperation) { + case "rename": + // 确保目标目录存在 + await fs.mkdir(path.dirname(newPath), { recursive: true }); + await fs.rename(filePath, newPath); + break; + + case "chmod": + if (recursive && stats.isDirectory()) { + const walk = async (dir) => { + const files = await fs.readdir(dir); + for (const file of files) { + const curPath = path.join(dir, file); + const stat = await fs.lstat(curPath); + await fs.chmod(curPath, parseInt(mode, 8)); + if (stat.isDirectory()) { + await walk(curPath); + } + } + }; + + await fs.chmod(filePath, parseInt(mode, 8)); + await walk(filePath); + } else { + await fs.chmod(filePath, parseInt(mode, 8)); + } + break; + + case "chown": + if (recursive && stats.isDirectory()) { + await fs.chown(filePath, uid, gid); + const walk = async (dir) => { + const files = await fs.readdir(dir); + for (const file of files) { + const curPath = path.join(dir, file); + const stat = await fs.lstat(curPath); + await fs.chown(curPath, uid, gid); + if (stat.isDirectory()) { + await walk(curPath); + } + } + }; + + await fs.chown(filePath, uid, gid); + await walk(filePath); + } else { + await fs.chown(filePath, uid, gid); + } + break; + + default: + throw new Error(`不支持的操作类型: ${manageOperation}`); + } +} + +/** + * 列出目录内容 + * @param {Object} config 配置对象 + * @param {string} config.filePath 目录路径 + * @param {boolean} config.recursive 是否递归列出子目录 + * @param {boolean} config.showHidden 是否显示隐藏文件 + * @returns {Promise} 文件列表 + */ +async function list(config) { + const { filePath, recursive, showHidden } = config; + + if (recursive) { + const result = []; + const walk = async (dir) => { + const files = await fs.readdir(dir); + for (const file of files) { + if (!showHidden && file.startsWith(".")) continue; + const curPath = path.join(dir, file); + const stat = await fs.lstat(curPath); + result.push({ + path: curPath, + isDirectory: stat.isDirectory(), + isFile: stat.isFile(), + isSymbolicLink: stat.isSymbolicLink(), + }); + if (stat.isDirectory()) { + await walk(curPath); + } + } + }; + await walk(filePath); + return result; + } else { + const files = await fs.readdir(filePath); + return Promise.all( + files + .filter((file) => showHidden || !file.startsWith(".")) + .map(async (file) => { + const curPath = path.join(filePath, file); + const stat = await fs.lstat(curPath); + return { + path: curPath, + isDirectory: stat.isDirectory(), + isFile: stat.isFile(), + isSymbolicLink: stat.isSymbolicLink(), + }; + }) + ); + } +} + +/** + * 获取文件或目录状态 + * @param {Object} config 配置对象 + * @param {string} config.filePath 路径 + * @param {string} config.targetType 目标类型 + * @param {string} config.statMode 检查类型 + * @param {boolean} [config.followSymlinks] 是否跟随符号链接 + * @returns {Promise} 状态信息 + */ +async function stat(config) { + const { filePath, targetType, statMode, followSymlinks } = config; + + try { + const statFn = followSymlinks ? fs.stat : fs.lstat; + const stats = await statFn(filePath); + + // 检查目标类型是否匹配 + if (targetType === "file" && !stats.isFile()) { + throw new Error("目标不是文件"); + } + if (targetType === "directory" && !stats.isDirectory()) { + throw new Error("目标不是目录"); + } + + // 根据检查类型返回不同的信息 + if (statMode === "exists") { + return { + exists: true, + isFile: stats.isFile(), + isDirectory: stats.isDirectory(), + }; + } + + return { + exists: true, + isFile: stats.isFile(), + isDirectory: stats.isDirectory(), + isSymbolicLink: stats.isSymbolicLink(), + size: stats.size, + mode: stats.mode, + uid: stats.uid, + gid: stats.gid, + accessTime: stats.atime, + modifyTime: stats.mtime, + changeTime: stats.ctime, + birthTime: stats.birthtime, + }; + } catch (error) { + if (error.code === "ENOENT") { + return { + exists: false, + ...(statMode === "exists" && { + isFile: false, + isDirectory: false, + }), + }; + } + throw error; + } +} + +/** + * 统一的文件操作入口 + */ +async function operation(config) { + if (!config || typeof config !== "object") { + throw new Error("配置参数必须是一个对象"); + } + + const { operation } = config; + if (!operation) { + throw new Error("缺少必要的 operation 参数"); + } + + switch (operation) { + case "read": + return await read(config); + case "write": + return await write(config); + case "list": + return await list(config); + case "stat": + return await stat(config); + case "delete": + return await delete_(config); + case "manage": + return await manage(config); + default: + throw new Error(`不支持的操作类型: ${operation}`); + } +} + +module.exports = { + read, + write, + list, + stat, + delete: delete_, + manage, + operation, +}; diff --git a/src/components/composer/CommandComposer.vue b/src/components/composer/CommandComposer.vue index aaaebd6..ea709ec 100644 --- a/src/components/composer/CommandComposer.vue +++ b/src/components/composer/CommandComposer.vue @@ -238,4 +238,14 @@ export default defineComponent({ .command-composer :deep(.q-checkbox__inner) { font-size: 24px; } + +/* 暗黑模式下的标签栏背景颜色 */ +.body--dark .command-composer :deep(.q-tab), +.body--dark .command-composer :deep(.q-tab-panel) { + background-color: #303133; +} + +.body--dark .command-composer :deep(.q-tab--inactive) { + opacity: 2; +} diff --git a/src/components/composer/ComposerList.vue b/src/components/composer/ComposerList.vue index 24e85e8..394cf39 100644 --- a/src/components/composer/ComposerList.vue +++ b/src/components/composer/ComposerList.vue @@ -56,7 +56,7 @@ diff --git a/src/components/composer/crypto/AsymmetricCryptoEditor.vue b/src/components/composer/crypto/AsymmetricCryptoEditor.vue index d53ad1f..52d280f 100644 --- a/src/components/composer/crypto/AsymmetricCryptoEditor.vue +++ b/src/components/composer/crypto/AsymmetricCryptoEditor.vue @@ -415,10 +415,6 @@ export default defineComponent({ } } -.body--dark .q-tab, -.body--dark .q-tab-panel { - background-color: #303133; -} /* 确保下拉按钮内容垂直居中 */ .codec-dropdown :deep(.q-btn__content) { diff --git a/src/components/composer/crypto/SymmetricCryptoEditor.vue b/src/components/composer/crypto/SymmetricCryptoEditor.vue index afb3a79..8ad9c7f 100644 --- a/src/components/composer/crypto/SymmetricCryptoEditor.vue +++ b/src/components/composer/crypto/SymmetricCryptoEditor.vue @@ -448,11 +448,6 @@ export default defineComponent({ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } -.body--dark .q-tab, -.body--dark .q-tab-panel { - background-color: #303133; -} - /* 确保下拉按钮内容垂直居中 */ .codec-dropdown :deep(.q-btn__content) { min-height: unset; diff --git a/src/components/composer/file/FileOperationEditor.vue b/src/components/composer/file/FileOperationEditor.vue new file mode 100644 index 0000000..db8e09e --- /dev/null +++ b/src/components/composer/file/FileOperationEditor.vue @@ -0,0 +1,699 @@ + + + + + diff --git a/src/components/composer/http/AxiosConfigEditor.vue b/src/components/composer/http/AxiosConfigEditor.vue index 2952d67..2eaedf8 100644 --- a/src/components/composer/http/AxiosConfigEditor.vue +++ b/src/components/composer/http/AxiosConfigEditor.vue @@ -467,9 +467,3 @@ export default defineComponent({ }); - diff --git a/src/components/composer/ubrowser/UBrowserEditor.vue b/src/components/composer/ubrowser/UBrowserEditor.vue index 9b077bc..0a504e2 100644 --- a/src/components/composer/ubrowser/UBrowserEditor.vue +++ b/src/components/composer/ubrowser/UBrowserEditor.vue @@ -131,11 +131,6 @@ export default defineComponent({ flex-direction: column; } -/* 暗色模式 */ -.body--dark .q-tab, -.body--dark .q-tab-panel { - background-color: #303133; -} /* 调整面板内边距和布局 */ .ubrowser-panels :deep(.q-tab-panel) { diff --git a/src/js/composer/cardComponents.js b/src/js/composer/cardComponents.js index e131233..f4047f5 100644 --- a/src/js/composer/cardComponents.js +++ b/src/js/composer/cardComponents.js @@ -49,3 +49,8 @@ export const SymmetricCryptoEditor = defineAsyncComponent(() => export const AsymmetricCryptoEditor = defineAsyncComponent(() => import("components/composer/crypto/AsymmetricCryptoEditor.vue") ); + +// File Components +export const FileOperationEditor = defineAsyncComponent(() => + import("components/composer/file/FileOperationEditor.vue") +); diff --git a/src/js/composer/commands/fileCommands.js b/src/js/composer/commands/fileCommands.js index 3559345..ba2b2ce 100644 --- a/src/js/composer/commands/fileCommands.js +++ b/src/js/composer/commands/fileCommands.js @@ -4,8 +4,15 @@ export const fileCommands = { defaultOpened: true, commands: [ { - value: "open", - label: "打开文件/文件夹/软件", + value: "quickcomposer.file.operation", + label: "文件/文件夹操作", + component: "FileOperationEditor", + desc: "文件和文件夹的读写、删除、重命名等操作", + isAsync: true, + }, + { + value: "utools.shellOpenItem", + label: "默认程序打开", config: [ { key: "path", @@ -17,8 +24,8 @@ export const fileCommands = { ], }, { - value: "locate", - label: "在文件管理器中定位文件", + value: "utools.shellShowItemInFolder", + label: "文件管理器中显示", config: [ { key: "path", @@ -29,5 +36,18 @@ export const fileCommands = { }, ], }, + { + value: "utools.getFileIcon", + label: "获取文件图标", + config: [ + { + key: "path", + label: "文件或软件的绝对路径", + type: "input", + defaultValue: "", + icon: "folder_open", + }, + ], + }, ], }; diff --git a/src/js/composer/commands/networkCommands.js b/src/js/composer/commands/networkCommands.js index ed3aa85..e8f02c3 100644 --- a/src/js/composer/commands/networkCommands.js +++ b/src/js/composer/commands/networkCommands.js @@ -5,7 +5,7 @@ export const networkCommands = { commands: [ { value: "visit", - label: "用默认浏览器打开网址", + label: "默认浏览器打开网址", config: [ { key: "url", @@ -18,7 +18,7 @@ export const networkCommands = { }, { value: "utools.ubrowser.goto", - label: "用ubrowser打开网址", + label: "ubrowser打开网址", config: [ { key: "url", diff --git a/src/js/composer/customComponentGuide.js b/src/js/composer/customComponentGuide.js new file mode 100644 index 0000000..0be4b84 --- /dev/null +++ b/src/js/composer/customComponentGuide.js @@ -0,0 +1,133 @@ +/** + * Custom Component Creation Guide + * 自定义组件创建指南 + */ +const customComponentGuide = { + description: "创建自定义命令组件的完整流程", + steps: { + "1. Backend Interface": { + location: "plugin/lib/quickcomposer/xxx/yyy.js", + description: "创建具体功能实现", + requirements: { + functionDefinition: "使用独立函数而非对象方法", + asyncHandling: "使用 async/await 处理异步操作", + errorHandling: "合理的错误捕获和提示", + paramValidation: "检查必要参数是否存在", + }, + }, + "2. Interface Export": { + location: "plugin/lib/quickcomposer/xxx/index.js", + description: "导出接口给quickcomposer使用", + examples: { + singleFunction: "module.exports = { operation }", + multipleFunctions: "module.exports = { ...encoder, ...hash }", + }, + }, + "3. Interface Registration": { + location: "plugin/lib/quickcomposer.js", + description: "将接口注册到quickcomposer对象", + format: "quickcomposer.xxx = require('./quickcomposer/xxx')", + }, + "4. Component Development": { + location: "src/components/composer/xxx/YourComponent.vue", + basicStructure: { + template: "组件模板,使用quasar组件库", + script: "组件逻辑,使用Vue3 defineComponent", + style: "组件样式,建议使用scoped", + }, + keyPoints: { + variableInput: { + scenarios: [ + "需要支持变量输入的文本框", + "数字输入框(设置inputType='number')", + "需要自动处理引号的输入", + ], + props: { + vModel: "双向绑定值", + command: "配置图标和输入类型", + label: "输入框标签", + }, + events: { + description: "需要监听的事件", + list: ["@update:model-value='updateConfig' - 监听值变化并更新代码"], + }, + }, + selectInput: { + description: "选择框组件", + component: "q-select", + props: { + "v-model": "双向绑定值", + options: "选项列表", + label: "标签文本", + "emit-value": "true - 使用选项的value作为值", + "map-options": "true - 启用选项映射", + }, + events: { + "@update:model-value": "必须监听此事件以触发代码更新", + }, + tips: "所有影响代码生成的输入组件都必须在值变化时触发updateConfig", + }, + codeGeneration: { + tool: "使用formatJsonVariables处理变量", + params: { + config: "完整的配置对象", + variableFields: "需要处理的字段列表", + }, + formats: { + objectParams: { + description: "当参数是对象时的处理方式", + example: + "`quickcomposer.xxx.yyy(${formatJsonVariables(config, variableFields)})`", + reference: "参考 AxiosConfigEditor.vue", + }, + simpleParams: { + description: "当参数是简单值时的处理方式", + example: "`${functionName}(${args.join(',')})`", + reference: "参考 MultiParams.vue", + }, + }, + }, + }, + }, + "5. Component Registration": { + location: "src/js/composer/cardComponents.js", + description: "使用defineAsyncComponent注册组件", + format: + "export const YourComponent = defineAsyncComponent(() => import('path/to/component'))", + }, + "6. Command Configuration": { + location: "src/js/composer/commands/xxxCommands.js", + requiredFields: { + value: "quickcomposer.xxx.yyy", + label: "显示名称", + component: "组件名称", + }, + optionalFields: { + desc: "命令描述", + isAsync: "是否异步命令", + isControlFlow: "是否控制流命令", + allowEmptyArgv: "是否允许空参数", + }, + }, + }, + notes: { + variableHandling: { + description: "VariableInput 值的处理方式取决于参数类型", + cases: { + objectCase: + "当值在对象中时,使用 formatJsonVariables 处理,如 AxiosConfigEditor", + simpleCase: "当值是直接参数时,直接使用值本身,如 MultiParams", + }, + tips: "formatJsonVariables 主要用于处理对象中的变量,避免对简单参数使用,以免产生不必要的引号", + }, + asyncCommand: "后端使用异步函数时,命令配置需要设置isAsync: true", + componentStructure: "参考现有组件的实现方式,保持一致的代码风格", + errorHandling: "前后端都需要适当的错误处理和提示", + typeChecking: "确保所有参数都有适当的类型检查", + }, + examples: { + simpleComponent: "RegexEditor - 单一功能的组件", + complexComponent: "AxiosConfigEditor - 多功能、多配置的组件", + controlComponent: "ConditionalJudgment - 流程控制组件", + }, +}; diff --git a/项目说明.json b/项目说明.json index 7ea5d10..1ac0611 100644 --- a/项目说明.json +++ b/项目说明.json @@ -15,6 +15,7 @@ "isControlFlow": "可选,是否是流程控制命令", "commandChain": "可选,命令链,流程控制命令使用", "allowEmptyArgv": "可选,是否允许空参数", + "isAsync": "可选,是否是异步命令", "config": { "描述": "可选,命令的配置,用来在MultiParams组件中显示,是一个数组,每个元素是一个对象", "配置项属性": { @@ -47,6 +48,9 @@ "导出组件": "导出所有注册的组件供CommandComposer使用" } }, + "customComponentGuide.js": { + "描述": "自定义组件创建指南" + }, "formatString.js": { "描述": "处理JSON字符串中的值,主要处理来自VariableInput的字段,最后返回字符串", "主要功能": { @@ -72,6 +76,34 @@ "描述": "存放Vue组件", "composer": { "描述": "存放可视化编排相关的组件", + "file": { + "描述": "存放文件操作相关的组件", + "组件列表": { + "FileOperationEditor.vue": { + "描述": "文件操作编辑器组件", + "主要功能": { + "文件读取": { + "编码支持": "UTF-8、ASCII、Base64、二进制、十六进制", + "读取模式": "全部读取、指定位置读取", + "参数配置": "起始位置、读取长度" + }, + "文件写入": { + "编码支持": "UTF-8、ASCII、Base64、二进制、十六进制", + "写入模式": "覆盖写入、追加写入", + "内容输入": "支持变量和文本输入" + }, + "文件删除": { + "删除选项": "递归删除、强制删除" + }, + "文件管理": { + "重命名/移动": "支持文件重命名和移动", + "权限管理": "支持修改文件权限,支持递归修改", + "所有者管理": "支持修改文件所有者和组,支持递归修改" + } + } + } + } + }, "ui": { "描述": "基础UI组件", "组件列表": {