编排添加解析路径功能

This commit is contained in:
fofolee 2025-01-05 13:08:22 +08:00
parent 00ddba20ec
commit 923fc9e4de
16 changed files with 684 additions and 34 deletions

View File

@ -1,5 +1,5 @@
const quickcomposer = {
textProcessor: require("./quickcomposer/textProcessor"),
text: require("./quickcomposer/text"),
simulate: require("./quickcomposer/simulate"),
file: require("./quickcomposer/file"),
system: require("./quickcomposer/system"),

View File

@ -1,13 +1,17 @@
const { execSync } = require("child_process");
const { exec: execAsync } = require("child_process");
const iconv = require("iconv-lite");
const os = require("os");
const util = require("util");
// 将 exec 转换为 Promise 版本
const execPromise = util.promisify(execAsync);
function getSystemEncoding() {
// Windows 默认使用 GBK/GB2312其他系统默认 UTF-8
return os.platform() === "win32" ? "gbk" : "utf8";
}
function exec(command, options = {}) {
async function exec(command, options = {}) {
try {
const {
autoEncoding = true,
@ -17,7 +21,7 @@ function exec(command, options = {}) {
} = options;
// 执行命令,总是使用 buffer 获取原始输出
const output = execSync(command, {
const { stdout: output } = await execPromise(command, {
...execOptions,
encoding: "buffer",
windowsHide,

View File

@ -1,7 +1,9 @@
const exec = require("./exec");
const os = require("./os");
const path = require("./path");
module.exports = {
exec,
os,
path,
};

View File

@ -0,0 +1,150 @@
const path = require("path");
/**
* 规范化路径
* @param {string} p 要规范化的路径
* @returns {string} 规范化后的路径
*/
async function normalize(p) {
try {
return path.normalize(p);
} catch (error) {
throw new Error(`路径规范化失败: ${error.message}`);
}
}
/**
* 连接路径片段
* @param {...string} paths 路径片段
* @returns {string} 连接后的路径
*/
async function join(...paths) {
try {
return path.join(...paths);
} catch (error) {
throw new Error(`路径连接失败: ${error.message}`);
}
}
/**
* 解析路径
* @param {string} p 要解析的路径
* @returns {Object} 解析结果包含 root, dir, base, ext, name
*/
async function parse(p) {
try {
return path.parse(p);
} catch (error) {
throw new Error(`路径解析失败: ${error.message}`);
}
}
/**
* 获取路径的目录名
* @param {string} p 路径
* @returns {string} 目录名
*/
async function dirname(p) {
try {
return path.dirname(p);
} catch (error) {
throw new Error(`获取目录名失败: ${error.message}`);
}
}
/**
* 获取路径的文件名
* @param {string} p 路径
* @param {string} [ext] 可选的扩展名如果提供则从结果中移除
* @returns {string} 文件名
*/
async function basename(p, ext) {
try {
return path.basename(p, ext);
} catch (error) {
throw new Error(`获取文件名失败: ${error.message}`);
}
}
/**
* 获取路径的扩展名
* @param {string} p 路径
* @returns {string} 扩展名
*/
async function extname(p) {
try {
return path.extname(p);
} catch (error) {
throw new Error(`获取扩展名失败: ${error.message}`);
}
}
/**
* 判断路径是否为绝对路径
* @param {string} p 路径
* @returns {boolean} 是否为绝对路径
*/
async function isAbsolute(p) {
try {
return path.isAbsolute(p);
} catch (error) {
throw new Error(`判断绝对路径失败: ${error.message}`);
}
}
/**
* 计算相对路径
* @param {string} from 起始路径
* @param {string} to 目标路径
* @returns {string} 相对路径
*/
async function relative(from, to) {
try {
return path.relative(from, to);
} catch (error) {
throw new Error(`计算相对路径失败: ${error.message}`);
}
}
/**
* 将路径解析为绝对路径
* @param {...string} paths 路径片段
* @returns {string} 解析后的绝对路径
*/
async function resolve(...paths) {
try {
return path.resolve(...paths);
} catch (error) {
throw new Error(`解析绝对路径失败: ${error.message}`);
}
}
/**
* 格式化路径对象为路径字符串
* @param {Object} pathObject 路径对象包含 dir, root, base, name, ext
* @returns {string} 格式化后的路径
*/
async function format(pathObject) {
try {
return path.format(pathObject);
} catch (error) {
throw new Error(`格式化路径失败: ${error.message}`);
}
}
module.exports = {
normalize,
join,
parse,
dirname,
basename,
extname,
isAbsolute,
relative,
resolve,
format,
sep: path.sep,
delimiter: path.delimiter,
win32: path.win32,
posix: path.posix,
};

View File

@ -0,0 +1,472 @@
<template>
<div class="path-editor">
<!-- 操作类型选择 -->
<div class="operation-cards">
<div
v-for="op in operations"
:key="op.name"
:class="['operation-card', { active: argvs.operation === op.name }]"
@click="updateArgvs('operation', op.name)"
>
<div
class="row items-center justify-center q-gutter-x-xs q-px-sm q-py-xs"
>
<q-icon
:name="op.icon"
size="16px"
:color="argvs.operation === op.name ? 'primary' : 'grey'"
/>
<div class="text-caption">{{ op.label }}</div>
</div>
</div>
</div>
<!-- 操作配置 -->
<div class="operation-options q-mt-sm">
<!-- 通用路径输入 -->
<div class="options-container">
<template
v-if="
[
'normalize',
'parse',
'dirname',
'basename',
'extname',
'isAbsolute',
].includes(argvs.operation)
"
>
<VariableInput
:model-value="argvs.path"
@update:model-value="(val) => updateArgvs('path', val)"
label="路径"
icon="folder"
/>
<!-- basename 的扩展名参数 -->
<div v-if="argvs.operation === 'basename'" class="q-mt-sm">
<VariableInput
:model-value="argvs.ext"
@update:model-value="(val) => updateArgvs('ext', val)"
label="要移除的扩展名(可选)"
icon="extension"
/>
</div>
</template>
<!-- join resolve 的多路径输入 -->
<template v-if="['join', 'resolve'].includes(argvs.operation)">
<div
v-for="(path, index) in argvs.paths"
:key="index"
class="q-mb-sm"
>
<div class="row items-center q-gutter-sm">
<div class="col">
<VariableInput
:model-value="path"
@update:model-value="(val) => updatePathAtIndex(index, val)"
:label="'路径片段 ' + (index + 1)"
icon="folder"
/>
</div>
<q-btn
v-if="index === argvs.paths.length - 1"
flat
round
dense
icon="add"
size="sm"
color="primary"
@click="addPath"
/>
<q-btn
v-if="argvs.paths.length > 1"
flat
round
dense
icon="remove"
color="negative"
size="sm"
@click="removePath(index)"
/>
</div>
</div>
</template>
<!-- relative 的起始和目标路径 -->
<template v-if="argvs.operation === 'relative'">
<VariableInput
:model-value="argvs.from"
@update:model-value="(val) => updateArgvs('from', val)"
label="起始路径"
icon="folder"
/>
<div class="q-mt-sm">
<VariableInput
:model-value="argvs.to"
@update:model-value="(val) => updateArgvs('to', val)"
label="目标路径"
icon="folder"
/>
</div>
</template>
<!-- format 的路径对象 -->
<template v-if="argvs.operation === 'format'">
<div class="row q-col-gutter-sm">
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.pathObject.root"
@update:model-value="(val) => updatePathObject('root', val)"
label="根路径"
icon="folder"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.pathObject.dir"
@update:model-value="(val) => updatePathObject('dir', val)"
label="目录"
icon="folder"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.pathObject.base"
@update:model-value="(val) => updatePathObject('base', val)"
label="基本名称"
icon="description"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.pathObject.name"
@update:model-value="(val) => updatePathObject('name', val)"
label="文件名"
icon="insert_drive_file"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.pathObject.ext"
@update:model-value="(val) => updatePathObject('ext', val)"
label="扩展名"
icon="extension"
/>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { stringifyObject, parseFunction } from "js/composer/formatString";
import VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "PathEditor",
components: {
VariableInput,
},
props: {
modelValue: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
data() {
return {
operations: [
{ name: "normalize", label: "规范化路径", icon: "straighten" },
{ name: "join", label: "连接路径", icon: "add_link" },
{ name: "parse", label: "解析路径", icon: "account_tree" },
{ name: "dirname", label: "获取目录名", icon: "folder" },
{ name: "basename", label: "获取文件名", icon: "description" },
{ name: "extname", label: "获取扩展名", icon: "extension" },
{ name: "isAbsolute", label: "判断绝对路径", icon: "check_circle" },
{ name: "relative", label: "计算相对路径", icon: "compare_arrows" },
{ name: "resolve", label: "解析绝对路径", icon: "assistant_direction" },
{ name: "format", label: "格式化路径", icon: "format_shapes" },
],
defaultArgvs: {
operation: "normalize",
path: {
value: "",
isString: true,
__varInputVal__: true,
},
paths: [
{
value: "",
isString: true,
__varInputVal__: true,
},
],
from: {
value: "",
isString: true,
__varInputVal__: true,
},
to: {
value: "",
isString: true,
__varInputVal__: true,
},
ext: {
value: "",
isString: true,
__varInputVal__: true,
},
pathObject: {
root: {
value: "",
isString: true,
__varInputVal__: true,
},
dir: {
value: "",
isString: true,
__varInputVal__: true,
},
base: {
value: "",
isString: true,
__varInputVal__: true,
},
name: {
value: "",
isString: true,
__varInputVal__: true,
},
ext: {
value: "",
isString: true,
__varInputVal__: true,
},
},
},
};
},
computed: {
argvs: {
get() {
return (
this.modelValue.argvs ||
this.parseCodeToArgvs(this.modelValue.code) || {
...this.defaultArgvs,
}
);
},
set(value) {
this.$emit("update:modelValue", {
...this.modelValue,
code: this.generateCode(value),
argvs: value,
});
},
},
pointerStyle() {
const activeIndex = this.operations.findIndex(
(op) => op.name === this.argvs.operation
);
if (activeIndex === -1) return {};
const cardWidth = 80;
const gap = 4;
const pointerWidth = 12;
const leftOffset =
(cardWidth + gap) * activeIndex + cardWidth / 2 - pointerWidth / 2;
return {
left: `${leftOffset}px`,
};
},
},
methods: {
generateCode(argvs = this.argvs) {
switch (argvs.operation) {
case "normalize":
case "parse":
case "dirname":
case "extname":
case "isAbsolute":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.path
)})`;
case "basename":
if (argvs.ext && argvs.ext.value) {
return `${this.modelValue.value}.${
argvs.operation
}(${stringifyObject(argvs.path)}, ${stringifyObject(argvs.ext)})`;
}
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.path
)})`;
case "join":
case "resolve":
return `${this.modelValue.value}.${argvs.operation}(${argvs.paths
.map((p) => stringifyObject(p))
.join(", ")})`;
case "relative":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.from
)}, ${stringifyObject(argvs.to)})`;
case "format":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.pathObject
)})`;
default:
return `${this.modelValue.value}.${argvs.operation}()`;
}
},
parseCodeToArgvs(code) {
if (!code) return null;
try {
// 使variable
const variableFormatPaths = [
"arg*", //
"arg*.**", //
];
// 使 parseFunction
const result = parseFunction(code, { variableFormatPaths });
if (!result) return this.defaultArgvs;
const operation = result.name.split(".").pop();
const [firstArg, secondArg] = result.args;
const newArgvs = {
...this.defaultArgvs,
operation,
};
switch (operation) {
case "normalize":
case "parse":
case "dirname":
case "extname":
case "isAbsolute":
newArgvs.path = firstArg;
break;
case "basename":
newArgvs.path = firstArg;
if (secondArg) {
newArgvs.ext = secondArg;
}
break;
case "join":
case "resolve":
newArgvs.paths = result.args.map((arg) => arg);
break;
case "relative":
newArgvs.from = firstArg;
newArgvs.to = secondArg;
break;
case "format":
newArgvs.pathObject = firstArg;
break;
}
return newArgvs;
} catch (e) {
console.error("解析Path参数失败:", e);
return this.defaultArgvs;
}
},
updateArgvs(key, value) {
this.argvs = {
...this.argvs,
[key]: value,
};
},
updatePathAtIndex(index, value) {
const newPaths = [...this.argvs.paths];
newPaths[index] = value;
this.updateArgvs("paths", newPaths);
},
updatePathObject(key, value) {
this.updateArgvs("pathObject", {
...this.argvs.pathObject,
[key]: value,
});
},
addPath() {
this.updateArgvs("paths", [
...this.argvs.paths,
{
value: "",
isString: true,
__varInputVal__: true,
},
]);
},
removePath(index) {
const newPaths = [...this.argvs.paths];
newPaths.splice(index, 1);
this.updateArgvs("paths", newPaths);
},
},
mounted() {
if (!this.modelValue.argvs && !this.modelValue.code) {
this.$emit("update:modelValue", {
...this.modelValue,
code: this.generateCode(this.defaultArgvs),
argvs: { ...this.defaultArgvs },
});
}
},
});
</script>
<style scoped>
.path-editor {
display: flex;
flex-direction: column;
}
.operation-cards {
display: flex;
justify-content: flex-start;
gap: 4px;
flex-wrap: wrap;
}
.operation-card {
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
border-radius: 4px;
min-width: 80px;
}
.operation-card:hover {
background: var(--q-primary-opacity-5);
}
.operation-card.active {
border-color: var(--q-primary);
background: var(--q-primary-opacity-5);
}
.options-container {
min-height: 32px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>

View File

@ -48,12 +48,12 @@
v-if="variables.length"
>
<q-list class="variable-list">
<q-item-label header class="text-subtitle2">
<q-icon name="functions" size="16px" class="q-mr-sm" />
<q-item-label header class="variable-label">
<q-icon name="functions" size="15px" />
选择变量
</q-item-label>
<q-separator />
<q-separator class="q-my-xs" />
<template v-if="variables.length">
<q-item
@ -216,21 +216,29 @@ export default defineComponent({
.variable-item {
border-radius: 4px;
margin: 2px 0;
padding: 0px 16px;
transition: all 0.3s ease;
min-height: 40px;
}
.variable-item:hover {
background: var(--q-primary-opacity-10);
}
.variable-label {
padding: 4px 8px;
display: flex;
align-items: center;
gap: 4px;
}
.variable-name {
font-size: 13px;
font-size: 12px;
font-weight: 500;
}
.variable-source {
font-size: 12px;
font-size: 11px;
opacity: 0.7;
}

View File

@ -63,3 +63,7 @@ export const SystemCommandEditor = defineAsyncComponent(() =>
export const OsEditor = defineAsyncComponent(() =>
import("components/composer/system/OsEditor.vue")
);
export const PathEditor = defineAsyncComponent(() =>
import("src/components/composer/system/PathEditor.vue")
);

View File

@ -2,7 +2,7 @@ import { fileCommands } from "./fileCommands";
import { networkCommands } from "./networkCommands";
import { systemCommands } from "./systemCommands";
import { notifyCommands } from "./notifyCommands";
import { textProcessorCommands } from "./textProcessorCommands";
import { textCommands } from "./textCommands";
import { otherCommands } from "./otherCommands";
import { simulateCommands } from "./simulateCommands";
import { controlCommands } from "./controlCommands";
@ -12,7 +12,7 @@ export const commandCategories = [
networkCommands,
systemCommands,
notifyCommands,
textProcessorCommands,
textCommands,
controlCommands,
otherCommands,
simulateCommands,

View File

@ -27,6 +27,7 @@ export const systemCommands = {
desc: "执行系统命令并返回输出结果",
component: "SystemCommandEditor",
icon: "terminal",
isAsync: true,
},
{
value: "quickcomposer.system.os",
@ -34,6 +35,15 @@ export const systemCommands = {
desc: "获取操作系统相关信息",
component: "OsEditor",
icon: "computer",
isAsync: true,
},
{
value: "quickcomposer.system.path",
label: "路径操作",
desc: "路径操作",
component: "PathEditor",
icon: "folder_path",
isAsync: true,
},
],
};

View File

@ -1,10 +1,10 @@
export const textProcessorCommands = {
export const textCommands = {
label: "文本处理",
icon: "code",
defaultOpened: false,
commands: [
{
value: "quickcomposer.textProcessor",
value: "quickcomposer.text",
label: "编解码",
desc: "文本编解码",
icon: "code",
@ -21,46 +21,46 @@ export const textProcessorCommands = {
options: [
{
label: "Base64编码",
value: "quickcomposer.textProcessor.base64Encode",
value: "quickcomposer.text.base64Encode",
},
{
label: "Base64解码",
value: "quickcomposer.textProcessor.base64Decode",
value: "quickcomposer.text.base64Decode",
},
{
label: "十六进制编码",
value: "quickcomposer.textProcessor.hexEncode",
value: "quickcomposer.text.hexEncode",
},
{
label: "十六进制解码",
value: "quickcomposer.textProcessor.hexDecode",
value: "quickcomposer.text.hexDecode",
},
{ label: "URL编码", value: "quickcomposer.textProcessor.urlEncode" },
{ label: "URL解码", value: "quickcomposer.textProcessor.urlDecode" },
{ label: "URL编码", value: "quickcomposer.text.urlEncode" },
{ label: "URL解码", value: "quickcomposer.text.urlDecode" },
{
label: "HTML编码",
value: "quickcomposer.textProcessor.htmlEncode",
value: "quickcomposer.text.htmlEncode",
},
{
label: "HTML解码",
value: "quickcomposer.textProcessor.htmlDecode",
value: "quickcomposer.text.htmlDecode",
},
],
width: 3,
},
},
{
value: "quickcomposer.textProcessor.symmetricCrypto",
value: "quickcomposer.text.symmetricCrypto",
label: "对称加解密",
component: "SymmetricCryptoEditor",
},
{
value: "quickcomposer.textProcessor.asymmetricCrypto",
value: "quickcomposer.text.asymmetricCrypto",
label: "非对称加解密",
component: "AsymmetricCryptoEditor",
},
{
value: "quickcomposer.textProcessor",
value: "quickcomposer.text",
label: "哈希计算",
desc: "计算文本的哈希值",
icon: "enhanced_encryption",
@ -75,17 +75,17 @@ export const textProcessorCommands = {
functionSelector: {
selectLabel: "哈希算法",
options: [
{ label: "MD5", value: "quickcomposer.textProcessor.md5Hash" },
{ label: "SHA1", value: "quickcomposer.textProcessor.sha1Hash" },
{ label: "SHA256", value: "quickcomposer.textProcessor.sha256Hash" },
{ label: "SHA512", value: "quickcomposer.textProcessor.sha512Hash" },
{ label: "SM3", value: "quickcomposer.textProcessor.sm3Hash" },
{ label: "MD5", value: "quickcomposer.text.md5Hash" },
{ label: "SHA1", value: "quickcomposer.text.sha1Hash" },
{ label: "SHA256", value: "quickcomposer.text.sha256Hash" },
{ label: "SHA512", value: "quickcomposer.text.sha512Hash" },
{ label: "SM3", value: "quickcomposer.text.sm3Hash" },
],
},
width: 3,
},
{
value: "quickcomposer.textProcessor.reverseString",
value: "quickcomposer.text.reverseString",
label: "字符串反转",
config: [
{
@ -97,7 +97,7 @@ export const textProcessorCommands = {
],
},
{
value: "quickcomposer.textProcessor.replaceString",
value: "quickcomposer.text.replaceString",
label: "字符串替换",
config: [
{
@ -124,7 +124,7 @@ export const textProcessorCommands = {
],
},
{
value: "quickcomposer.textProcessor.substring",
value: "quickcomposer.text.substring",
label: "字符串截取",
config: [
{
@ -151,7 +151,7 @@ export const textProcessorCommands = {
],
},
{
value: "quickcomposer.textProcessor.regexTransform",
value: "quickcomposer.text.regexTransform",
label: "正则提取/替换",
component: "RegexEditor",
componentProps: {