编排新增url/dns/buffer/zlib,新增arrayEditor组件,优化parseFunction

This commit is contained in:
fofolee 2025-01-05 22:50:21 +08:00
parent 54bb43dcc8
commit a6cc1c8737
30 changed files with 3214 additions and 243 deletions

View File

@ -3,6 +3,8 @@ const quickcomposer = {
simulate: require("./quickcomposer/simulate"),
file: require("./quickcomposer/file"),
system: require("./quickcomposer/system"),
network: require("./quickcomposer/network"),
developer: require("./quickcomposer/developer"),
};
module.exports = quickcomposer;

View File

@ -0,0 +1,140 @@
// 创建 Buffer
function from(data, encoding = "utf8") {
try {
return Buffer.from(data, encoding);
} catch (error) {
throw new Error(`创建Buffer失败: ${error.message}`);
}
}
// 转换为字符串
function toString(buffer, encoding = "utf8", start = 0, end = buffer.length) {
try {
return buffer.toString(encoding, start, end);
} catch (error) {
throw new Error(`转换字符串失败: ${error.message}`);
}
}
// 写入数据
function write(
buffer,
string,
offset = 0,
length = buffer.length,
encoding = "utf8"
) {
try {
return buffer.write(string, offset, length, encoding);
} catch (error) {
throw new Error(`写入数据失败: ${error.message}`);
}
}
// 填充数据
function fill(
buffer,
value,
offset = 0,
end = buffer.length,
encoding = "utf8"
) {
try {
return buffer.fill(value, offset, end, encoding);
} catch (error) {
throw new Error(`填充数据失败: ${error.message}`);
}
}
// 复制数据
function copy(
source,
target,
targetStart = 0,
sourceStart = 0,
sourceEnd = source.length
) {
try {
return source.copy(target, targetStart, sourceStart, sourceEnd);
} catch (error) {
throw new Error(`复制数据失败: ${error.message}`);
}
}
// 比较数据
function compare(buf1, buf2) {
try {
return Buffer.compare(buf1, buf2);
} catch (error) {
throw new Error(`比较数据失败: ${error.message}`);
}
}
// 连接 Buffer
function concat(buffers, totalLength) {
try {
return Buffer.concat(buffers, totalLength);
} catch (error) {
throw new Error(`连接Buffer失败: ${error.message}`);
}
}
// 查找数据
function indexOf(buffer, value, byteOffset = 0, encoding = "utf8") {
try {
return buffer.indexOf(value, byteOffset, encoding);
} catch (error) {
throw new Error(`查找数据失败: ${error.message}`);
}
}
// 切片数据
function slice(buffer, start = 0, end = buffer.length) {
try {
return buffer.slice(start, end);
} catch (error) {
throw new Error(`切片数据失败: ${error.message}`);
}
}
// 交换字节序
function swap(buffer, size) {
try {
switch (size) {
case 16:
return buffer.swap16();
case 32:
return buffer.swap32();
case 64:
return buffer.swap64();
default:
throw new Error("不支持的字节大小");
}
} catch (error) {
throw new Error(`交换字节序失败: ${error.message}`);
}
}
module.exports = {
from,
toString,
write,
fill,
copy,
compare,
concat,
indexOf,
slice,
swap,
// 编码类型
encodings: [
"utf8",
"utf16le",
"latin1",
"base64",
"hex",
"ascii",
"binary",
"ucs2",
],
};

View File

@ -0,0 +1,3 @@
module.exports = {
buffer: require("./buffer"),
};

View File

@ -1,5 +1,7 @@
const operation = require("./operation");
const zlib = require("./zlib");
module.exports = {
operation: operation.operation,
zlib: zlib,
};

View File

@ -0,0 +1,75 @@
const zlib = require("zlib");
const { promisify } = require("util");
// 压缩方法
const gzip = promisify(zlib.gzip);
const deflate = promisify(zlib.deflate);
const brotliCompress = promisify(zlib.brotliCompress);
// 解压方法
const gunzip = promisify(zlib.gunzip);
const inflate = promisify(zlib.inflate);
const brotliDecompress = promisify(zlib.brotliDecompress);
// 压缩选项
const defaultGzipOptions = {
level: zlib.constants.Z_DEFAULT_COMPRESSION,
memLevel: zlib.constants.Z_DEFAULT_MEMLEVEL,
strategy: zlib.constants.Z_DEFAULT_STRATEGY,
};
const defaultBrotliOptions = {
params: {
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_GENERIC,
[zlib.constants.BROTLI_PARAM_QUALITY]:
zlib.constants.BROTLI_DEFAULT_QUALITY,
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: 0,
},
};
// 异步压缩函数
async function compressData(data, method, options = {}) {
try {
const buffer = Buffer.from(data);
switch (method) {
case "gzip":
return await gzip(buffer, { ...defaultGzipOptions, ...options });
case "deflate":
return await deflate(buffer, { ...defaultGzipOptions, ...options });
case "brotli":
return await brotliCompress(buffer, {
...defaultBrotliOptions,
...options,
});
default:
throw new Error("不支持的压缩方法");
}
} catch (error) {
throw new Error(`压缩失败: ${error.message}`);
}
}
// 异步解压函数
async function decompressData(data, method, options = {}) {
try {
const buffer = Buffer.from(data);
switch (method) {
case "gzip":
return await gunzip(buffer, options);
case "deflate":
return await inflate(buffer, options);
case "brotli":
return await brotliDecompress(buffer, options);
default:
throw new Error("不支持的解压方法");
}
} catch (error) {
throw new Error(`解压失败: ${error.message}`);
}
}
module.exports = {
compressData,
decompressData,
constants: zlib.constants,
};

View File

@ -0,0 +1,106 @@
const dns = require("dns");
const { promisify } = require("util");
// 将回调函数转换为 Promise
const lookup = promisify(dns.lookup);
const resolve = promisify(dns.resolve);
const resolve4 = promisify(dns.resolve4);
const resolve6 = promisify(dns.resolve6);
const resolveMx = promisify(dns.resolveMx);
const resolveTxt = promisify(dns.resolveTxt);
const resolveNs = promisify(dns.resolveNs);
const resolveCname = promisify(dns.resolveCname);
const reverse = promisify(dns.reverse);
// 解析主机名
async function lookupHost(hostname, options = {}) {
try {
return await lookup(hostname, options);
} catch (error) {
throw new Error(`DNS查询失败: ${error.message}`);
}
}
// 解析所有记录
async function resolveAll(hostname) {
try {
return await resolve(hostname);
} catch (error) {
throw new Error(`DNS解析失败: ${error.message}`);
}
}
// 解析 IPv4 地址
async function resolveIpv4(hostname) {
try {
return await resolve4(hostname);
} catch (error) {
throw new Error(`IPv4解析失败: ${error.message}`);
}
}
// 解析 IPv6 地址
async function resolveIpv6(hostname) {
try {
return await resolve6(hostname);
} catch (error) {
throw new Error(`IPv6解析失败: ${error.message}`);
}
}
// 解析 MX 记录
async function resolveMxRecords(hostname) {
try {
return await resolveMx(hostname);
} catch (error) {
throw new Error(`MX记录解析失败: ${error.message}`);
}
}
// 解析 TXT 记录
async function resolveTxtRecords(hostname) {
try {
return await resolveTxt(hostname);
} catch (error) {
throw new Error(`TXT记录解析失败: ${error.message}`);
}
}
// 解析 NS 记录
async function resolveNsRecords(hostname) {
try {
return await resolveNs(hostname);
} catch (error) {
throw new Error(`NS记录解析失败: ${error.message}`);
}
}
// 解析 CNAME 记录
async function resolveCnameRecords(hostname) {
try {
return await resolveCname(hostname);
} catch (error) {
throw new Error(`CNAME记录解析失败: ${error.message}`);
}
}
// 反向解析 IP 地址
async function reverseResolve(ip) {
try {
return await reverse(ip);
} catch (error) {
throw new Error(`反向解析失败: ${error.message}`);
}
}
module.exports = {
lookupHost,
resolveAll,
resolveIpv4,
resolveIpv6,
resolveMxRecords,
resolveTxtRecords,
resolveNsRecords,
resolveCnameRecords,
reverseResolve,
};

View File

@ -0,0 +1,4 @@
module.exports = {
url: require("./url"),
dns: require("./dns"),
};

View File

@ -0,0 +1,98 @@
const url = require("url");
// URL 解析
function parse(urlString, parseQueryString = false) {
return url.parse(urlString, parseQueryString);
}
// URL 格式化
function format(urlObject) {
return url.format(urlObject);
}
// 解析查询字符串
function parseQuery(query) {
const searchParams = new URLSearchParams(query);
const result = {};
for (const [key, value] of searchParams) {
result[key] = value;
}
return result;
}
// 格式化查询字符串
function formatQuery(queryObject) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(queryObject)) {
searchParams.append(key, value);
}
return searchParams.toString();
}
// 解析路径名
function parsePath(path) {
return url.parse(path);
}
// 解析主机名
function parseHost(host) {
const { hostname, port } = url.parse(`http://${host}`);
return { hostname, port };
}
// 解析 URL 参数
function getQueryParam(urlString, param) {
const { query } = url.parse(urlString, true);
return query[param];
}
// 添加 URL 参数
function addQueryParam(urlString, param, value) {
const parsedUrl = url.parse(urlString, true);
parsedUrl.query[param] = value;
parsedUrl.search = null; // 清除 search以便 format 时使用 query
return url.format(parsedUrl);
}
// 移除 URL 参数
function removeQueryParam(urlString, param) {
const parsedUrl = url.parse(urlString, true);
delete parsedUrl.query[param];
parsedUrl.search = null;
return url.format(parsedUrl);
}
// 检查是否是绝对 URL
function isAbsolute(urlString) {
return url.parse(urlString).protocol !== null;
}
// 解析 URL 的各个部分
function parseComponents(urlString) {
const { protocol, auth, hostname, port, pathname, search, hash } =
url.parse(urlString);
return {
protocol: protocol?.replace(":", ""),
auth,
hostname,
port,
pathname,
search: search?.replace("?", ""),
hash: hash?.replace("#", ""),
};
}
module.exports = {
parse,
format,
parseQuery,
formatQuery,
parsePath,
parseHost,
getQueryParam,
addQueryParam,
removeQueryParam,
isAbsolute,
parseComponents,
};

View File

@ -0,0 +1 @@
module.exports = require("../file/zlib");

View File

@ -0,0 +1,126 @@
const zlib = require("zlib");
const { promisify } = require("util");
// 压缩方法
const gzip = promisify(zlib.gzip);
const deflate = promisify(zlib.deflate);
const brotliCompress = promisify(zlib.brotliCompress);
// 解压方法
const gunzip = promisify(zlib.gunzip);
const inflate = promisify(zlib.inflate);
const brotliDecompress = promisify(zlib.brotliDecompress);
// 同步方法
const gzipSync = zlib.gzipSync;
const gunzipSync = zlib.gunzipSync;
const deflateSync = zlib.deflateSync;
const inflateSync = zlib.inflateSync;
const brotliCompressSync = zlib.brotliCompressSync;
const brotliDecompressSync = zlib.brotliDecompressSync;
// 压缩选项
const defaultGzipOptions = {
level: zlib.constants.Z_DEFAULT_COMPRESSION,
memLevel: zlib.constants.Z_DEFAULT_MEMLEVEL,
strategy: zlib.constants.Z_DEFAULT_STRATEGY,
};
const defaultBrotliOptions = {
params: {
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_GENERIC,
[zlib.constants.BROTLI_PARAM_QUALITY]:
zlib.constants.BROTLI_DEFAULT_QUALITY,
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: 0,
},
};
// 异步压缩函数
async function compressData(data, method, options = {}) {
try {
const buffer = Buffer.from(data);
switch (method) {
case "gzip":
return await gzip(buffer, { ...defaultGzipOptions, ...options });
case "deflate":
return await deflate(buffer, { ...defaultGzipOptions, ...options });
case "brotli":
return await brotliCompress(buffer, {
...defaultBrotliOptions,
...options,
});
default:
throw new Error("不支持的压缩方法");
}
} catch (error) {
throw new Error(`压缩失败: ${error.message}`);
}
}
// 异步解压函数
async function decompressData(data, method, options = {}) {
try {
const buffer = Buffer.from(data);
switch (method) {
case "gzip":
return await gunzip(buffer, options);
case "deflate":
return await inflate(buffer, options);
case "brotli":
return await brotliDecompress(buffer, options);
default:
throw new Error("不支持的解压方法");
}
} catch (error) {
throw new Error(`解压失败: ${error.message}`);
}
}
// 同步压缩函数
function compressDataSync(data, method, options = {}) {
try {
const buffer = Buffer.from(data);
switch (method) {
case "gzip":
return gzipSync(buffer, { ...defaultGzipOptions, ...options });
case "deflate":
return deflateSync(buffer, { ...defaultGzipOptions, ...options });
case "brotli":
return brotliCompressSync(buffer, {
...defaultBrotliOptions,
...options,
});
default:
throw new Error("不支持的压缩方法");
}
} catch (error) {
throw new Error(`压缩失败: ${error.message}`);
}
}
// 同步解压函数
function decompressDataSync(data, method, options = {}) {
try {
const buffer = Buffer.from(data);
switch (method) {
case "gzip":
return gunzipSync(buffer, options);
case "deflate":
return inflateSync(buffer, options);
case "brotli":
return brotliDecompressSync(buffer, options);
default:
throw new Error("不支持的解压方法");
}
} catch (error) {
throw new Error(`解压失败: ${error.message}`);
}
}
module.exports = {
compressData,
decompressData,
compressDataSync,
decompressDataSync,
constants: zlib.constants,
};

View File

@ -29,7 +29,7 @@ module.exports = configure(function (ctx) {
boot: [],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
css: ["app.css"],
css: ["app.css", "composer.css"],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
@ -98,6 +98,7 @@ module.exports = configure(function (ctx) {
path.join(__dirname, "./src/plugins")
);
chain.resolve.alias.set("js", path.join(__dirname, "./src/js"));
chain.resolve.alias.set("css", path.join(__dirname, "./src/css"));
},
extendWebpack(cfg) {
cfg.optimization.splitChunks = {

View File

@ -90,7 +90,8 @@ export default defineComponent({
if (type === "load") return this.loadFlow();
const code = flow ? generateCode(flow) : generateCode(this.commandFlow);
this.$emit("use-composer", { type, code });
if (type !== "run") this.$emit("update:modelValue", false);
if (type !== "run") return this.$emit("update:modelValue", false);
if (!code.includes("console.log")) quickcommand.showMessageBox("已运行");
},
saveFlow() {
const flow = window.lodashM.cloneDeep(this.commandFlow);
@ -153,17 +154,6 @@ export default defineComponent({
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
/* 滚动美化 */
:deep(.q-scrollarea__thumb) {
width: 2px;
opacity: 0.4;
transition: opacity 0.3s ease;
}
:deep(.q-scrollarea__thumb:hover) {
opacity: 0.8;
}
/* 动画效果 */
.command-section {
transition: all 0.3s ease;
@ -176,96 +166,4 @@ export default defineComponent({
.body--dark .command-section:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* 布局更加紧凑 */
/* 输入框高度及字体 */
.command-composer :deep(.q-field--filled:not(.q-textarea) .q-field__control),
.command-composer
:deep(.q-field--filled:not(.q-textarea) .q-field__control > *),
.command-composer
:deep(.q-field--filled:not(.q-field--labeled):not(.q-textarea)
.q-field__native) {
max-height: 36px !important;
min-height: 36px !important;
}
.command-composer :deep(.q-field--filled .q-field__control),
.command-composer :deep(.q-field--filled .q-field__control > *),
.command-composer :deep(.q-field--filled .q-field__native) {
border-radius: 5px;
font-size: 12px;
}
/* 输入框图标大小 */
.command-composer :deep(.q-field--filled .q-field__control .q-icon) {
font-size: 18px;
}
/* 输入框标签字体大小,占位时的位置 */
.command-composer :deep(.q-field--filled .q-field__label) {
font-size: 11px;
top: 11px;
}
/* 输入框标签悬浮的位置 */
.command-composer :deep(.q-field--filled .q-field--float .q_field__label) {
transform: translateY(-35%) scale(0.7);
}
/* 去除filled输入框边框 */
.command-composer :deep(.q-field--filled .q-field__control:before) {
border: none;
}
/* 去除filled输入框下划线 */
.command-composer :deep(.q-field--filled .q-field__control:after) {
height: 0;
border-bottom: none;
}
/* 输入框背景颜色及内边距 */
.command-composer :deep(.q-field--filled .q-field__control) {
background: rgba(0, 0, 0, 0.03);
padding: 0 8px;
}
/* 输入框聚焦时的背景颜色 */
.command-composer
:deep(.q-field--filled.q-field--highlighted .q-field__control) {
background: rgba(0, 0, 0, 0.03);
}
/* 暗黑模式下的输入框背景颜色 */
.body--dark .command-composer :deep(.q-field--filled .q-field__control) {
background: rgba(255, 255, 255, 0.04);
}
/* 暗黑模式下输入框聚焦时的背景颜色 */
.body--dark
.command-composer
:deep(.q-field--filled.q-field--highlighted .q-field__control) {
background: rgba(255, 255, 255, 0.08);
}
/* checkbox/toggle大小及字体 */
.command-composer :deep(.q-checkbox__label),
.command-composer :deep(.q-toggle__label) {
font-size: 12px;
}
.command-composer :deep(.q-checkbox__inner),
.command-composer :deep(.q-toggle__inner) {
font-size: 28px;
margin: 4px 0px;
}
/* 暗黑模式下的标签栏背景颜色 */
.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;
}
</style>

View File

@ -0,0 +1,721 @@
<template>
<div class="buffer-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)"
:data-value="op.name"
>
<q-icon
:name="op.icon"
size="16px"
:color="argvs.operation === op.name ? 'primary' : 'grey'"
/>
<div class="text-caption">{{ op.label }}</div>
</div>
</div>
<!-- 操作配置 -->
<div class="operation-options q-mt-sm">
<div class="options-container">
<!-- 创建 Buffer -->
<div v-if="argvs.operation === 'from'" class="row q-col-gutter-sm">
<VariableInput
:model-value="argvs.data"
@update:model-value="(val) => updateArgvs('data', val)"
label="数据"
icon="text_fields"
class="col"
/>
<q-select
:model-value="argvs.encoding"
@update:model-value="(val) => updateArgvs('encoding', val)"
:options="encodings"
label="编码"
dense
filled
emit-value
map-options
options-dense
class="col-3"
/>
</div>
<!-- 转换为字符串 -->
<div
v-if="argvs.operation === 'toString'"
class="column q-col-gutter-sm"
>
<VariableInput
:model-value="argvs.buffer"
@update:model-value="(val) => updateArgvs('buffer', val)"
label="Buffer"
icon="memory"
/>
<div class="row q-col-gutter-sm">
<div class="col-12 col-sm-4">
<q-select
:model-value="argvs.encoding"
@update:model-value="(val) => updateArgvs('encoding', val)"
:options="encodings"
label="编码"
dense
filled
emit-value
map-options
options-dense
/>
</div>
<div class="col-12 col-sm-4">
<NumberInput
:model-value="argvs.start"
@update:model-value="(val) => updateArgvs('start', val)"
label="起始位置"
/>
</div>
<div class="col-12 col-sm-4">
<NumberInput
:model-value="argvs.end"
@update:model-value="(val) => updateArgvs('end', val)"
label="结束位置"
/>
</div>
</div>
</div>
<!-- 写入数据 -->
<div v-if="argvs.operation === 'write'" class="column q-col-gutter-sm">
<div class="row q-col-gutter-sm">
<VariableInput
:model-value="argvs.buffer"
@update:model-value="(val) => updateArgvs('buffer', val)"
label="Buffer"
icon="memory"
class="col"
/>
<VariableInput
:model-value="argvs.string"
@update:model-value="(val) => updateArgvs('string', val)"
label="要写入的字符串"
icon="edit"
class="col"
/>
</div>
<div class="row q-col-gutter-sm">
<div class="col-4">
<NumberInput
:model-value="argvs.offset"
@update:model-value="(val) => updateArgvs('offset', val)"
label="偏移量"
/>
</div>
<div class="col-4">
<NumberInput
:model-value="argvs.length"
@update:model-value="(val) => updateArgvs('length', val)"
label="长度"
/>
</div>
<div class="col-4">
<q-select
:model-value="argvs.encoding"
@update:model-value="(val) => updateArgvs('encoding', val)"
:options="encodings"
label="编码"
dense
filled
emit-value
map-options
options-dense
/>
</div>
</div>
</div>
<!-- 填充数据 -->
<div v-if="argvs.operation === 'fill'" class="column q-col-gutter-sm">
<div class="row q-col-gutter-sm">
<VariableInput
:model-value="argvs.buffer"
@update:model-value="(val) => updateArgvs('buffer', val)"
label="Buffer"
icon="memory"
class="col"
/>
<VariableInput
:model-value="argvs.value"
@update:model-value="(val) => updateArgvs('value', val)"
label="填充值"
icon="format_color_fill"
class="col"
/>
</div>
<div class="row q-col-gutter-sm">
<div class="col-4">
<NumberInput
:model-value="argvs.offset"
@update:model-value="(val) => updateArgvs('offset', val)"
label="起始位置"
/>
</div>
<div class="col-12 col-sm-4">
<NumberInput
:model-value="argvs.end"
@update:model-value="(val) => updateArgvs('end', val)"
label="结束位置"
/>
</div>
<div class="col-12 col-sm-4">
<q-select
:model-value="argvs.encoding"
@update:model-value="(val) => updateArgvs('encoding', val)"
:options="encodings"
label="编码"
dense
filled
emit-value
map-options
options-dense
/>
</div>
</div>
</div>
<!-- 复制数据 -->
<div v-if="argvs.operation === 'copy'" class="column q-col-gutter-sm">
<div class="row q-col-gutter-sm">
<VariableInput
:model-value="argvs.source"
@update:model-value="(val) => updateArgvs('source', val)"
label="源Buffer"
icon="content_copy"
class="col"
/>
<VariableInput
:model-value="argvs.target"
@update:model-value="(val) => updateArgvs('target', val)"
label="目标Buffer"
icon="save"
class="col"
/>
</div>
<div class="row q-col-gutter-sm">
<div class="col-4">
<NumberInput
:model-value="argvs.targetStart"
@update:model-value="(val) => updateArgvs('targetStart', val)"
label="目标起始位置"
/>
</div>
<div class="col-12 col-sm-4">
<NumberInput
:model-value="argvs.sourceStart"
@update:model-value="(val) => updateArgvs('sourceStart', val)"
label="源起始位置"
/>
</div>
<div class="col-12 col-sm-4">
<NumberInput
:model-value="argvs.sourceEnd"
@update:model-value="(val) => updateArgvs('sourceEnd', val)"
label="源结束位置"
/>
</div>
</div>
</div>
<!-- 比较数据 -->
<div
v-if="argvs.operation === 'compare'"
class="column q-col-gutter-sm"
>
<div class="row q-col-gutter-sm">
<VariableInput
:model-value="argvs.buf1"
@update:model-value="(val) => updateArgvs('buf1', val)"
label="Buffer 1"
icon="memory"
class="col"
/>
<VariableInput
:model-value="argvs.buf2"
@update:model-value="(val) => updateArgvs('buf2', val)"
label="Buffer 2"
icon="memory"
class="col"
/>
</div>
</div>
<!-- 连接 Buffer -->
<div v-if="argvs.operation === 'concat'" class="column q-gutter-sm">
<ArrayEditor
:model-value="argvs.buffers"
@update:model-value="(val) => updateArgvs('buffers', val)"
label="Buffer"
icon="memory"
/>
<NumberInput
:model-value="argvs.totalLength"
@update:model-value="(val) => updateArgvs('totalLength', val)"
label="总长度(可选)"
/>
</div>
<!-- 查找数据 -->
<div
v-if="argvs.operation === 'indexOf'"
class="column q-col-gutter-sm"
>
<div class="row q-col-gutter-sm">
<VariableInput
:model-value="argvs.buffer"
@update:model-value="(val) => updateArgvs('buffer', val)"
label="Buffer"
icon="memory"
class="col"
/>
<VariableInput
:model-value="argvs.value"
@update:model-value="(val) => updateArgvs('value', val)"
label="要查找的值"
icon="search"
class="col"
/>
</div>
<div class="row q-col-gutter-sm">
<div class="col-12 col-sm-6">
<NumberInput
:model-value="argvs.byteOffset"
@update:model-value="(val) => updateArgvs('byteOffset', val)"
label="起始位置"
/>
</div>
<div class="col-12 col-sm-6">
<q-select
:model-value="argvs.encoding"
@update:model-value="(val) => updateArgvs('encoding', val)"
:options="encodings"
label="编码"
dense
filled
emit-value
map-options
options-dense
/>
</div>
</div>
</div>
<!-- 切片数据 -->
<div v-if="argvs.operation === 'slice'" class="row q-col-gutter-sm">
<VariableInput
:model-value="argvs.buffer"
@update:model-value="(val) => updateArgvs('buffer', val)"
label="Buffer"
icon="memory"
class="col"
/>
<NumberInput
:model-value="argvs.start"
@update:model-value="(val) => updateArgvs('start', val)"
label="起始位置"
class="col"
/>
<NumberInput
:model-value="argvs.end"
@update:model-value="(val) => updateArgvs('end', val)"
label="结束位置"
class="col"
/>
</div>
<!-- 交换字节序 -->
<div v-if="argvs.operation === 'swap'" class="row q-col-gutter-sm">
<VariableInput
:model-value="argvs.buffer"
@update:model-value="(val) => updateArgvs('buffer', val)"
label="Buffer"
class="col"
icon="memory"
/>
<q-select
:model-value="argvs.size"
@update:model-value="(val) => updateArgvs('size', val)"
:options="swapSizes"
label="字节大小"
class="col-3"
dense
filled
emit-value
map-options
options-dense
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { stringifyObject, parseFunction } from "js/composer/formatString";
import VariableInput from "components/composer/ui/VariableInput.vue";
import NumberInput from "components/composer/ui/NumberInput.vue";
import ArrayEditor from "components/composer/ui/ArrayEditor.vue";
export default defineComponent({
name: "BufferEditor",
components: {
VariableInput,
NumberInput,
ArrayEditor,
},
props: {
modelValue: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
data() {
return {
operations: [
{ name: "from", label: "创建Buffer", icon: "add_box" },
{ name: "toString", label: "转换字符串", icon: "text_fields" },
{ name: "write", label: "写入数据", icon: "edit" },
{ name: "fill", label: "填充数据", icon: "format_color_fill" },
{ name: "copy", label: "复制数据", icon: "content_copy" },
{ name: "compare", label: "比较数据", icon: "compare" },
{ name: "concat", label: "连接Buffer", icon: "merge" },
{ name: "indexOf", label: "查找数据", icon: "search" },
{ name: "slice", label: "切片数据", icon: "content_cut" },
{ name: "swap", label: "交换字节序", icon: "swap_horiz" },
],
encodings: [
{ label: "UTF-8", value: "utf8" },
{ label: "UTF-16LE", value: "utf16le" },
{ label: "Latin1", value: "latin1" },
{ label: "Base64", value: "base64" },
{ label: "Hex", value: "hex" },
{ label: "ASCII", value: "ascii" },
{ label: "Binary", value: "binary" },
{ label: "UCS-2", value: "ucs2" },
],
swapSizes: [
{ label: "16位", value: 16 },
{ label: "32位", value: 32 },
{ label: "64位", value: 64 },
],
defaultArgvs: {
operation: "from",
data: {
value: "",
isString: true,
__varInputVal__: true,
},
buffer: {
value: "",
isString: false,
__varInputVal__: true,
},
encoding: "utf8",
start: 0,
end: 0,
string: {
value: "",
isString: true,
__varInputVal__: true,
},
offset: 0,
length: 0,
value: {
value: "",
isString: true,
__varInputVal__: true,
},
source: {
value: "",
isString: false,
__varInputVal__: true,
},
target: {
value: "",
isString: false,
__varInputVal__: true,
},
targetStart: 0,
sourceStart: 0,
sourceEnd: 0,
buf1: {
value: "",
isString: false,
__varInputVal__: true,
},
buf2: {
value: "",
isString: false,
__varInputVal__: true,
},
buffers: [
{
value: "",
isString: false,
__varInputVal__: true,
},
],
totalLength: undefined,
byteOffset: 0,
size: 16,
},
};
},
computed: {
argvs: {
get() {
return (
this.modelValue.argvs ||
this.parseCodeToArgvs(this.modelValue.code) || {
...this.defaultArgvs,
}
);
},
set(value) {
this.updateModelValue(value);
},
},
},
methods: {
generateCode(argvs = this.argvs) {
switch (argvs.operation) {
case "from":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.data
)}, "${argvs.encoding}")`;
case "toString":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.buffer
)}, "${argvs.encoding}", ${argvs.start}, ${argvs.end})`;
case "write":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.buffer
)}, ${stringifyObject(argvs.string)}, ${argvs.offset}, ${
argvs.length
}, "${argvs.encoding}")`;
case "fill":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.buffer
)}, ${stringifyObject(argvs.value)}, ${argvs.offset}, ${
argvs.end
}, "${argvs.encoding}")`;
case "copy":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.source
)}, ${stringifyObject(argvs.target)}, ${argvs.targetStart}, ${
argvs.sourceStart
}, ${argvs.sourceEnd})`;
case "compare":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.buf1
)}, ${stringifyObject(argvs.buf2)})`;
case "concat":
const buffersStr = argvs.buffers
.map((buf) => stringifyObject(buf))
.join(", ");
return `${this.modelValue.value}.${argvs.operation}([${buffersStr}]${
argvs.totalLength !== undefined ? `, ${argvs.totalLength}` : ""
})`;
case "indexOf":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.buffer
)}, ${stringifyObject(argvs.value)}, ${argvs.byteOffset}, "${
argvs.encoding
}")`;
case "slice":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.buffer
)}, ${argvs.start}, ${argvs.end})`;
case "swap":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.buffer
)}, ${argvs.size})`;
default:
return `${this.modelValue.value}.${argvs.operation}()`;
}
},
parseCodeToArgvs(code) {
if (!code) return null;
try {
// 使variable
const variableFormatPaths = ["arg0", "arg0[*]"];
const subFunc = code.match(/buffer\.(\w+)\((.*)\)/);
switch (subFunc[1]) {
case "write":
case "fill":
case "copy":
case "compare":
case "indexOf":
variableFormatPaths.push("arg1");
break;
}
// 使 parseFunction
const result = parseFunction(code, { variableFormatPaths });
if (!result) return this.defaultArgvs;
const operation = result.name.split(".").pop();
const args = result.args;
const newArgvs = {
...this.defaultArgvs,
operation,
};
switch (operation) {
case "from":
newArgvs.data = args[0];
newArgvs.encoding = args[1]?.value || "utf8";
break;
case "toString":
newArgvs.buffer = args[0];
newArgvs.encoding = args[1]?.value || "utf8";
newArgvs.start = args[2] ?? 0;
newArgvs.end = args[3] ?? 0;
break;
case "write":
newArgvs.buffer = args[0];
newArgvs.string = args[1];
newArgvs.offset = args[2] ?? 0;
newArgvs.length = args[3] ?? 0;
newArgvs.encoding = args[4]?.value || "utf8";
break;
case "fill":
newArgvs.buffer = args[0];
newArgvs.value = args[1];
newArgvs.offset = args[2] ?? 0;
newArgvs.end = args[3] ?? 0;
newArgvs.encoding = args[4]?.value || "utf8";
break;
case "copy":
newArgvs.source = args[0];
newArgvs.target = args[1];
newArgvs.targetStart = args[2] ?? 0;
newArgvs.sourceStart = args[3] ?? 0;
newArgvs.sourceEnd = args[4] ?? 0;
break;
case "compare":
newArgvs.buf1 = args[0];
newArgvs.buf2 = args[1];
break;
case "concat":
if (Array.isArray(args[0])) {
newArgvs.buffers = args[0];
}
newArgvs.totalLength = args[1];
break;
case "indexOf":
newArgvs.buffer = args[0];
newArgvs.value = args[1];
newArgvs.byteOffset = args[2] ?? 0;
newArgvs.encoding = args[3]?.value || "utf8";
break;
case "slice":
newArgvs.buffer = args[0];
newArgvs.start = args[1] ?? 0;
newArgvs.end = args[2] ?? 0;
break;
case "swap":
newArgvs.buffer = args[0];
newArgvs.size = args[1] ?? 16;
break;
}
return newArgvs;
} catch (e) {
console.error("解析Buffer参数失败:", e);
return this.defaultArgvs;
}
},
updateArgvs(key, value) {
this.argvs = {
...this.argvs,
[key]: value,
};
},
getSummary(argvs) {
const op = this.operations.find(
(op) => op.name === argvs.operation
)?.label;
return op;
},
updateModelValue(argvs) {
this.$emit("update:modelValue", {
...this.modelValue,
summary: this.getSummary(argvs),
argvs,
code: this.generateCode(argvs),
});
},
},
mounted() {
if (!this.modelValue.argvs && !this.modelValue.code) {
this.updateModelValue(this.defaultArgvs);
}
},
watch: {
"argvs.operation": {
immediate: true,
handler(newVal) {
this.$nextTick(() => {
document
.querySelector(`.operation-card[data-value="${newVal}"]`)
?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
});
});
},
},
},
});
</script>
<style scoped>
.buffer-editor {
display: flex;
flex-direction: column;
}
.options-container {
min-height: 32px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>

View File

@ -78,7 +78,7 @@
<template v-if="argvs.operation === 'read'">
<div class="row q-gutter-sm">
<q-select
v-model="argvs.encoding"
:model-value="argvs.encoding || 'Buffer'"
:options="encodingOptions"
label="编码"
dense
@ -392,6 +392,7 @@ const ENCODING_OPTIONS = [
{ label: "GB2312", value: "gb2312" },
{ label: "GBK", value: "gbk" },
{ label: "GB18030", value: "gb18030" },
{ label: "Buffer", value: null },
{ label: "Big5", value: "big5" },
{ label: "ASCII", value: "ascii" },
{ label: "Latin1", value: "latin1" },

View File

@ -0,0 +1,338 @@
<template>
<div class="zlib-editor">
<!-- 操作类型选择 -->
<div class="row items-center q-gutter-x-xs">
<!-- 数据输入 -->
<VariableInput
:model-value="argvs.data"
@update:model-value="(val) => updateArgvs('data', val)"
label="要处理的数据"
class="col"
icon="data_object"
/>
<div class="col-auto row items-center q-gutter-x-xs">
<div
v-for="op in operations"
:key="op.name"
:class="['operation-card', { active: argvs.operation === op.name }]"
@click="updateArgvs('operation', op.name)"
>
<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">
<div class="options-container">
<!-- 压缩选项 -->
<div class="row q-col-gutter-sm">
<!-- 压缩方法选择 -->
<q-select
:model-value="argvs.method"
@update:model-value="updateArgvs('method', $event)"
:options="methods"
label="压缩方法"
class="col-3"
dense
filled
emit-value
map-options
options-dense
/>
<!-- Gzip/Deflate 选项 -->
<template v-if="argvs.method !== 'brotli'">
<div class="col-12 col-sm-3">
<q-select
:model-value="argvs.options.level"
@update:model-value="(val) => updateArgvs('options.level', val)"
:options="compressionLevels"
label="压缩级别"
dense
filled
emit-value
map-options
options-dense
/>
</div>
<div class="col-12 col-sm-3">
<q-select
:model-value="argvs.options.memLevel"
@update:model-value="
(val) => updateArgvs('options.memLevel', val)
"
:options="memoryLevels"
label="内存级别"
dense
filled
emit-value
map-options
options-dense
/>
</div>
<div class="col-12 col-sm-3">
<q-select
:model-value="argvs.options.strategy"
@update:model-value="
(val) => updateArgvs('options.strategy', val)
"
:options="strategies"
label="压缩策略"
dense
filled
emit-value
map-options
options-dense
/>
</div>
</template>
<!-- Brotli 选项 -->
<template v-else>
<div class="col-12 col-sm-3">
<q-select
:model-value="argvs.options.params.mode"
@update:model-value="
(val) => updateArgvs('options.params.mode', val)
"
:options="brotliModes"
label="压缩模式"
dense
filled
emit-value
map-options
options-dense
/>
</div>
<div class="col-12 col-sm-3">
<q-select
:model-value="argvs.options.params.quality"
@update:model-value="
(val) => updateArgvs('options.params.quality', val)
"
:options="brotliQualities"
label="压缩质量"
dense
filled
emit-value
map-options
options-dense
/>
</div>
<div class="col-12 col-sm-3">
<NumberInput
:model-value="argvs.options.params.sizeHint"
@update:model-value="
(val) => updateArgvs('options.params.sizeHint', val)
"
label="大小提示"
/>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { stringifyObject, parseFunction } from "js/composer/formatString";
import VariableInput from "components/composer/ui/VariableInput.vue";
import NumberInput from "components/composer/ui/NumberInput.vue";
export default defineComponent({
name: "ZlibEditor",
components: {
VariableInput,
NumberInput,
},
props: {
modelValue: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
data() {
return {
operations: [
{ name: "compressData", label: "压缩", icon: "compress" },
{ name: "decompressData", label: "解压", icon: "expand" },
],
methods: [
{ label: "Gzip", value: "gzip" },
{ label: "Deflate", value: "deflate" },
{ label: "Brotli", value: "brotli" },
],
compressionLevels: [
{ label: "默认压缩", value: -1 },
{ label: "不压缩", value: 0 },
{ label: "最快压缩", value: 1 },
{ label: "最佳压缩", value: 9 },
],
memoryLevels: [
{ label: "默认内存", value: 8 },
{ label: "最小内存", value: 1 },
{ label: "最大内存", value: 9 },
],
strategies: [
{ label: "默认策略", value: 0 },
{ label: "过滤策略", value: 1 },
{ label: "哈夫曼策略", value: 2 },
{ label: "RLE策略", value: 3 },
{ label: "固定策略", value: 4 },
],
brotliModes: [
{ label: "通用模式", value: 0 },
{ label: "文本模式", value: 1 },
{ label: "字体模式", value: 2 },
],
brotliQualities: [
{ label: "默认质量", value: 11 },
{ label: "最快压缩", value: 0 },
{ label: "最佳压缩", value: 11 },
],
defaultArgvs: {
operation: "compressData",
method: "gzip",
data: {
value: "",
isString: true,
__varInputVal__: true,
},
options: {
level: -1,
memLevel: 8,
strategy: 0,
params: {
mode: 0,
quality: 11,
sizeHint: 0,
},
},
},
};
},
computed: {
argvs: {
get() {
return (
this.modelValue.argvs ||
this.parseCodeToArgvs(this.modelValue.code) || {
...this.defaultArgvs,
}
);
},
set(value) {
this.updateModelValue(value);
},
},
},
methods: {
generateCode(argvs = this.argvs) {
const options =
argvs.method === "brotli"
? { params: argvs.options.params }
: {
level: argvs.options.level,
memLevel: argvs.options.memLevel,
strategy: argvs.options.strategy,
};
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.data
)}, "${argvs.method}", ${stringifyObject(options)})`;
},
parseCodeToArgvs(code) {
if (!code) return null;
try {
// 使variable
const variableFormatPaths = [
"arg0", //
];
// 使 parseFunction
const result = parseFunction(code, { variableFormatPaths });
if (!result) return this.defaultArgvs;
const operation = result.name.split(".").pop();
const [data, method, options] = result.args;
const newArgvs = {
...this.defaultArgvs,
operation,
data,
method: method?.value || "gzip",
};
if (options) {
if (method?.value === "brotli") {
newArgvs.options = {
params: options.params || this.defaultArgvs.options.params,
};
} else {
newArgvs.options = {
level: options.level ?? this.defaultArgvs.options.level,
memLevel: options.memLevel ?? this.defaultArgvs.options.memLevel,
strategy: options.strategy ?? this.defaultArgvs.options.strategy,
};
}
}
return newArgvs;
} catch (e) {
console.error("解析Zlib参数失败:", e);
return this.defaultArgvs;
}
},
updateArgvs(key, value) {
this.argvs = {
...this.argvs,
[key]: value,
};
},
getSummary(argvs) {
const op = this.operations.find(
(op) => op.name === argvs.operation
)?.label;
const method = this.methods.find((m) => m.value === argvs.method)?.label;
return `${op} (${method})`;
},
updateModelValue(argvs) {
this.$emit("update:modelValue", {
...this.modelValue,
summary: this.getSummary(argvs),
argvs,
code: this.generateCode(argvs),
});
},
},
mounted() {
if (!this.modelValue.argvs && !this.modelValue.code) {
this.updateModelValue(this.defaultArgvs);
}
},
});
</script>
<style scoped>
.zlib-editor {
display: flex;
flex-direction: column;
gap: 8px;
}
.options-container {
min-height: 32px;
display: flex;
gap: 8px;
flex-direction: column;
justify-content: center;
}
</style>

View File

@ -0,0 +1,272 @@
<template>
<div class="dns-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)"
:data-value="op.name"
>
<q-icon
:name="op.icon"
size="16px"
:color="argvs.operation === op.name ? 'primary' : 'grey'"
/>
<div class="text-caption">{{ op.label }}</div>
</div>
</div>
<!-- 操作配置 -->
<div class="operation-options">
<div class="options-container row">
<!-- 主机名输入 -->
<VariableInput
:model-value="argvs.hostname"
@update:model-value="(val) => updateArgvs('hostname', val)"
label="主机名"
icon="dns"
class="col"
v-if="needsHostname"
/>
<!-- IP地址输入 -->
<VariableInput
:model-value="argvs.ip"
@update:model-value="(val) => updateArgvs('ip', val)"
label="IP地址"
icon="router"
v-if="argvs.operation === 'reverseResolve'"
/>
<!-- lookup 选项 -->
<div
class="row items-center col-5"
v-if="argvs.operation === 'lookupHost'"
>
<div class="col-6">
<q-select
:model-value="argvs.options.family"
@update:model-value="(val) => updateArgvs('options.family', val)"
:options="families"
label="IP版本"
dense
filled
emit-value
map-options
options-dense
/>
</div>
<div class="col-6 q-px-sm">
<q-toggle
:model-value="argvs.options.all"
@update:model-value="(val) => updateArgvs('options.all', val)"
label="返回所有地址"
dense
/>
</div>
</div>
</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: "DnsEditor",
components: {
VariableInput,
},
props: {
modelValue: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
data() {
return {
operations: [
{ name: "lookupHost", label: "DNS查询", icon: "search" },
{ name: "resolveAll", label: "解析所有记录", icon: "all_inclusive" },
{ name: "resolveIpv4", label: "解析IPv4", icon: "filter_4" },
{ name: "resolveIpv6", label: "解析IPv6", icon: "filter_6" },
{ name: "resolveMxRecords", label: "解析MX记录", icon: "mail" },
{
name: "resolveTxtRecords",
label: "解析TXT记录",
icon: "text_fields",
},
{ name: "resolveNsRecords", label: "解析NS记录", icon: "dns" },
{ name: "resolveCnameRecords", label: "解析CNAME记录", icon: "link" },
{ name: "reverseResolve", label: "反向解析", icon: "swap_horiz" },
],
families: [
{ label: "自动", value: 0 },
{ label: "IPv4", value: 4 },
{ label: "IPv6", value: 6 },
],
defaultArgvs: {
operation: "lookupHost",
hostname: {
value: "",
isString: true,
__varInputVal__: true,
},
ip: {
value: "",
isString: true,
__varInputVal__: true,
},
options: {
family: 0,
all: false,
},
},
};
},
computed: {
argvs: {
get() {
return (
this.modelValue.argvs ||
this.parseCodeToArgvs(this.modelValue.code) || {
...this.defaultArgvs,
}
);
},
set(value) {
this.updateModelValue(value);
},
},
needsHostname() {
return this.argvs.operation !== "reverseResolve";
},
},
methods: {
generateCode(argvs = this.argvs) {
switch (argvs.operation) {
case "lookupHost":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.hostname
)}, ${stringifyObject(argvs.options)})`;
case "reverseResolve":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.ip
)})`;
default:
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.hostname
)})`;
}
},
parseCodeToArgvs(code) {
if (!code) return null;
try {
// 使variable
const variableFormatPaths = [
"arg0", // /IP
"arg1.**", // options
];
// 使 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,
};
if (operation === "reverseResolve") {
newArgvs.ip = firstArg;
} else {
newArgvs.hostname = firstArg;
if (operation === "lookupHost" && secondArg) {
newArgvs.options = {
family: secondArg.family ?? this.defaultArgvs.options.family,
all: secondArg.all ?? this.defaultArgvs.options.all,
};
}
}
return newArgvs;
} catch (e) {
console.error("解析DNS参数失败:", e);
return this.defaultArgvs;
}
},
updateArgvs(key, value) {
this.argvs = {
...this.argvs,
[key]: value,
};
},
getSummary(argvs) {
const op = this.operations.find(
(op) => op.name === argvs.operation
)?.label;
return op === "反向解析"
? "反向解析 " + argvs.ip.value
: op + " " + argvs.hostname.value;
},
updateModelValue(argvs) {
this.$emit("update:modelValue", {
...this.modelValue,
summary: this.getSummary(argvs),
argvs,
code: this.generateCode(argvs),
});
},
},
mounted() {
if (!this.modelValue.argvs && !this.modelValue.code) {
this.updateModelValue(this.defaultArgvs);
}
},
watch: {
"argvs.operation": {
immediate: true,
handler(newVal) {
this.$nextTick(() => {
document
.querySelector(`.operation-card[data-value="${newVal}"]`)
?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
});
});
},
},
},
});
</script>
<style scoped>
.dns-editor {
display: flex;
flex-direction: column;
}
.options-container {
min-height: 32px;
gap: 8px;
padding-top: 8px;
}
.command-composer .operation-card {
min-width: 95px;
}
</style>

View File

@ -0,0 +1,487 @@
<template>
<div class="url-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)"
:data-value="op.name"
>
<q-icon
:name="op.icon"
size="16px"
:color="argvs.operation === op.name ? 'primary' : 'grey'"
/>
<div class="text-caption">{{ op.label }}</div>
</div>
</div>
<!-- 操作配置 -->
<div class="operation-options q-mt-sm">
<div class="options-container">
<!-- URL 输入 -->
<template v-if="needsUrlInput">
<VariableInput
:model-value="argvs.url"
@update:model-value="(val) => updateArgvs('url', val)"
label="URL"
icon="link"
/>
</template>
<!-- 查询字符串输入 -->
<template
v-if="
argvs.operation === 'parseQuery' ||
argvs.operation === 'formatQuery'
"
>
<template v-if="argvs.operation === 'parseQuery'">
<VariableInput
:model-value="argvs.query"
@update:model-value="(val) => updateArgvs('query', val)"
label="查询字符串"
icon="search"
/>
</template>
<template v-else>
<DictEditor
:model-value="argvs.queryParams"
@update:model-value="(val) => updateArgvs('queryParams', val)"
/>
</template>
</template>
<!-- 参数操作的参数输入 -->
<template
v-if="
['getQueryParam', 'addQueryParam', 'removeQueryParam'].includes(
argvs.operation
)
"
>
<VariableInput
:model-value="argvs.param"
@update:model-value="(val) => updateArgvs('param', val)"
label="参数名"
icon="key"
class="q-mt-sm"
/>
<template v-if="argvs.operation === 'addQueryParam'">
<div class="q-mt-sm">
<VariableInput
:model-value="argvs.value"
@update:model-value="(val) => updateArgvs('value', val)"
label="参数值"
icon="text_fields"
/>
</div>
</template>
</template>
<!-- 主机名输入 -->
<template v-if="argvs.operation === 'parseHost'">
<VariableInput
:model-value="argvs.host"
@update:model-value="(val) => updateArgvs('host', val)"
label="主机名"
icon="dns"
/>
</template>
<!-- 路径输入 -->
<template v-if="argvs.operation === 'parsePath'">
<VariableInput
:model-value="argvs.path"
@update:model-value="(val) => updateArgvs('path', val)"
label="路径"
icon="folder"
/>
</template>
<!-- URL 对象输入 -->
<template v-if="argvs.operation === 'format'">
<div class="row q-col-gutter-sm">
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.urlObject.protocol"
@update:model-value="(val) => updateUrlObject('protocol', val)"
label="协议"
icon="security"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.urlObject.auth"
@update:model-value="(val) => updateUrlObject('auth', val)"
label="认证信息"
icon="person"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.urlObject.hostname"
@update:model-value="(val) => updateUrlObject('hostname', val)"
label="主机名"
icon="dns"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.urlObject.port"
@update:model-value="(val) => updateUrlObject('port', val)"
label="端口"
icon="settings_ethernet"
/>
</div>
<div class="col-12">
<VariableInput
:model-value="argvs.urlObject.pathname"
@update:model-value="(val) => updateUrlObject('pathname', val)"
label="路径"
icon="folder"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.urlObject.search"
@update:model-value="(val) => updateUrlObject('search', val)"
label="查询字符串"
icon="search"
/>
</div>
<div class="col-12 col-sm-6">
<VariableInput
:model-value="argvs.urlObject.hash"
@update:model-value="(val) => updateUrlObject('hash', val)"
label="锚点"
icon="tag"
/>
</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";
import DictEditor from "components/composer/ui/DictEditor.vue";
export default defineComponent({
name: "UrlEditor",
components: {
VariableInput,
DictEditor,
},
props: {
modelValue: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
data() {
return {
operations: [
{ name: "parse", label: "解析URL", icon: "link_off" },
{ name: "format", label: "格式化URL", icon: "link" },
{ name: "parseQuery", label: "解析查询字符串", icon: "search" },
{ name: "formatQuery", label: "格式化查询字符串", icon: "edit" },
{ name: "parsePath", label: "解析路径", icon: "folder_open" },
{ name: "parseHost", label: "解析主机名", icon: "dns" },
{ name: "getQueryParam", label: "获取参数", icon: "find_in_page" },
{ name: "addQueryParam", label: "添加参数", icon: "add_circle" },
{ name: "removeQueryParam", label: "移除参数", icon: "remove_circle" },
{ name: "isAbsolute", label: "检查绝对URL", icon: "check_circle" },
{ name: "parseComponents", label: "解析组成部分", icon: "category" },
],
defaultArgvs: {
operation: "parse",
url: {
value: "",
isString: true,
__varInputVal__: true,
},
query: {
value: "",
isString: true,
__varInputVal__: true,
},
queryParams: {},
param: {
value: "",
isString: true,
__varInputVal__: true,
},
value: {
value: "",
isString: true,
__varInputVal__: true,
},
host: {
value: "",
isString: true,
__varInputVal__: true,
},
path: {
value: "",
isString: true,
__varInputVal__: true,
},
urlObject: {
protocol: {
value: "",
isString: true,
__varInputVal__: true,
},
auth: {
value: "",
isString: true,
__varInputVal__: true,
},
hostname: {
value: "",
isString: true,
__varInputVal__: true,
},
port: {
value: "",
isString: true,
__varInputVal__: true,
},
pathname: {
value: "",
isString: true,
__varInputVal__: true,
},
search: {
value: "",
isString: true,
__varInputVal__: true,
},
hash: {
value: "",
isString: true,
__varInputVal__: true,
},
},
},
};
},
computed: {
argvs: {
get() {
return (
this.modelValue.argvs ||
this.parseCodeToArgvs(this.modelValue.code) || {
...this.defaultArgvs,
}
);
},
set(value) {
this.updateModelValue(value);
},
},
needsUrlInput() {
return [
"parse",
"getQueryParam",
"addQueryParam",
"removeQueryParam",
"isAbsolute",
"parseComponents",
].includes(this.argvs.operation);
},
},
methods: {
generateCode(argvs = this.argvs) {
switch (argvs.operation) {
case "parse":
case "isAbsolute":
case "parseComponents":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.url
)})`;
case "format":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.urlObject
)})`;
case "parseQuery":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.query
)})`;
case "formatQuery":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.queryParams
)})`;
case "parsePath":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.path
)})`;
case "parseHost":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.host
)})`;
case "getQueryParam":
case "removeQueryParam":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.url
)}, ${stringifyObject(argvs.param)})`;
case "addQueryParam":
return `${this.modelValue.value}.${argvs.operation}(${stringifyObject(
argvs.url
)}, ${stringifyObject(argvs.param)}, ${stringifyObject(
argvs.value
)})`;
default:
return `${this.modelValue.value}.${argvs.operation}()`;
}
},
parseCodeToArgvs(code) {
if (!code) return null;
try {
const variableFormatPaths = ["arg*", "arg*.**"];
const result = parseFunction(code, { variableFormatPaths });
if (!result) return this.defaultArgvs;
const operation = result.name.split(".").pop();
const [firstArg, secondArg, thirdArg] = result.args;
const newArgvs = {
...this.defaultArgvs,
operation,
};
switch (operation) {
case "parse":
case "isAbsolute":
case "parseComponents":
newArgvs.url = firstArg;
break;
case "format":
newArgvs.urlObject = firstArg || this.defaultArgvs.urlObject;
break;
case "parseQuery":
newArgvs.query = firstArg;
break;
case "formatQuery":
if (firstArg) {
newArgvs.queryParams = firstArg;
}
break;
case "parsePath":
newArgvs.path = firstArg;
break;
case "parseHost":
newArgvs.host = firstArg;
break;
case "getQueryParam":
case "removeQueryParam":
newArgvs.url = firstArg;
newArgvs.param = secondArg;
break;
case "addQueryParam":
newArgvs.url = firstArg;
newArgvs.param = secondArg;
newArgvs.value = thirdArg;
break;
}
return newArgvs;
} catch (e) {
console.error("解析URL参数失败:", e);
return this.defaultArgvs;
}
},
updateArgvs(key, value) {
this.argvs = {
...this.argvs,
[key]: value,
};
},
updateUrlObject(key, value) {
this.updateArgvs("urlObject", {
...this.argvs.urlObject,
[key]: value,
});
},
getSummary(argvs) {
const op = this.operations.find(
(op) => op.name === argvs.operation
)?.label;
return op + " " + argvs.url.value;
},
updateModelValue(argvs) {
this.$emit("update:modelValue", {
...this.modelValue,
summary: this.getSummary(argvs),
argvs,
code: this.generateCode(argvs),
});
},
},
mounted() {
if (!this.modelValue.argvs && !this.modelValue.code) {
this.updateModelValue(this.defaultArgvs);
}
},
watch: {
"argvs.operation": {
immediate: true,
handler(newVal) {
this.$nextTick(() => {
document
.querySelector(`.operation-card[data-value="${newVal}"]`)
?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
});
});
},
},
},
});
</script>
<style scoped>
.url-editor {
display: flex;
flex-direction: column;
}
.options-container {
min-height: 32px;
display: flex;
flex-direction: column;
justify-content: center;
}
/* 覆盖command-composer的样式 */
.command-composer .operation-card {
min-width: 100px;
}
</style>

View File

@ -7,9 +7,6 @@
: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"
@ -19,7 +16,6 @@
<div class="text-caption">{{ op.label }}</div>
</div>
</div>
</div>
<!-- 操作配置 -->
<div class="operation-options q-mt-sm" v-if="hasOptions">
@ -200,17 +196,24 @@ export default defineComponent({
);
if (activeIndex === -1) return {};
//
const cardWidth = 80; //
const gap = 4; //
//
const container = document.querySelector(".operation-cards");
if (!container) return {};
const containerWidth = container.offsetWidth;
const cardCount = this.operations.length;
//
const cardWidth = 100; //
const pointerWidth = 12; //
//
// 1. (cardWidth + gap) * activeIndex
// 2. cardWidth / 2
// 3. pointerWidth / 2
//
const totalGapWidth = containerWidth - cardWidth * cardCount;
const gapWidth = totalGapWidth / (cardCount - 1);
//
const leftOffset =
(cardWidth + gap) * activeIndex + cardWidth / 2 - pointerWidth / 2;
(cardWidth + gapWidth) * activeIndex + cardWidth / 2 - pointerWidth / 2;
return {
left: `${leftOffset}px`,
@ -303,29 +306,6 @@ export default defineComponent({
flex-direction: column;
}
.operation-cards {
display: flex;
justify-content: flex-start;
gap: 4px;
}
.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);
}
.operation-options {
position: relative;
background: #f8f8f8;
@ -381,4 +361,15 @@ export default defineComponent({
color: white;
background: var(--q-primary);
}
/* 覆盖command-composer的样式 */
.command-composer .operation-cards {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.command-composer .operation-card {
width: 100px;
}
</style>

View File

@ -7,9 +7,7 @@
: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"
:data-value="op.name"
>
<q-icon
:name="op.icon"
@ -19,7 +17,6 @@
<div class="text-caption">{{ op.label }}</div>
</div>
</div>
</div>
<!-- 操作配置 -->
<div class="operation-options q-mt-sm">
@ -433,6 +430,22 @@ export default defineComponent({
this.updateModelValue(this.defaultArgvs);
}
},
watch: {
"argvs.operation": {
immediate: true,
handler(newVal) {
this.$nextTick(() => {
document
.querySelector(`.operation-card[data-value="${newVal}"]`)
?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
});
});
},
},
},
});
</script>
@ -442,30 +455,6 @@ export default defineComponent({
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;

View File

@ -0,0 +1,395 @@
<template>
<div class="array-editor">
<div v-for="(item, index) in items" :key="index" class="row items-center">
<!-- 如果传入options.keys则生成多键对象
示例
options: {
keys: ['name', 'age', 'email']
}
生成数据结构示例
[
{
name: { value: "张三", isString: true, __varInputVal__: true },
age: { value: "18", isString: false, __varInputVal__: true },
email: { value: "zhangsan@example.com", isString: true, __varInputVal__: true }
}
]
-->
<template v-if="options?.keys">
<div
v-for="key in options.keys"
:key="key"
:class="['col', options.keys.length > 1 ? 'q-pr-sm' : '']"
>
<VariableInput
:model-value="item[key]"
:label="key"
:icon="icon || 'code'"
@update:model-value="(val) => updateItemKeyValue(index, key, val)"
/>
</div>
</template>
<template v-else>
<div class="col">
<!-- 如果传入options.items则生成下拉选择
示例
options: {
items: ['选项1', '选项2', '选项3']
}
-->
<template v-if="options?.items">
<q-select
:model-value="item.value"
:options="filterOptions"
:label="`${label || '项目'} ${index + 1}`"
dense
filled
use-input
input-debounce="0"
:hide-selected="!!inputValue"
@filter="filterFn"
@update:model-value="(val) => handleSelect(val, index)"
@input-value="(val) => handleInput(val, index)"
@blur="handleBlur"
>
<template v-slot:prepend>
<q-icon :name="icon || 'code'" />
</template>
</q-select>
</template>
<!-- 不传options情况下生成单值对象
生成数据结构示例
[
"张三",
"李四",
"王五"
]
-->
<template v-else>
<VariableInput
:model-value="item"
:label="`${label || '项目'} ${index + 1}`"
:icon="icon || 'code'"
@update:model-value="(val) => updateItemValue(index, val)"
/>
</template>
</div>
</template>
<div class="col-auto">
<div class="btn-container">
<template v-if="items.length === 1">
<q-btn
flat
dense
size="sm"
icon="add"
class="center-btn"
@click="addItem"
/>
</template>
<template v-else-if="index === items.length - 1">
<q-btn
flat
dense
size="sm"
icon="remove"
class="top-btn"
@click="removeItem(index)"
/>
<q-btn
flat
dense
size="sm"
icon="add"
class="bottom-btn"
@click="addItem"
/>
</template>
<template v-else>
<q-btn
flat
dense
size="sm"
icon="remove"
class="center-btn"
@click="removeItem(index)"
/>
</template>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "ArrayEditor",
components: {
VariableInput,
},
props: {
modelValue: {
type: Array,
required: true,
},
label: {
type: String,
default: "",
},
icon: {
type: String,
default: "",
},
/**
* 配置选项支持两种模式
* 1. 选项模式通过 items 提供选项列表
* 数组的每个元素都可以从选项中选择值
*
* 2. 多键模式通过 keys 定义每个数组元素包含的键
* 数组的每个元素都是一个对象包含指定的键每个键对应一个输入框
*/
options: {
type: Object,
default: null,
},
},
emits: ["update:modelValue"],
data() {
return {
//
localItems: this.initializeItems(),
//
filterOptions: this.options?.items || [],
//
inputValue: "",
};
},
computed: {
items: {
get() {
return this.localItems;
},
set(newItems) {
this.localItems = newItems;
this.$emit("update:modelValue", newItems);
},
},
},
methods: {
/**
* 初始化数组项
* 1. 如果传入了初始值直接使用
* 2. 如果配置了 keys创建包含所有键的对象
* 3. 默认创建单值对象
*/
initializeItems() {
if (this.modelValue.length) {
return this.modelValue;
}
if (this.options?.keys) {
const item = {};
this.options.keys.forEach((key) => {
item[key] = {
value: "",
isString: false,
__varInputVal__: true,
};
});
return [item];
}
return [
{
value: "",
isString: false,
__varInputVal__: true,
},
];
},
/**
* 添加新的数组项
* 根据配置创建相应的数据结构
*/
addItem() {
if (this.options?.keys) {
const newItem = {};
this.options.keys.forEach((key) => {
newItem[key] = {
value: "",
isString: false,
__varInputVal__: true,
};
});
this.items = [...this.items, newItem];
} else {
this.items = [
...this.items,
{
value: "",
isString: false,
__varInputVal__: true,
},
];
}
},
/**
* 移除指定索引的数组项
* 如果移除后数组为空则创建一个新的空项
*/
removeItem(index) {
const newItems = [...this.items];
newItems.splice(index, 1);
if (newItems.length === 0) {
if (this.options?.keys) {
const newItem = {};
this.options.keys.forEach((key) => {
newItem[key] = {
value: "",
isString: false,
__varInputVal__: true,
};
});
newItems.push(newItem);
} else {
newItems.push({
value: "",
isString: false,
__varInputVal__: true,
});
}
}
this.items = newItems;
},
/**
* 更新单值模式下的值
*/
updateItemValue(index, value) {
const newItems = [...this.items];
newItems[index] = value;
this.items = newItems;
},
/**
* 更新多键模式下指定键的值
*/
updateItemKeyValue(index, key, value) {
const newItems = [...this.items];
newItems[index] = {
...newItems[index],
[key]: value,
};
this.items = newItems;
},
/**
* 选项模式下的输入处理
* 当输入的值不在选项中时创建新值
*/
handleInput(val, index) {
this.inputValue = val;
if (val && !this.filterOptions.includes(val)) {
const newItems = [...this.items];
newItems[index] = {
value: val,
isString: false,
__varInputVal__: true,
};
this.items = newItems;
}
},
/**
* 选项模式下的选择处理
*/
handleSelect(val, index) {
this.inputValue = "";
const newItems = [...this.items];
newItems[index] = {
value: val,
isString: false,
__varInputVal__: true,
};
this.items = newItems;
},
/**
* 选项模式下的失焦处理
*/
handleBlur() {
this.inputValue = "";
},
/**
* 选项模式下的过滤处理
* 根据输入值过滤可选项
*/
filterFn(val, update) {
if (!this.options?.items) return;
update(() => {
if (val === "") {
this.filterOptions = this.options.items;
} else {
const needle = val.toLowerCase();
this.filterOptions = this.options.items.filter(
(v) => v.toLowerCase().indexOf(needle) > -1
);
}
});
},
},
});
</script>
<style scoped>
.array-editor {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
/* 防止输入框换行 */
:deep(.q-field__native) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn-container {
position: relative;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-container .q-btn {
position: absolute;
width: 16px;
height: 16px;
min-height: 16px;
padding: 0;
}
.btn-container .center-btn {
position: relative;
}
.btn-container .top-btn {
top: 0;
}
.btn-container .bottom-btn {
bottom: 0;
}
:deep(.q-btn .q-icon) {
font-size: 14px;
}
:deep(.q-btn.q-btn--dense) {
padding: 0;
min-height: 16px;
}
</style>

View File

@ -6,6 +6,12 @@
class="row q-col-gutter-sm items-center"
>
<div class="col-4">
<!-- 如果传入options.items则键值支持下拉选择
示例
options: {
items: ['User-Agent', 'Content-Type', 'Accept']
}
-->
<q-select
v-if="options?.items"
:model-value="item.key"
@ -25,6 +31,7 @@
<q-icon name="code" />
</template>
</q-select>
<!-- 不传options.items时键值为非VariableInput的输入框 -->
<q-input
v-else
:model-value="item.key"
@ -39,9 +46,10 @@
</q-input>
</div>
<div class="col">
<!-- 值使用VariableInput组件 -->
<VariableInput
:model-value="item.value"
:label="item.key || ''"
label="值"
icon="code"
class="col-grow"
@update:model-value="(val) => updateItemValue(val, index)"
@ -107,6 +115,11 @@ export default defineComponent({
type: Object,
required: true,
},
/**
* 配置选项支持
* 选项模式通过 items 提供选项列表
* 字典的每个键都可以从选项中选择值
*/
options: {
type: Object,
default: null,

View File

@ -5,20 +5,24 @@
class="flex-item"
:style="{ flex: localCommand.functionSelector.width || 3 }"
>
<q-select
v-model="functionName"
:options="localCommand.functionSelector.options"
:label="localCommand.functionSelector.selectLabel"
dense
filled
emit-value
map-options
<div class="operation-cards">
<div
v-for="option in localCommand.functionSelector?.options"
:key="option.value"
:class="['operation-card', { active: functionName === option.value }]"
:data-value="option.value"
@click="functionName = option.value"
>
<template v-slot:prepend>
<q-icon :name="localCommand.icon || 'functions'" />
</template>
</q-select>
<q-icon
:name="option.icon || localCommand.icon || 'functions'"
size="16px"
:color="functionName === option.value ? 'primary' : 'grey'"
/>
<div class="text-caption">{{ option.label }}</div>
</div>
</div>
</div>
<div class="flex-container">
<div
v-for="(item, index) in localConfig"
:key="index"
@ -43,6 +47,7 @@
</div>
</div>
</div>
</div>
</template>
<script>
@ -170,7 +175,11 @@ export default defineComponent({
},
getSummary(argvs) {
// header
return argvs
const funcNameLabel = this.localCommand.functionSelector?.options.find(
(option) => option.value === this.functionName
)?.label;
const subFeature = funcNameLabel ? `${funcNameLabel} ` : "";
const allArgvs = argvs
.map((item) =>
item?.hasOwnProperty("__varInputVal__")
? window.lodashM.truncate(item.value, {
@ -179,8 +188,8 @@ export default defineComponent({
})
: item
)
.filter((item) => item != null)
.join("、");
.filter((item) => item != null && item != "");
return `${subFeature}${allArgvs.join(",")}`;
},
updateModelValue(functionName, argvs) {
this.$emit("update:modelValue", {
@ -201,6 +210,23 @@ export default defineComponent({
this.updateModelValue(this.functionName, this.defaultArgvs);
}
},
watch: {
functionName: {
immediate: true,
handler(newVal) {
//
this.$nextTick(() => {
document
.querySelector(`.operation-card[data-value="${newVal}"]`)
?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
});
});
},
},
},
});
</script>
@ -213,12 +239,56 @@ export default defineComponent({
}
.flex-item {
min-width: 100px; /* 设置最小宽度以确保内容可读性 */
min-width: 100px;
}
@media (max-width: 600px) {
.flex-item {
flex: 1 1 100% !important; /* 在小屏幕上强制换行 */
flex: 1 1 100% !important;
}
}
.operation-cards {
display: flex;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding: 1px;
gap: 8px;
border-radius: 8px;
}
.operation-cards::-webkit-scrollbar {
display: none;
}
.operation-card {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
border-radius: 6px;
min-width: 72px;
padding: 2px 0;
background: rgba(0, 0, 0, 0.05);
}
.body--dark .operation-card {
background: rgba(0, 0, 0, 0.05);
}
.operation-card:hover {
background: var(--q-primary-opacity-5);
transform: translateY(-1px);
border: 1px solid var(--q-primary-opacity-10);
}
.operation-card.active {
border-color: var(--q-primary);
background: var(--q-primary-opacity-5);
}
.body--dark .operation-card.active {
border-color: var(--q-primary-opacity-50);
}
</style>

146
src/css/composer.css Normal file
View File

@ -0,0 +1,146 @@
/* 操作卡片样式 */
.command-composer .operation-cards {
display: flex;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding: 1px;
gap: 8px;
border-radius: 8px;
}
.command-composer .operation-cards::-webkit-scrollbar {
display: none;
}
.command-composer .operation-card {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
border-radius: 6px;
min-width: 72px;
max-height: 36px;
background: rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.command-composer .operation-card:hover {
background: var(--q-primary-opacity-5);
transform: translateY(-1px);
border: 1px solid var(--q-primary-opacity-10);
}
.command-composer .operation-card.active {
border-color: var(--q-primary);
background: var(--q-primary-opacity-5);
}
/* 暗色模式适配 */
.body--dark .command-composer .operation-card {
background: rgba(255, 255, 255, 0.03);
}
.body--dark .command-composer .operation-card.active {
border-color: var(--q-primary-opacity-50);
}
/* 滚动美化 */
.command-composer .q-scrollarea__thumb {
width: 2px;
opacity: 0.4;
transition: opacity 0.3s ease;
}
.command-composer .q-scrollarea__thumb:hover {
opacity: 0.8;
}
/* 布局更加紧凑 */
/* 输入框高度及字体 */
.command-composer .q-field--filled:not(.q-textarea) .q-field__control,
.command-composer .q-field--filled:not(.q-textarea) .q-field__control>*,
.command-composer .q-field--filled:not(.q-field--labeled):not(.q-textarea) .q-field__native {
max-height: 36px !important;
min-height: 36px !important;
}
.command-composer .q-field--filled .q-field__control,
.command-composer .q-field--filled .q-field__control>*,
.command-composer .q-field--filled .q-field__native {
border-radius: 5px;
font-size: 12px;
}
/* 输入框图标大小 */
.command-composer .q-field--filled .q-field__control .q-icon {
font-size: 18px;
}
/* 输入框标签字体大小,占位时的位置 */
.command-composer .q-field--filled .q-field__label {
font-size: 11px;
top: 11px;
}
/* 输入框标签悬浮的位置 */
.command-composer .q-field--filled .q-field--float .q_field__label {
transform: translateY(-35%) scale(0.7);
}
/* 去除filled输入框边框 */
.command-composer .q-field--filled .q-field__control:before {
border: none;
}
/* 去除filled输入框下划线 */
.command-composer .q-field--filled .q-field__control:after {
height: 0;
border-bottom: none;
}
/* 输入框背景颜色及内边距 */
.command-composer .q-field--filled .q-field__control {
background: rgba(0, 0, 0, 0.03);
padding: 0 8px;
}
/* 输入框聚焦时的背景颜色 */
.command-composer .q-field--filled.q-field--highlighted .q-field__control {
background: rgba(0, 0, 0, 0.03);
}
/* 暗黑模式下的输入框背景颜色 */
.body--dark .command-composer .q-field--filled .q-field__control {
background: rgba(255, 255, 255, 0.04);
}
/* 暗黑模式下输入框聚焦时的背景颜色 */
.body--dark .command-composer .q-field--filled.q-field--highlighted .q-field__control {
background: rgba(255, 255, 255, 0.08);
}
/* checkbox/toggle大小及字体 */
.command-composer .q-checkbox__label,
.command-composer .q-toggle__label {
font-size: 12px;
}
.command-composer .q-checkbox__inner,
.command-composer .q-toggle__inner {
font-size: 28px;
margin: 4px 0px;
}
/* 暗黑模式下的标签栏背景颜色 */
.body--dark .command-composer .q-tab,
.body--dark .command-composer .q-tab-panel {
background-color: #303133;
}
.body--dark .command-composer .q-tab--inactive {
opacity: 2;
}

View File

@ -65,5 +65,17 @@ export const OsEditor = defineAsyncComponent(() =>
);
export const PathEditor = defineAsyncComponent(() =>
import("src/components/composer/system/PathEditor.vue")
import("components/composer/system/PathEditor.vue")
);
export const ZlibEditor = defineAsyncComponent(() =>
import("components/composer/file/ZlibEditor.vue")
);
export const UrlEditor = defineAsyncComponent(() =>
import("components/composer/network/UrlEditor.vue")
);
export const DnsEditor = defineAsyncComponent(() =>
import("components/composer/network/DnsEditor.vue")
);
export const BufferEditor = defineAsyncComponent(() =>
import("components/composer/developer/BufferEditor.vue")
);

View File

@ -0,0 +1,14 @@
export const developerCommands = {
label: "开发相关",
icon: "code",
defaultOpened: true,
commands: [
{
value: "quickcomposer.developer.buffer",
label: "Buffer操作",
desc: "Buffer创建、转换和操作",
component: "BufferEditor",
icon: "memory",
},
],
};

View File

@ -46,5 +46,13 @@ export const fileCommands = {
},
],
},
{
value: "quickcomposer.file.zlib",
label: "压缩解压",
desc: "使用 zlib 进行数据压缩和解压",
component: "ZlibEditor",
icon: "compress",
isAsync: true,
},
],
};

View File

@ -6,6 +6,7 @@ import { textCommands } from "./textCommands";
import { otherCommands } from "./otherCommands";
import { simulateCommands } from "./simulateCommands";
import { controlCommands } from "./controlCommands";
import { developerCommands } from "./developerCommands";
export const commandCategories = [
fileCommands,
@ -16,4 +17,5 @@ export const commandCategories = [
controlCommands,
otherCommands,
simulateCommands,
developerCommands,
];

View File

@ -44,5 +44,20 @@ export const networkCommands = {
isAsync: true,
icon: "http",
},
{
value: "quickcomposer.network.url",
label: "URL操作",
desc: "URL解析、格式化和参数处理",
component: "UrlEditor",
icon: "link",
},
{
value: "quickcomposer.network.dns",
label: "DNS操作",
desc: "DNS解析和查询",
component: "DnsEditor",
icon: "dns",
isAsync: true,
},
],
};

View File

@ -22,28 +22,42 @@ export const textCommands = {
{
label: "Base64编码",
value: "quickcomposer.text.base64Encode",
icon: "title",
},
{
label: "Base64解码",
value: "quickcomposer.text.base64Decode",
icon: "title",
},
{
label: "十六进制编码",
value: "quickcomposer.text.hexEncode",
icon: "code",
},
{
label: "十六进制解码",
value: "quickcomposer.text.hexDecode",
icon: "code",
},
{
label: "URL编码",
value: "quickcomposer.text.urlEncode",
icon: "link",
},
{
label: "URL解码",
value: "quickcomposer.text.urlDecode",
icon: "link",
},
{ label: "URL编码", value: "quickcomposer.text.urlEncode" },
{ label: "URL解码", value: "quickcomposer.text.urlDecode" },
{
label: "HTML编码",
value: "quickcomposer.text.htmlEncode",
icon: "html",
},
{
label: "HTML解码",
value: "quickcomposer.text.htmlDecode",
icon: "html",
},
],
width: 3,
@ -75,11 +89,31 @@ export const textCommands = {
functionSelector: {
selectLabel: "哈希算法",
options: [
{ 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" },
{
label: "MD5",
value: "quickcomposer.text.md5Hash",
icon: "functions",
},
{
label: "SHA1",
value: "quickcomposer.text.sha1Hash",
icon: "functions",
},
{
label: "SHA256",
value: "quickcomposer.text.sha256Hash",
icon: "functions",
},
{
label: "SHA512",
value: "quickcomposer.text.sha512Hash",
icon: "functions",
},
{
label: "SM3",
value: "quickcomposer.text.sm3Hash",
icon: "functions",
},
],
},
width: 3,

View File

@ -150,12 +150,16 @@ const isPathMatched = (path, patterns) => {
const regexPattern = pattern
// 先处理 **,将其转换为特殊标记
.replace(/\*\*/g, "###DOUBLEWILDCARD###")
// 处理数组索引通配符 [*]
.replace(/\[\*\]/g, "###ARRAYINDEX###")
// 处理普通的 *
.replace(/\*/g, "[^/.]+")
.replace(/\*/g, "[^/.\\[\\]]+")
// 转义特殊字符
.replace(/[.]/g, "\\$&")
.replace(/[.[\]]/g, "\\$&")
// 还原 ** 为正则表达式
.replace(/###DOUBLEWILDCARD###/g, ".*");
.replace(/###DOUBLEWILDCARD###/g, ".*")
// 还原数组索引通配符
.replace(/###ARRAYINDEX###/g, "\\[\\d+\\]");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
@ -188,25 +192,28 @@ const isPathMatched = (path, patterns) => {
* - arg1.data.* - 匹配data下的所有直接子属性
* - arg2.params.** - 匹配params下的所有属性包括嵌套
*
* 3. 通配符
* - * - 匹配单个层级的任意字符不包含点号
* - ** - 匹配任意层级包含点号
* 3. 数组索引
* - arg0[0] - 匹配数组的第一个元素
* - arg0[*] - 匹配数组的任意元素
* - arg0[*].name - 匹配数组任意元素的name属性
* - arg0[*].** - 匹配数组任意元素的所有属性包括嵌套
*
* 4. 排除规则
* 4. 通配符
* - * - 匹配单个层级的任意字符不包含点号和方括号
* - ** - 匹配任意层级包含点号
* - [*] - 匹配任意数组索引
*
* 5. 排除规则
* - !pattern - 排除匹配的路径
* - 排除优先级高于包含
*
* 5. 示例
* 6. 示例
* - arg0 - 匹配第一个参数
* - arg*.headers.** - 匹配任意参数中headers下的所有属性
* - arg*.data.* - 匹配任意参数中data下的直接子属性
* - arg0[*] - 匹配第一个参数的所有数组元素
* - arg0[*].name - 匹配第一个参数数组中所有元素的name属性
* - !arg*.headers.Content-Type - 排除所有参数中的Content-Type头
* - arg*.headers.Accept* - 匹配所有以Accept开头的头部
*
* 6. 使用建议
* - 优先使用精确匹配arg0, arg1.data
* - 使用通配符时注意层级* vs **
* - 合理使用排除规则避免过度匹配
*
* @returns {Object} 解析结果包含函数名和参数数组
*/