diff --git a/plugin/lib/quickcomposer.js b/plugin/lib/quickcomposer.js index 5b89605..2a0b246 100644 --- a/plugin/lib/quickcomposer.js +++ b/plugin/lib/quickcomposer.js @@ -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; diff --git a/plugin/lib/quickcomposer/developer/buffer.js b/plugin/lib/quickcomposer/developer/buffer.js new file mode 100644 index 0000000..5632ebf --- /dev/null +++ b/plugin/lib/quickcomposer/developer/buffer.js @@ -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", + ], +}; diff --git a/plugin/lib/quickcomposer/developer/index.js b/plugin/lib/quickcomposer/developer/index.js new file mode 100644 index 0000000..f0e6cde --- /dev/null +++ b/plugin/lib/quickcomposer/developer/index.js @@ -0,0 +1,3 @@ +module.exports = { + buffer: require("./buffer"), +}; diff --git a/plugin/lib/quickcomposer/file/index.js b/plugin/lib/quickcomposer/file/index.js index 34e535d..b78c2c5 100644 --- a/plugin/lib/quickcomposer/file/index.js +++ b/plugin/lib/quickcomposer/file/index.js @@ -1,5 +1,7 @@ const operation = require("./operation"); +const zlib = require("./zlib"); module.exports = { operation: operation.operation, + zlib: zlib, }; diff --git a/plugin/lib/quickcomposer/file/zlib.js b/plugin/lib/quickcomposer/file/zlib.js new file mode 100644 index 0000000..03b28ed --- /dev/null +++ b/plugin/lib/quickcomposer/file/zlib.js @@ -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, +}; diff --git a/plugin/lib/quickcomposer/network/dns.js b/plugin/lib/quickcomposer/network/dns.js new file mode 100644 index 0000000..8e00ad2 --- /dev/null +++ b/plugin/lib/quickcomposer/network/dns.js @@ -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, +}; diff --git a/plugin/lib/quickcomposer/network/index.js b/plugin/lib/quickcomposer/network/index.js new file mode 100644 index 0000000..991a25c --- /dev/null +++ b/plugin/lib/quickcomposer/network/index.js @@ -0,0 +1,4 @@ +module.exports = { + url: require("./url"), + dns: require("./dns"), +}; diff --git a/plugin/lib/quickcomposer/network/url.js b/plugin/lib/quickcomposer/network/url.js new file mode 100644 index 0000000..f917480 --- /dev/null +++ b/plugin/lib/quickcomposer/network/url.js @@ -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, +}; diff --git a/plugin/lib/quickcomposer/zlib/index.js b/plugin/lib/quickcomposer/zlib/index.js new file mode 100644 index 0000000..de00123 --- /dev/null +++ b/plugin/lib/quickcomposer/zlib/index.js @@ -0,0 +1 @@ +module.exports = require("../file/zlib"); diff --git a/plugin/lib/quickcomposer/zlib/zlib.js b/plugin/lib/quickcomposer/zlib/zlib.js new file mode 100644 index 0000000..6e08fe2 --- /dev/null +++ b/plugin/lib/quickcomposer/zlib/zlib.js @@ -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, +}; diff --git a/quasar.config.js b/quasar.config.js index 935feb4..bd1efa6 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -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 = { diff --git a/src/components/composer/CommandComposer.vue b/src/components/composer/CommandComposer.vue index b73e041..5019b27 100644 --- a/src/components/composer/CommandComposer.vue +++ b/src/components/composer/CommandComposer.vue @@ -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; -} diff --git a/src/components/composer/developer/BufferEditor.vue b/src/components/composer/developer/BufferEditor.vue new file mode 100644 index 0000000..3819818 --- /dev/null +++ b/src/components/composer/developer/BufferEditor.vue @@ -0,0 +1,721 @@ + + + + + diff --git a/src/components/composer/file/FileOperationEditor.vue b/src/components/composer/file/FileOperationEditor.vue index 45b45e8..667ca37 100644 --- a/src/components/composer/file/FileOperationEditor.vue +++ b/src/components/composer/file/FileOperationEditor.vue @@ -78,7 +78,7 @@ + + + + diff --git a/src/components/composer/network/DnsEditor.vue b/src/components/composer/network/DnsEditor.vue new file mode 100644 index 0000000..85b510d --- /dev/null +++ b/src/components/composer/network/DnsEditor.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/src/components/composer/network/UrlEditor.vue b/src/components/composer/network/UrlEditor.vue new file mode 100644 index 0000000..d731c5e --- /dev/null +++ b/src/components/composer/network/UrlEditor.vue @@ -0,0 +1,487 @@ + + + + + diff --git a/src/components/composer/system/OsEditor.vue b/src/components/composer/system/OsEditor.vue index aeb17b2..e6efb56 100644 --- a/src/components/composer/system/OsEditor.vue +++ b/src/components/composer/system/OsEditor.vue @@ -8,16 +8,12 @@ :class="['operation-card', { active: argvs.operation === op.name }]" @click="updateArgvs('operation', op.name)" > -
- -
{{ op.label }}
-
+ +
{{ op.label }}
@@ -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; +} diff --git a/src/components/composer/system/PathEditor.vue b/src/components/composer/system/PathEditor.vue index 881812c..5dc01c4 100644 --- a/src/components/composer/system/PathEditor.vue +++ b/src/components/composer/system/PathEditor.vue @@ -7,17 +7,14 @@ :key="op.name" :class="['operation-card', { active: argvs.operation === op.name }]" @click="updateArgvs('operation', op.name)" + :data-value="op.name" > -
- -
{{ op.label }}
-
+ +
{{ op.label }}
@@ -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", + }); + }); + }, + }, + }, }); @@ -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; diff --git a/src/components/composer/ui/ArrayEditor.vue b/src/components/composer/ui/ArrayEditor.vue new file mode 100644 index 0000000..fd12d91 --- /dev/null +++ b/src/components/composer/ui/ArrayEditor.vue @@ -0,0 +1,395 @@ + + + + + diff --git a/src/components/composer/ui/DictEditor.vue b/src/components/composer/ui/DictEditor.vue index 8f3f934..f2d98c5 100644 --- a/src/components/composer/ui/DictEditor.vue +++ b/src/components/composer/ui/DictEditor.vue @@ -6,6 +6,12 @@ class="row q-col-gutter-sm items-center" >
+ +
+ - - - -
-
-
- +
+
+ +
{{ option.label }}
+
-
- +
+
+
+
+ +
+
+ +
@@ -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", + }); + }); + }, + }, + }, }); @@ -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); +} diff --git a/src/css/composer.css b/src/css/composer.css new file mode 100644 index 0000000..61f1b9c --- /dev/null +++ b/src/css/composer.css @@ -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; +} diff --git a/src/js/composer/cardComponents.js b/src/js/composer/cardComponents.js index bf5dd93..3ccf045 100644 --- a/src/js/composer/cardComponents.js +++ b/src/js/composer/cardComponents.js @@ -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") ); diff --git a/src/js/composer/commands/developerCommands.js b/src/js/composer/commands/developerCommands.js new file mode 100644 index 0000000..57161d6 --- /dev/null +++ b/src/js/composer/commands/developerCommands.js @@ -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", + }, + ], +}; diff --git a/src/js/composer/commands/fileCommands.js b/src/js/composer/commands/fileCommands.js index d015636..42ee0e1 100644 --- a/src/js/composer/commands/fileCommands.js +++ b/src/js/composer/commands/fileCommands.js @@ -46,5 +46,13 @@ export const fileCommands = { }, ], }, + { + value: "quickcomposer.file.zlib", + label: "压缩解压", + desc: "使用 zlib 进行数据压缩和解压", + component: "ZlibEditor", + icon: "compress", + isAsync: true, + }, ], }; diff --git a/src/js/composer/commands/index.js b/src/js/composer/commands/index.js index de35771..014e01e 100644 --- a/src/js/composer/commands/index.js +++ b/src/js/composer/commands/index.js @@ -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, ]; diff --git a/src/js/composer/commands/networkCommands.js b/src/js/composer/commands/networkCommands.js index 0712231..52b4884 100644 --- a/src/js/composer/commands/networkCommands.js +++ b/src/js/composer/commands/networkCommands.js @@ -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, + }, ], }; diff --git a/src/js/composer/commands/textCommands.js b/src/js/composer/commands/textCommands.js index ec04872..e803102 100644 --- a/src/js/composer/commands/textCommands.js +++ b/src/js/composer/commands/textCommands.js @@ -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, diff --git a/src/js/composer/formatString.js b/src/js/composer/formatString.js index 412f547..3b2f2f3 100644 --- a/src/js/composer/formatString.js +++ b/src/js/composer/formatString.js @@ -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} 解析结果,包含函数名和参数数组 */