完成axios可视化

This commit is contained in:
fofolee 2024-12-30 01:00:53 +08:00
parent 7c1ca78ec0
commit d135f5b7a8
14 changed files with 706 additions and 661 deletions

View File

@ -93,7 +93,10 @@ module.exports = configure(function (ctx) {
],
},
]);
chain.resolve.alias.set("plugins", path.join(__dirname, "./src/plugins"));
chain.resolve.alias.set(
"plugins",
path.join(__dirname, "./src/plugins")
);
chain.resolve.alias.set("js", path.join(__dirname, "./src/js"));
},
extendWebpack(cfg) {
@ -194,7 +197,6 @@ module.exports = configure(function (ctx) {
orientation: "portrait",
background_color: "#ffffff",
theme_color: "#027be3",
},
},

View File

View File

@ -0,0 +1,60 @@
<template>
<div class="border-label" :data-label="label">
<slot></slot>
</div>
</template>
<script>
export default {
name: "BorderLabel",
props: {
label: {
type: String,
default: "",
},
},
};
</script>
<style scoped>
.border-label {
width: 100%;
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
border-radius: 6px;
padding: 8px;
position: relative;
margin-top: 8px;
}
.border-label::before {
content: attr(data-label);
position: absolute;
top: -9px;
left: 16px;
background: #fff;
color: rgba(0, 0, 0, 0.6);
font-size: 12px;
line-height: 16px;
padding: 0 8px;
z-index: 1;
}
.border-label::after {
content: "";
position: absolute;
top: -1px;
left: 0;
right: 0;
height: 1px;
background: inherit;
}
.body--dark .border-label {
--border-color: rgba(255, 255, 255, 0.1);
}
.body--dark .border-label::before {
background: #303133;
color: rgba(255, 255, 255, 0.7);
}
</style>

View File

@ -202,7 +202,7 @@ export default defineComponent({
.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-select--with-input .q-field__native) {
:deep(.q-field--filled:not(.q-field--labeled) .q-field__native) {
border-radius: 5px;
font-size: 12px;
max-height: 36px !important;
@ -236,9 +236,10 @@ export default defineComponent({
border-bottom: none;
}
/* 输入框背景颜色 */
/* 输入框背景颜色及内边距 */
.command-composer :deep(.q-field--filled .q-field__control) {
background: rgba(0, 0, 0, 0.03);
padding: 0 8px;
}
/* 输入框聚焦时的背景颜色 */

View File

@ -88,12 +88,8 @@
<template v-else-if="command.hasAxiosEditor">
<AxiosConfigEditor v-model="argvLocal" class="col" />
</template>
<!-- Fetch编辑器 -->
<template v-else-if="command.hasFetchEditor">
<FetchConfigEditor v-model="argvLocal" class="col" />
</template>
<!-- 普通参数输入 -->
<template v-else>
<template v-else-if="!command.hasNoArgs">
<VariableInput
v-model="argvLocal"
:label="placeholder"
@ -115,7 +111,6 @@ import KeyEditor from "./KeyEditor.vue";
import UBrowserEditor from "./ubrowser/UBrowserEditor.vue";
import VariableInput from "./VariableInput.vue";
import AxiosConfigEditor from "./http/AxiosConfigEditor.vue";
import FetchConfigEditor from "./http/FetchConfigEditor.vue";
import { validateVariableName } from "js/common/variableValidator";
export default defineComponent({
@ -125,7 +120,6 @@ export default defineComponent({
UBrowserEditor,
VariableInput,
AxiosConfigEditor,
FetchConfigEditor,
},
props: {
command: {
@ -162,7 +156,7 @@ export default defineComponent({
},
argvLocal: {
get() {
if (this.command.hasAxiosEditor || this.command.hasFetchEditor) {
if (this.command.hasAxiosEditor) {
//
if (
this.command.argv &&
@ -183,8 +177,7 @@ export default defineComponent({
set(value) {
const updatedCommand = {
...this.command,
argv:
this.command.hasAxiosEditor || this.command.hasFetchEditor
argv: this.command.hasAxiosEditor
? typeof value === "string"
? value
: JSON.stringify(value)

View File

@ -0,0 +1,261 @@
<template>
<div class="dict-editor">
<div
v-for="(item, index) in items"
:key="index"
class="row q-col-gutter-sm items-center"
>
<div class="col-4">
<q-select
v-if="options?.items"
:model-value="item.key"
:options="options.items"
label="名称"
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="code" />
</template>
</q-select>
<q-input
v-else
:model-value="item.key"
label="名称"
dense
filled
@update:model-value="(val) => updateItemKey(val, index)"
>
<template v-slot:prepend>
<q-icon name="code" />
</template>
</q-input>
</div>
<div class="col">
<VariableInput
:model-value="item.value"
:label="item.key || '值'"
:command="{ icon: 'code' }"
@update:model-value="(val) => updateItemValue(val, index)"
/>
</div>
<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 "./VariableInput.vue";
export default defineComponent({
name: "DictEditor",
components: {
VariableInput,
},
props: {
modelValue: {
type: Object,
default: () => ({}),
},
options: {
type: Object,
default: null,
},
},
emits: ["update:modelValue"],
data() {
const modelEntries = Object.entries(this.modelValue || {});
return {
localItems: modelEntries.length
? modelEntries.map(([key, value]) => ({
key,
value: typeof value === "string" ? value : JSON.stringify(value),
}))
: [{ key: "", value: "" }],
filterOptions: this.options?.items || [],
inputValue: "",
};
},
computed: {
items: {
get() {
return this.localItems;
},
set(newItems) {
this.localItems = newItems;
const dict = {};
newItems.forEach((item) => {
if (item.key && item.value) {
dict[item.key] = item.value;
}
});
this.$emit("update:modelValue", dict);
},
},
},
methods: {
addItem() {
this.items = [...this.items, { key: "", value: "" }];
},
removeItem(index) {
const newItems = [...this.items];
newItems.splice(index, 1);
if (newItems.length === 0) {
newItems.push({ key: "", value: "" });
}
this.items = newItems;
const dict = {};
newItems.forEach((item) => {
if (item.key && item.value) {
dict[item.key] = item.value;
}
});
this.$emit("update:modelValue", dict);
},
updateItemKey(val, index) {
const newItems = [...this.items];
newItems[index].key = val;
this.items = newItems;
},
updateItemValue(val, index) {
const newItems = [...this.items];
newItems[index].value = val;
this.items = newItems;
},
handleInput(val, index) {
this.inputValue = val;
if (val && !this.filterOptions.includes(val)) {
const newItems = [...this.items];
newItems[index].key = val;
this.items = newItems;
}
},
handleSelect(val, index) {
this.inputValue = "";
const newItems = [...this.items];
newItems[index].key = val;
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>
.dict-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

@ -101,7 +101,6 @@
<q-btn
flat
dense
round
icon="keyboard_arrow_up"
size="xs"
class="number-btn"
@ -110,7 +109,6 @@
<q-btn
flat
dense
round
icon="keyboard_arrow_down"
size="xs"
class="number-btn"
@ -262,7 +260,7 @@ export default defineComponent({
.string-toggle {
min-width: 24px;
padding: 4px;
opacity: 0.8;
opacity: 0.6;
transition: all 0.3s ease;
}
@ -348,6 +346,7 @@ export default defineComponent({
.number-controls {
height: 100%;
display: flex;
width: 32px;
flex-direction: column;
justify-content: center;
}

View File

@ -37,80 +37,18 @@
</div>
</div>
</div>
<!-- Headers -->
<div class="col-12">
<HeaderEditor
:headers="localConfig.headers"
@input="
(val) => {
localConfig.headers = val;
updateConfig();
}
"
/>
</div>
<!-- 请求参数 -->
<div class="col-12">
<VariableInput
v-model="localConfig.params"
label="URL参数"
:command="{ icon: 'link' }"
@update:model-value="updateConfig"
/>
</div>
<!-- 请求体 -->
<div class="col-12">
<VariableInput
v-model="localConfig.data"
label="请求体"
:command="{ icon: 'data_object' }"
@update:model-value="updateConfig"
/>
</div>
<!-- 超时设置 -->
<div class="col-12">
<VariableInput
v-model="localConfig.timeout"
label="超时时间(ms)"
:command="{ icon: 'timer', inputType: 'number' }"
@update:model-value="updateConfig"
/>
</div>
<!-- 其他选项 -->
<!-- 响应设置 -->
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-checkbox
v-model="localConfig.withCredentials"
label="发送凭证"
@update:model-value="updateConfig"
/>
</div>
<div class="col-6">
<q-checkbox
v-model="localConfig.decompress"
label="自动解压"
@update:model-value="updateConfig"
/>
</div>
</div>
</div>
<!-- 响应类型 -->
<div class="col-12">
<div class="col-3">
<q-select
v-model="localConfig.responseType"
:options="['json', 'text', 'blob', 'arraybuffer']"
label="响应类型"
dense
filled
dense
emit-value
map-options
:options="['json', 'text', 'blob', 'arraybuffer']"
label="响应类型"
@update:model-value="updateConfig"
>
<template v-slot:prepend>
@ -118,20 +56,184 @@
</template>
</q-select>
</div>
<div class="col">
<VariableInput
v-model="localConfig.maxRedirects"
label="最大重定向次数"
:command="{ icon: 'repeat', inputType: 'number' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col">
<VariableInput
v-model="localConfig.timeout"
label="超时时间(ms)"
:command="{ icon: 'timer', inputType: 'number' }"
@update:model-value="updateConfig"
/>
</div>
</div>
</div>
<!-- Headers -->
<!-- Content-Type -->
<q-select
v-model="localConfig.headers['Content-Type']"
label="Content-Type"
dense
filled
emit-value
map-options
:options="contentTypes"
class="col-12"
@update:model-value="updateConfig"
>
<template v-slot:prepend>
<q-icon name="data_object" />
</template>
</q-select>
<!-- User-Agent -->
<div class="col-12 row q-col-gutter-sm">
<div class="col">
<VariableInput
v-model="localConfig.headers['User-Agent']"
label="User Agent"
:command="{ icon: 'devices' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col-auto flex items-center">
<q-btn-dropdown flat dense dropdown-icon="menu">
<q-list>
<q-item
v-for="ua in userAgentOptions"
:key="ua.value"
clickable
v-close-popup
@click="setUserAgent(ua.value)"
>
<q-item-section>
<q-item-label>{{ ua.label }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</div>
<!-- Other Headers -->
<div class="col-12" style="margin-top: -8px">
<BorderLabel label="Headers">
<DictEditor
v-model="otherHeaders"
:options="{
items: commonHeaderOptions,
}"
@update:model-value="updateHeaders"
/>
</BorderLabel>
</div>
<!-- 请求体 -->
<div v-if="hasRequestData" class="col-12" style="margin-top: -8px">
<BorderLabel label="请求体">
<DictEditor
v-model="localConfig.data"
@update:model-value="updateConfig"
/>
</BorderLabel>
</div>
<!-- 请求参数 -->
<div class="col-12" style="margin-top: -8px">
<BorderLabel label="URL参数">
<DictEditor
v-model="localConfig.params"
@update:model-value="updateConfig"
/>
</BorderLabel>
</div>
<!-- 认证信息 -->
<div class="col-12" style="margin-top: -8px">
<BorderLabel label="HTTP认证">
<div class="row q-col-gutter-sm">
<div class="col-6">
<VariableInput
v-model="localConfig.auth.username"
label="用户名"
:command="{ icon: 'person' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col-6">
<VariableInput
v-model="localConfig.auth.password"
label="密码"
:command="{ icon: 'password' }"
@update:model-value="updateConfig"
/>
</div>
</div>
</BorderLabel>
</div>
<!-- 代理设置 -->
<div class="col-12" style="margin-top: -8px">
<BorderLabel label="代理设置">
<div class="row q-col-gutter-sm">
<div class="col-3">
<VariableInput
v-model="localConfig.proxy.host"
label="主机"
:command="{ icon: 'dns' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfig.proxy.port"
label="端口"
:command="{ icon: 'router', inputType: 'number' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfig.proxy.auth.username"
label="用户名"
:command="{ icon: 'person' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfig.proxy.auth.password"
label="密码"
:command="{ icon: 'password' }"
@update:model-value="updateConfig"
/>
</div>
</div>
</BorderLabel>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "../VariableInput.vue";
import HeaderEditor from "./HeaderEditor.vue";
import DictEditor from "../DictEditor.vue";
import { formatJsonVariables } from "js/composer/formatString";
import { userAgent, commonHeaders, contentTypes } from "js/options/httpHeaders";
import BorderLabel from "../BorderLabel.vue";
export default defineComponent({
name: "AxiosConfigEditor",
components: {
VariableInput,
HeaderEditor,
DictEditor,
BorderLabel,
},
props: {
modelValue: {
@ -144,7 +246,6 @@ export default defineComponent({
let initialConfig = {};
if (typeof this.modelValue === "string") {
try {
//
const match = this.modelValue.match(
/axios\.\w+\([^{]*({\s*[^]*})\s*\)/
);
@ -162,15 +263,34 @@ export default defineComponent({
localConfig: {
url: "",
method: "GET",
headers: {},
params: "",
data: "",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
params: {},
data: {},
timeout: 0,
withCredentials: false,
maxRedirects: 5,
responseType: "json",
decompress: true,
auth: {
username: "",
password: "",
},
proxy: {
host: "",
port: "",
auth: {
username: "",
password: "",
},
},
...initialConfig,
},
userAgentOptions: userAgent,
contentTypes,
commonHeaderOptions: commonHeaders
.filter((h) => !["User-Agent", "Content-Type"].includes(h.value))
.map((h) => h.value),
otherHeaders: {},
};
},
created() {
@ -179,72 +299,43 @@ export default defineComponent({
([name, value]) => ({ name, value })
);
},
computed: {
hasRequestData() {
return ["PUT", "POST", "PATCH"].includes(this.localConfig.method);
},
},
methods: {
updateConfig() {
//
const config = { ...this.localConfig };
Object.keys(config).forEach((key) => {
if (
config[key] === "" ||
config[key] === null ||
config[key] === undefined
) {
delete config[key];
}
});
//
const { method = "GET", url, data, ...restConfig } = config;
const { method = "GET", url, data, ...restConfig } = this.localConfig;
if (!url) return;
let code = "";
const variableFields = ["headers", "timeout", "params", "data"];
if (method.toUpperCase() === "GET") {
code = `axios.get(${url}${
Object.keys(restConfig).length
? `, ${formatJsonVariables(restConfig, variableFields)}`
: ""
})`;
} else {
const { data: reqData, ...configWithoutData } = restConfig;
code = `axios.${method.toLowerCase()}(${url}${
reqData ? `, ${reqData}` : ", undefined"
}${
Object.keys(configWithoutData).length
? `, ${formatJsonVariables(restConfig, variableFields)}`
: ""
})`;
}
// VariableInput
const excludeFields = ["headers.Content-Type", "responseType"];
const configStr = Object.keys(restConfig).length
? `, ${formatJsonVariables(restConfig, null, excludeFields)}`
: "";
const code = `axios.${method.toLowerCase()}(${url}${
this.hasRequestData ? `, ${formatJsonVariables(data)}` : ""
}${configStr})?.data`;
this.$emit("update:modelValue", code);
},
addHeader() {
this.headers.push({ name: "", value: "" });
updateHeaders(headers) {
// Content-Type User-Agent
const { "Content-Type": contentType, "User-Agent": userAgent } =
this.localConfig.headers;
// headers
this.localConfig.headers = {
"Content-Type": contentType,
...(userAgent ? { "User-Agent": userAgent } : {}),
...headers,
};
this.updateConfig();
},
removeHeader(index) {
this.headers.splice(index, 1);
this.updateHeaders();
},
updateHeaders() {
this.localConfig.headers = this.headers.reduce((acc, header) => {
if (header.name) {
// 使
if (
header.value &&
!header.value.startsWith('"') &&
!header.value.endsWith('"')
) {
acc[header.name] = header.value;
} else {
// <EFBFBD><EFBFBD>JSON使
try {
acc[header.name] = JSON.parse(header.value);
} catch (e) {
acc[header.name] = header.value;
}
}
}
return acc;
}, {});
setUserAgent(value) {
this.localConfig.headers["User-Agent"] = value;
this.updateConfig();
},
},

View File

@ -1,276 +0,0 @@
<template>
<div class="row q-col-gutter-sm">
<!-- 基础配置 -->
<div class="col-12">
<VariableInput
v-model="localConfig.url"
label="请求地址"
:command="{ icon: 'link' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col-12">
<q-select
v-model="localConfig.method"
:options="['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']"
label="请求方法"
dense
filled
emit-value
map-options
@update:model-value="updateConfig"
>
<template v-slot:prepend>
<q-icon name="send" />
</template>
</q-select>
</div>
<!-- Headers -->
<div class="col-12">
<HeaderEditor
:headers="localConfig.headers"
@input="
(val) => {
localConfig.headers = val;
updateConfig();
}
"
/>
</div>
<!-- 请求体 -->
<div class="col-12">
<VariableInput
v-model="localConfig.body"
label="请求体"
:command="{ icon: 'data_object' }"
@update:model-value="updateConfig"
/>
</div>
<!-- 重定向策略 -->
<div class="col-12">
<q-select
v-model="localConfig.redirect"
:options="['follow', 'error', 'manual']"
label="重定向策略"
dense
filled
emit-value
map-options
@update:model-value="updateConfig"
>
<template v-slot:prepend>
<q-icon name="alt_route" />
</template>
</q-select>
</div>
<!-- 其他选项 -->
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-6">
<VariableInput
v-model="localConfig.follow"
label="最大重定向次数"
:command="{ icon: 'repeat', inputType: 'number' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col-6">
<VariableInput
v-model="localConfig.timeout"
label="超时时间(ms)"
:command="{ icon: 'timer', inputType: 'number' }"
@update:model-value="updateConfig"
/>
</div>
</div>
</div>
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-6">
<VariableInput
v-model="localConfig.size"
label="最大响应大小(bytes)"
:command="{ icon: 'data_usage', inputType: 'number' }"
@update:model-value="updateConfig"
/>
</div>
<div class="col-6">
<q-checkbox
v-model="localConfig.compress"
label="启用压缩"
@update:model-value="updateConfig"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "../VariableInput.vue";
import HeaderEditor from "./HeaderEditor.vue";
export default defineComponent({
name: "FetchConfigEditor",
components: {
VariableInput,
HeaderEditor,
},
props: {
modelValue: {
type: [Object, String],
default: () => ({}),
},
},
emits: ["update:modelValue"],
data() {
let initialConfig = {};
if (typeof this.modelValue === "string") {
try {
//
const match = this.modelValue.match(/fetch\([^{]*({\s*[^]*})\s*\)/);
if (match) {
initialConfig = JSON.parse(match[1]);
}
} catch (e) {
console.warn("Failed to parse config from code string");
}
} else {
initialConfig = this.modelValue;
}
return {
localConfig: {
url: "",
method: "GET",
headers: {},
body: "",
redirect: "follow",
follow: 20,
timeout: 0,
size: 0,
compress: true,
...initialConfig,
},
};
},
created() {
// headers
this.headers = Object.entries(this.localConfig.headers || {}).map(
([name, value]) => ({ name, value })
);
},
methods: {
updateConfig() {
//
const config = { ...this.localConfig };
Object.keys(config).forEach((key) => {
if (
config[key] === "" ||
config[key] === null ||
config[key] === undefined
) {
delete config[key];
}
});
//
const { url, body, headers, ...init } = config;
if (!url) return;
//
if (body) {
init.body = body;
}
// headers
if (headers && Object.keys(headers).length) {
init.headers = headers;
}
const variableFields = [
"headers",
"body",
"redirect",
"follow",
"timeout",
"size",
];
const code = `fetch(${url}${
Object.keys(init).length
? `, ${formatJsonVariables(init, variableFields)}`
: ""
})`;
this.$emit("update:modelValue", code);
},
addHeader() {
this.headers.push({ name: "", value: "" });
},
removeHeader(index) {
this.headers.splice(index, 1);
this.updateHeaders();
},
updateHeaders() {
this.localConfig.headers = this.headers.reduce((acc, header) => {
if (header.name) {
// 使
if (
header.value &&
!header.value.startsWith('"') &&
!header.value.endsWith('"')
) {
acc[header.name] = header.value;
} else {
// JSON使
try {
acc[header.name] = JSON.parse(header.value);
} catch (e) {
acc[header.name] = header.value;
}
}
}
return acc;
}, {});
this.updateConfig();
},
},
watch: {
modelValue: {
deep: true,
handler(newValue) {
if (typeof newValue === "string") {
//
try {
const config = JSON.parse(newValue);
this.localConfig = {
...this.localConfig,
...config,
};
this.headers = Object.entries(config.headers || {}).map(
([name, value]) => ({ name, value })
);
} catch (e) {
//
}
} else {
this.localConfig = {
...this.localConfig,
...newValue,
};
this.headers = Object.entries(this.localConfig.headers || {}).map(
([name, value]) => ({ name, value })
);
}
},
},
},
});
</script>

View File

@ -1,198 +0,0 @@
<template>
<div class="q-pa-sm">
<!-- 添加新header按钮 -->
<div class="row items-center q-gutter-sm">
<q-select
v-model="newHeaderField"
:options="commonHeaders"
label="添加常用Header"
dense
filled
emit-value
map-options
style="width: 200px"
@update:model-value="addCommonHeader"
>
<template v-slot:prepend>
<q-icon name="add" />
</template>
</q-select>
<q-btn flat round dense icon="add" @click="addCustomHeader" />
</div>
<!-- header列表 -->
<div
v-for="(header, index) in headersList"
:key="index"
class="row q-col-gutter-sm q-mt-sm items-center"
>
<!-- header名称 -->
<div class="col-4">
<q-input
v-if="!header.isCommon"
v-model="header.name"
label="Header名称"
dense
filled
@update:model-value="emitUpdate"
/>
<q-input
v-else
:model-value="header.name"
label="Header名称"
dense
filled
readonly
/>
</div>
<!-- header值 -->
<div class="col">
<div class="row items-center q-col-gutter-sm">
<!-- User-Agent特殊处理 -->
<template v-if="header.name === 'User-Agent'">
<div class="col">
<VariableInput
v-model="header.value"
label="User Agent"
:command="{ icon: 'devices' }"
@update:model-value="emitUpdate"
/>
</div>
<div class="col-auto">
<q-btn-dropdown flat dense icon="list">
<q-list>
<q-item
v-for="ua in userAgentOptions"
:key="ua.value"
clickable
v-close-popup
@click="setUserAgent(header, ua.value)"
>
<q-item-section>
<q-item-label>{{ ua.label }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</template>
<!-- 其他header -->
<template v-else>
<div class="col">
<VariableInput
v-model="header.value"
:label="header.name"
:command="{ icon: 'code' }"
@update:model-value="emitUpdate"
/>
</div>
</template>
<!-- 删除按钮 -->
<div class="col-auto">
<q-btn
flat
round
dense
icon="delete"
@click="removeHeader(index)"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "../VariableInput.vue";
import { userAgent, commonHeaders } from "js/options/httpHeaders";
export default defineComponent({
name: "HeaderEditor",
components: {
VariableInput,
},
props: {
headers: {
type: [Object, String],
default: () => ({}),
},
},
emits: ["input"],
data() {
let headersObj = {};
if (typeof this.headers === "string") {
try {
const match = this.headers.match(/headers":\s*({[^}]+})/);
if (match) {
headersObj = JSON.parse(match[1]);
}
} catch (e) {
console.warn("Failed to parse headers from code string");
}
} else if (typeof this.headers === "object") {
headersObj = this.headers;
}
return {
headersList: Object.entries(headersObj).map(([name, value]) => ({
name,
value: typeof value === "string" ? value : JSON.stringify(value),
isCommon: commonHeaders.some((h) => h.value === name),
})),
newHeaderField: null,
commonHeaders,
userAgentOptions: userAgent,
};
},
methods: {
addCommonHeader(headerName) {
if (!headerName) return;
if (this.headersList.some((h) => h.name === headerName)) {
this.newHeaderField = null;
return;
}
this.headersList.push({
name: headerName,
value: "",
isCommon: true,
});
this.$nextTick(() => {
this.newHeaderField = null;
});
this.emitUpdate();
},
addCustomHeader() {
this.headersList.push({
name: "",
value: "",
isCommon: false,
});
},
removeHeader(index) {
this.headersList.splice(index, 1);
this.emitUpdate();
},
setUserAgent(header, value) {
header.value = value;
this.emitUpdate();
},
emitUpdate() {
const headers = {};
this.headersList.forEach((header) => {
if (header.name && header.value) {
headers[header.name] = header.value;
}
});
this.$emit("input", headers);
},
},
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="row q-col-gutter-sm">
<div class="row q-col-gutter-sm ubrowser-function-input">
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-3">
@ -35,6 +35,7 @@
type="textarea"
dense
borderless
style="font-family: monospace, monoca, consola"
autogrow
@update:model-value="updateFunction"
>
@ -198,7 +199,39 @@ export default defineComponent({
</script>
<style scoped>
:deep(.q-field__control) .text-primary.func-symbol {
font-size: 18px !important;
.ubrowser-function-input :deep(.q-field__control) .text-primary.func-symbol {
font-size: 24px !important;
}
.ubrowser-function-input :deep(.q-select__input) {
display: flex !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
.ubrowser-function-input :deep(.q-select .q-field__native) {
display: flex !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
.ubrowser-function-input :deep(.q-select .q-field__native > div) {
display: flex !important;
flex-wrap: nowrap !important;
flex: 0 0 auto !important;
}
.ubrowser-function-input :deep(.q-select .q-chip) {
flex: 0 0 auto !important;
margin-right: 4px !important;
}
.ubrowser-function-input :deep(.q-select__input::-webkit-scrollbar),
.ubrowser-function-input :deep(.q-select .q-field__native::-webkit-scrollbar) {
display: none !important;
}
</style>

View File

@ -42,28 +42,20 @@ export const commandCategories = [
},
{
value: "ubrowser",
label: "UBrowser浏览器操作",
desc: "配置UBrowser浏览器操作",
label: "ubrowser浏览器操作",
desc: "配置ubrowser浏览器操作",
hasUBrowserEditor: true,
isAsync: true,
icon: "public",
},
{
value: "axios",
label: "发送HTTP请求(Axios)",
label: "HTTP请求(Axios)",
desc: "使用Axios发送HTTP请求",
hasAxiosEditor: true,
isAsync: true,
icon: "http",
},
{
value: "fetch",
label: "发送HTTP请求(Fetch)",
desc: "使用Fetch API发送HTTP请求",
hasFetchEditor: true,
isAsync: true,
icon: "http",
},
],
},
{
@ -82,24 +74,31 @@ export const commandCategories = [
desc: "要写入剪切板的内容",
icon: "content_copy",
},
{
value: "electron.clipboard.readText",
label: "获取剪贴板内容",
desc: "获取剪贴板内容",
icon: "content_copy",
hasNoArgs: true,
},
],
},
{
label: "消息通知",
icon: "notifications",
commands: [
{
value: "console.log",
label: "打印消息",
desc: "要打印的消息文本",
icon: "info",
},
{
value: "message",
label: "发送系统消息",
desc: "要发送的系统消息文本",
icon: "message",
},
{
value: "quickcommand.showMessageBox",
label: "弹窗显示消息",
desc: "要弹窗显示的消息文本",
icon: "warning",
},
{
value: "send",
label: "发送文本到活动窗口",

View File

@ -12,14 +12,46 @@ const processVariableValue = (value) => {
return value.slice(1, -1);
};
/**
* 检查路径是否匹配或是目标路径的父路径
* @param {string} currentPath 当前路径
* @param {string[]} targetPaths 目标路径列表
* @returns {boolean} 是否匹配
*/
const isPathMatched = (currentPath, targetPaths) => {
if (!targetPaths) return false;
return targetPaths.some(
(path) =>
path === currentPath || // 精确匹配
path.startsWith(currentPath + ".") // 是父路径
);
};
/**
* 递归移除对象中的空值
* @param {Object} obj 要处理的对象
* @returns {Object} 处理后的对象
*/
const removeEmptyValues = (obj) => {
return _.omitBy(obj, (value) => {
if (_.isNil(value) || value === "") return true;
if (typeof value === "object")
return _.isEmpty(removeEmptyValues(value));
return false;
});
};
/**
* 递归处理对象的值
* @param {Object} obj 要处理的对象
* @param {string} parentPath 父路径
* @param {string[]|null} variableFields 需要处理的字段列表null表示处理所有字段
* @param {string[]|null} excludeFields 需要排除的字段列表即使匹配了处理条件也不处理
* @returns {string} 处理后的字符串
*/
const processObject = (obj, parentPath = "", variableFields) => {
const processObject = (obj, parentPath = "", variableFields, excludeFields) => {
// 移除空值
obj = removeEmptyValues(obj);
let result = "{\n";
const entries = Object.entries(obj);
@ -28,16 +60,20 @@ const processObject = (obj, parentPath = "", variableFields) => {
let valueStr = "";
// 检查是否需要处理当前字段
const shouldProcess =
!variableFields || // 不传递variableFields则处理所有字段
variableFields.includes(parentPath) || // 父字段是完整处理字段
variableFields.includes(key) || // 当前字段是完整处理字段
variableFields.includes(currentPath) || // 当前路径精确匹配
variableFields.some((field) => field.startsWith(currentPath + ".")); // 当前路径是指定路径的父路径
const isIncluded =
!variableFields || isPathMatched(currentPath, variableFields);
const isExcluded =
excludeFields && isPathMatched(currentPath, excludeFields);
const shouldProcess = isIncluded && !isExcluded;
// 处理对象类型
if (typeof value === "object" && value !== null) {
valueStr = processObject(value, currentPath, variableFields);
valueStr = processObject(
value,
currentPath,
variableFields,
excludeFields
);
}
// 处理字符串类型
else if (typeof value === "string") {
@ -71,13 +107,19 @@ const processObject = (obj, parentPath = "", variableFields) => {
* 1. 完整字段处理 headers - 处理整个对象及其所有子字段
* 2. 指定路径处理 data.headers.Referer - 只处理特定路径
* 3. 不传递 variableFields 则处理所有字段
* 4. 可以通过 excludeFields 排除特定字段即使匹配了处理条件也不处理
* @param {string} jsonStr JSON字符串
* @param {string[]|null} [variableFields] 需要处理的字段列表包括完整字段和指定路径不传则处理所有字段
* @param {string[]|null} [excludeFields] 需要排除的字段列表即使匹配了处理条件也不处理
* @returns {string} 处理后的字符串
*/
export const formatJsonVariables = (jsonObj, variableFields = null) => {
export const formatJsonVariables = (
jsonObj,
variableFields = null,
excludeFields = null
) => {
try {
return processObject(jsonObj, "", variableFields);
return processObject(jsonObj, "", variableFields, excludeFields);
} catch (e) {
console.warn("Failed to process JSON variables:", e);
return JSON.stringify(jsonObj, null, 2);

View File

@ -55,12 +55,15 @@ export const commonHeaders = [
{ label: "Content-Type", value: "Content-Type" },
{ label: "Authorization", value: "Authorization" },
{ label: "User-Agent", value: "User-Agent" },
{ label: "Cookie", value: "Cookie" },
{ label: "Accept", value: "Accept" },
{ label: "Accept-Language", value: "Accept-Language" },
{ label: "Accept-Encoding", value: "Accept-Encoding" },
{ label: "Cookie", value: "Cookie" },
{ label: "Origin", value: "Origin" },
{ label: "Referer", value: "Referer" },
{ label: "X-Requested-With", value: "X-Requested-With" },
{ label: "X-Forwarded-For", value: "X-Forwarded-For" },
{ label: "X-Real-IP", value: "X-Real-IP" },
];
export const deviceName = [
@ -75,3 +78,38 @@ export const deviceName = [
{ label: "HUAWEI Mate30", value: "HUAWEI Mate30" },
{ label: "HUAWEI Mate30 Pro", value: "HUAWEI Mate30 Pro" },
];
export const contentTypes = [
{
label: "application/json",
value: "application/json",
},
{
label: "application/x-www-form-urlencoded",
value: "application/x-www-form-urlencoded",
},
{
label: "multipart/form-data",
value: "multipart/form-data",
},
{
label: "text/plain",
value: "text/plain",
},
{
label: "text/html",
value: "text/html",
},
{
label: "text/xml",
value: "text/xml",
},
{
label: "application/xml",
value: "application/xml",
},
{
label: "application/octet-stream",
value: "application/octet-stream",
},
];