使用通用组件重构Ubrowser的operations部分

This commit is contained in:
fofolee 2025-01-29 20:56:59 +08:00
parent ae8edfd710
commit 5a0675d913
18 changed files with 1101 additions and 2126 deletions

View File

@ -18,7 +18,9 @@
{ 'check-btn--selected': isSelected(option.value) }, { 'check-btn--selected': isSelected(option.value) },
]" ]"
:style="{ :style="{
flex: `1 0 ${100 / options.length}%`, flex: `1 0 calc(${100 / options.length}% - ${
(4 * (options.length - 1)) / options.length
}px)`,
}" }"
@click="toggleOption(option.value)" @click="toggleOption(option.value)"
> >
@ -108,7 +110,7 @@ export default defineComponent({
height: auto !important; height: auto !important;
min-height: 32px; min-height: 32px;
font-size: 12px; font-size: 12px;
padding: 4px 12px; padding: 4px 8px;
border-radius: 4px !important; border-radius: 4px !important;
transition: all 0.3s; transition: all 0.3s;
background-color: rgba(0, 0, 0, 0.03); background-color: rgba(0, 0, 0, 0.03);

View File

@ -168,7 +168,10 @@ export default defineComponent({
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
created() { created() {
if (!this.modelValue || Object.keys(this.modelValue).length === 0) { if (
(!this.modelValue || Object.keys(this.modelValue).length === 0) &&
!this.options?.disableAdd
) {
this.$emit("update:modelValue", { "": newVarInputVal("str") }); this.$emit("update:modelValue", { "": newVarInputVal("str") });
} }
}, },

View File

@ -0,0 +1,175 @@
<template>
<div class="row q-col-gutter-sm function-input">
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-3">
<q-select
:model-value="params"
use-input
use-chips
multiple
dense
borderless
hide-dropdown-icon
options-dense
input-debounce="0"
new-value-mode="add-unique"
label="参数"
@update:model-value="updateParams"
@input-value="handleInput"
@blur="handleBlur"
ref="paramSelect"
>
<template v-slot:prepend>
<div class="text-primary func-symbol">(</div>
</template>
<template v-slot:append>
<div class="text-primary func-symbol">)</div>
</template>
</q-select>
</div>
<div class="col-9">
<q-input
:model-value="funcContent"
:label="label"
type="textarea"
dense
borderless
:placeholder="placeholder"
style="font-family: Consolas, Monaco, 'Courier New'"
autogrow
@update:model-value="updateContent"
>
<template v-slot:prepend>
<div class="text-primary func-symbol">=> {</div>
</template>
<template v-slot:append>
<div class="text-primary func-symbol">}</div>
</template>
</q-input>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { newVarInputVal } from "src/js/composer/varInputValManager";
export default defineComponent({
name: "FunctionInput",
props: {
modelValue: {
type: Object,
default: newVarInputVal("var", ""),
},
label: {
type: String,
default: "函数内容",
},
icon: {
type: String,
default: "code",
},
placeholder: {
type: String,
default: "",
},
},
emits: ["update:model-value"],
computed: {
funcStr() {
return this.modelValue.value;
},
params() {
const match = this.funcStr.match(/^\((.*?)\)/);
if (!match) return [];
return match[1]
.split(",")
.map((p) => p.trim())
.filter((p) => p);
},
funcContent() {
const match = this.funcStr.match(/\{([\s\S]*)\}/);
if (!match) return "";
return match[1].trim();
},
},
methods: {
updateParams(newParams) {
const functionStr = `(${newParams.join(", ")}) => { ${
this.funcContent
} }`;
this.updateModelValue(functionStr);
},
updateContent(newContent) {
const functionStr = `(${this.params.join(", ")}) => { ${newContent} }`;
this.updateModelValue(functionStr);
},
handleInput(val) {
if (!val) return;
if (val.includes(",") || val.includes(" ")) {
const newParams = val
.split(/[,\s]+/)
.map((p) => p.trim())
.filter((p) => p);
const allParams = [...new Set([...this.params, ...newParams])];
this.updateParams(allParams);
this.$refs.paramSelect.updateInputValue("");
}
},
handleBlur() {
const inputValue = this.$refs.paramSelect.inputValue;
if (inputValue && !this.params.includes(inputValue)) {
const newParams = [...this.params, inputValue];
this.updateParams(newParams);
this.$refs.paramSelect.updateInputValue("");
}
},
updateModelValue(newVal) {
this.$emit("update:model-value", newVarInputVal("var", newVal));
},
},
});
</script>
<style scoped>
.function-input :deep(.q-field__control) .text-primary.func-symbol {
font-size: 24px !important;
}
.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;
}
.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;
}
.function-input :deep(.q-select .q-field__native > div) {
display: flex !important;
flex-wrap: nowrap !important;
flex: 0 0 auto !important;
}
.function-input :deep(.q-select .q-chip) {
flex: 0 0 auto !important;
margin-right: 4px !important;
}
.function-input :deep(.q-select__input::-webkit-scrollbar),
.function-input :deep(.q-select .q-field__native::-webkit-scrollbar) {
display: none !important;
}
</style>

View File

@ -74,6 +74,7 @@
filled filled
emit-value emit-value
map-options map-options
options-dense
:options="contentTypes" :options="contentTypes"
@update:model-value=" @update:model-value="
updateArgvs('headers.Content-Type', $event) updateArgvs('headers.Content-Type', $event)

View File

@ -23,6 +23,7 @@ import ControlInput from "components/composer/common/ControlInput.vue";
import CheckGroup from "components/composer/common/CheckGroup.vue"; import CheckGroup from "components/composer/common/CheckGroup.vue";
import CheckButton from "components/composer/common/CheckButton.vue"; import CheckButton from "components/composer/common/CheckButton.vue";
import TimeInput from "components/composer/common/TimeInput.vue"; import TimeInput from "components/composer/common/TimeInput.vue";
import FunctionInput from "components/composer/common/FunctionInput.vue";
import { QInput, QSelect, QToggle, QCheckbox } from "quasar"; import { QInput, QSelect, QToggle, QCheckbox } from "quasar";
const CodeEditor = defineAsyncComponent(() => const CodeEditor = defineAsyncComponent(() =>
import("components/composer/common/CodeEditor.vue") import("components/composer/common/CodeEditor.vue")
@ -45,6 +46,7 @@ export default defineComponent({
QCheckbox, QCheckbox,
TimeInput, TimeInput,
CodeEditor, CodeEditor,
FunctionInput,
}, },
props: { props: {
config: { config: {
@ -85,6 +87,7 @@ export default defineComponent({
if (this.isQuasarSelect) { if (this.isQuasarSelect) {
props.emitValue = true; props.emitValue = true;
props.mapOptions = true; props.mapOptions = true;
props.optionsDense = true;
} }
return props; return props;

View File

@ -1,126 +1,161 @@
<template> <template>
<div class="ubrowser-basic">
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<!-- 基础配置 --> <!-- 基础配置 -->
<div class="col-12"> <div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-9">
<VariableInput <VariableInput
v-model="localConfigs.goto.url" v-model="url"
label="网址" label="网址"
icon="link" icon="link"
@update:model-value="updateConfigs" class="col"
:options="{
items: userAgentOptions,
}"
/> />
</div> </div>
<div class="col-3">
<NumberInput
v-model="timeout"
label="超时时间(ms)"
icon="timer"
:min="0"
:step="1000"
/>
</div>
</div>
</div>
<!-- Headers配置 --> <!-- Headers配置 -->
<div class="col-12"> <div class="col-12">
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-12"> <div class="col-12">
<VariableInput <VariableInput
v-model="localConfigs.goto.headers.Referer" v-model="userAgent"
label="Referer"
icon="link"
@update:model-value="updateConfigs"
/>
</div>
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col">
<VariableInput
v-model="localConfigs.goto.headers.userAgent"
label="User-Agent" label="User-Agent"
icon="devices" icon="devices"
@update:model-value="updateConfigs" :options="{
items: userAgentOptions,
}"
/> />
</div> </div>
<div class="col-auto">
<q-select
:model-value="selectedUA"
@update:model-value="handleUAChange"
:options="userAgentOptions"
label="常用 UA"
dense
filled
emit-value
map-options
options-dense
style="min-width: 150px"
>
<template v-slot:prepend>
<q-icon name="list" />
</template>
</q-select>
</div>
</div>
</div>
</div>
</div>
<!-- 超时配置 -->
<div class="col-12"> <div class="col-12">
<NumberInput <DictEditor
v-model="localConfigs.goto.timeout" v-model="otherHeaders"
icon="timer" label="其他请求头"
label="超时时间(ms)" :options="{
@update:model-value="updateConfigs" optionKeys: commonHeaders,
}"
/> />
</div> </div>
</div> </div>
</div>
</div>
</div>
</template> </template>
<script> <script>
import { defineComponent, ref, computed } from "vue";
import { userAgent } from "js/options/httpOptions";
import VariableInput from "components/composer/common/VariableInput.vue"; import VariableInput from "components/composer/common/VariableInput.vue";
import NumberInput from "components/composer/common/NumberInput.vue"; import NumberInput from "components/composer/common/NumberInput.vue";
import DictEditor from "components/composer/common/DictEditor.vue";
import { userAgent, commonHeaders } from "js/options/httpOptions";
import { newVarInputVal } from "js/composer/varInputValManager"; import { newVarInputVal } from "js/composer/varInputValManager";
export default defineComponent({
export default {
name: "UBrowserBasic", name: "UBrowserBasic",
components: { components: {
VariableInput, VariableInput,
NumberInput, NumberInput,
DictEditor,
}, },
props: { props: {
configs: { modelValue: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
emits: ["update:configs"], emits: ["update:model-value"],
setup(props, { emit }) { data() {
const selectedUA = ref(null);
// 使 computed
const localConfigs = computed({
get: () => props.configs,
set: (val) => {
emit("update:configs", val);
},
});
//
const updateConfigs = () => {
emit("update:configs", localConfigs.value);
};
// UA
const handleUAChange = (val) => {
if (!val) return;
const newConfigs = window.lodashM.cloneDeep(props.configs);
if (!newConfigs.goto.headers) {
newConfigs.goto.headers = {};
}
newConfigs.goto.headers.userAgent = newVarInputVal("str", val);
emit("update:configs", newConfigs);
selectedUA.value = null;
};
return { return {
selectedUA, selectedUA: null,
localConfigs,
userAgentOptions: userAgent, userAgentOptions: userAgent,
updateConfigs, commonHeaders: commonHeaders,
handleUAChange,
}; };
}, },
}); computed: {
url: {
get() {
return this.modelValue.url || newVarInputVal("str", "");
},
set(value) {
this.updateField("url", value);
},
},
userAgent: {
get() {
return (
this.modelValue.headers?.["User-Agent"] || newVarInputVal("str", "")
);
},
set(value) {
this.updateHeaders("User-Agent", value);
},
},
timeout: {
get() {
return this.modelValue.timeout || 60000;
},
set(value) {
this.updateField("timeout", value);
},
},
otherHeaders: {
get() {
if (!this.modelValue.headers) return {};
const standardHeaders = ["User-Agent"];
return Object.entries(this.modelValue.headers)
.filter(([key]) => !standardHeaders.includes(key))
.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
},
set(value) {
const userAgent = this.modelValue.headers?.["User-Agent"];
const newHeaders = {
...(userAgent ? { "User-Agent": userAgent } : {}),
...value,
};
this.updateField("headers", newHeaders);
},
},
},
methods: {
updateField(field, value) {
this.$emit("update:model-value", {
...this.modelValue,
[field]: value,
});
},
updateHeaders(key, value) {
const headers = {
...this.modelValue.headers,
[key]: value,
};
this.updateField("headers", headers);
},
handleUAChange(value) {
if (!value) return;
this.updateHeaders("User-Agent", newVarInputVal("str", value));
this.selectedUA = null;
},
},
};
</script> </script>
<style scoped>
.ubrowser-basic {
width: 100%;
}
</style>

View File

@ -1,52 +1,57 @@
<template> <template>
<div class="ubrowser-editor"> <div class="ubrowser-editor">
<!-- 标签页导航 -->
<q-tabs <q-tabs
v-model="step" v-model="tab"
class="ubrowser-tabs"
dense dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="left" align="left"
narrow-indicator narrow-indicator
no-caps
inline-label inline-label
active-class="ubrowser-tabs-active" outside-arrows
mobile-arrows
> >
<q-tab name="1" icon="settings" label="基础参数" size="sm" /> <q-tab name="1" label="基础配置" icon="link" class="q-px-sm" />
<q-tab name="2" icon="touch_app" label="浏览器操作" size="sm" /> <q-tab name="2" label="操作配置" icon="touch_app" class="q-px-sm" />
<q-tab name="3" icon="settings_applications" label="运行参数" size="sm" /> <q-tab name="3" label="运行配置" icon="settings" class="q-px-sm" />
</q-tabs> </q-tabs>
<!-- 内容区域 --> <q-separator />
<q-tab-panels v-model="step" class="ubrowser-panels">
<q-tab-panels v-model="tab" class="ubrowser-panels" animated>
<q-tab-panel name="1" class="panel-content"> <q-tab-panel name="1" class="panel-content">
<UBrowserBasic :configs="configs" @update:configs="updateConfigs" /> <UBrowserBasic
v-model="argvs.goto"
@update:model-value="updateArgvs('goto', $event)"
/>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="2" class="panel-content"> <q-tab-panel name="2" class="panel-content">
<UBrowserOperations <UBrowserOperations
:configs="configs" v-model="argvs.operations"
:selected-actions="selectedActions" @update:model-value="updateArgvs('operations', $event)"
@update:configs="updateConfigs"
@update:selected-actions="(val) => (selectedActions = val)"
@remove-action="removeAction"
/> />
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="3" class="panel-content"> <q-tab-panel name="3" class="panel-content">
<UBrowserRun :configs="configs" @update:configs="updateConfigs" /> <UBrowserRun
v-model="argvs.run"
@update:model-value="updateArgvs('run', $event)"
/>
</q-tab-panel> </q-tab-panel>
</q-tab-panels> </q-tab-panels>
</div> </div>
</template> </template>
<script> <script>
import { defineComponent, ref, computed } from "vue"; import UBrowserBasic from "components/composer/ubrowser/UBrowserBasic.vue";
import UBrowserBasic from "./UBrowserBasic.vue"; import UBrowserOperations from "components/composer/ubrowser/UBrowserOperations.vue";
import UBrowserOperations from "./UBrowserOperations.vue"; import UBrowserRun from "components/composer/ubrowser/UBrowserRun.vue";
import UBrowserRun from "./UBrowserRun.vue";
import { defaultUBrowserConfigs } from "js/composer/ubrowserConfig";
import { generateUBrowserCode } from "js/composer/generateUBrowserCode"; import { generateUBrowserCode } from "js/composer/generateUBrowserCode";
export default defineComponent({ export default {
name: "UBrowserEditor", name: "UBrowserEditor",
components: { components: {
UBrowserBasic, UBrowserBasic,
@ -59,76 +64,59 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
emits: ["update:modelValue"], emits: ["update:model-value"],
setup(props, { emit }) { data() {
//
const step = ref("1");
const selectedActions = ref([]);
// run
const localConfigs = ref(window.lodashM.cloneDeep(defaultUBrowserConfigs));
if (props.modelValue?.argvs) {
// run
localConfigs.value = {
...localConfigs.value,
...props.modelValue.argvs,
run: {
...localConfigs.value.run,
...props.modelValue.argvs.run,
},
};
}
const getSummary = (argvs) => {
return argvs.goto.url.value;
};
// argvs
const argvs = computed({
get: () => localConfigs.value,
set: (val) => {
// run
const newConfigs = {
...val,
run: {
...localConfigs.value.run,
...val.run,
},
};
localConfigs.value = newConfigs;
emit("update:modelValue", {
...props.modelValue,
argvs: newConfigs,
summary: getSummary(newConfigs),
code: generateUBrowserCode(newConfigs, selectedActions.value),
});
},
});
//
const updateConfigs = (newConfigs) => {
argvs.value = window.lodashM.cloneDeep(newConfigs);
};
//
const removeAction = (action) => {
selectedActions.value = selectedActions.value.filter(
(a) => a.id !== action.id
);
const newConfigs = { ...argvs.value };
delete newConfigs[action.value];
argvs.value = newConfigs;
};
return { return {
step, tab: "1",
selectedActions, defaultArgvs: {
configs: argvs, goto: [],
updateConfigs, operations: [],
removeAction, run: {},
},
}; };
}, },
}); computed: {
configs() {
return this.modelValue;
},
argvs() {
return this.modelValue.argvs || this.defaultArgvs;
},
summary() {
const goto = this.argvs.goto?.url || "";
return `访问 ${goto}`;
},
},
mounted() {
this.initializeConfigs();
},
methods: {
initializeConfigs() {
if (!this.modelValue.argvs) {
this.updateModelValue(this.defaultArgvs);
}
},
generateCode() {
return generateUBrowserCode(this.argvs);
},
updateArgvs(key, value) {
const newArgvs = { ...this.argvs };
newArgvs[key] = value;
this.updateModelValue(newArgvs);
},
updateModelValue(argvs) {
this.$emit("update:model-value", {
...this.modelValue,
argvs,
summary: this.summary,
code: this.generateCode(),
});
},
parseCodeToArgvs(code) {
// TODO:
},
},
};
</script> </script>
<style> <style>
@ -137,42 +125,67 @@ export default defineComponent({
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 100%; width: 100%;
} background-color: var(--q-card-background);
.ubrowser-tabs-active {
color: var(--q-primary);
}
.ubrowser-tabs {
flex-shrink: 0;
} }
.ubrowser-panels { .ubrowser-panels {
flex: 1;
overflow: hidden;
background-color: transparent;
display: flex;
flex-direction: column;
}
.ubrowser-panels :deep(.q-tab-panel) {
padding: 0;
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
.panel-content { .panel-content {
padding: 8px; padding: 12px;
min-height: 200px; height: 100%;
overflow: auto;
} }
.ubrowser-panels :deep(.q-tab-panel) { .ubrowser-editor :deep(.q-tabs) {
padding: 0; min-height: 36px;
padding: 0 8px;
background-color: var(--q-card-background);
} }
.ubrowser-tabs :deep(.q-tab) { .ubrowser-editor :deep(.q-tab) {
min-height: 40px; min-height: 36px;
padding: 0 16px; padding: 0 12px;
font-size: 13px;
font-weight: 500;
text-transform: none;
opacity: 0.7;
} }
.ubrowser-tabs :deep(.q-tab__icon) { .ubrowser-editor :deep(.q-tab--active) {
font-size: 20px; opacity: 1;
} }
.ubrowser-tabs :deep(.q-tab__label) { .ubrowser-editor :deep(.q-tab__icon) {
font-size: 14px; font-size: 16px;
margin-right: 4px;
}
.ubrowser-editor :deep(.q-tab__label) {
line-height: 1.2; line-height: 1.2;
margin-left: 8px; }
.ubrowser-editor :deep(.q-tab__indicator) {
height: 2px;
}
/* 暗色模式适配 */
.body--dark .ubrowser-editor {
background-color: var(--q-dark);
}
.body--dark .ubrowser-editor :deep(.q-tabs) {
background-color: var(--q-dark);
} }
</style> </style>

View File

@ -1,11 +1,12 @@
<template> <template>
<div class="ubrowser-operations">
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-12"> <div class="col-12">
<!-- 操作选择网格 --> <!-- 操作选择网格 -->
<div class="row q-col-gutter-xs"> <div class="row q-col-gutter-xs">
<div <div
v-for="action in ubrowserOperationConfigs" v-for="[actionKey, action] in Object.entries(operationsMap)"
:key="action.value" :key="actionKey"
class="col-2" class="col-2"
> >
<q-card <q-card
@ -13,11 +14,9 @@
bordered bordered
class="action-card cursor-pointer" class="action-card cursor-pointer"
:class="{ :class="{
'action-selected': selectedActions.some( 'action-selected': selectedActionKeys.includes(actionKey),
(a) => a.value === action.value
),
}" }"
@click="toggleAction(action)" @click="toggleAction(actionKey)"
> >
<div class="q-pa-xs text-caption text-wrap text-center"> <div class="q-pa-xs text-caption text-wrap text-center">
{{ action.label }} {{ action.label }}
@ -25,29 +24,36 @@
</q-card> </q-card>
</div> </div>
</div> </div>
</div>
</div>
<!-- 已选操作列表 --> <!-- 已选操作列表 -->
<q-list separator class="operation-list q-mt-md"> <q-list separator class="operation-list q-mt-md">
<div <div
v-for="(action, index) in selectedActions" v-for="(selectedActionKey, index) in selectedActionKeys"
:key="action.id" :key="selectedActionKey"
class="operation-item" class="operation-item"
> >
<div class="row items-center justify-between"> <div class="row items-center justify-between">
<q-chip <q-chip
square square
removable removable
@remove="$emit('remove-action', action)" @remove="toggleAction(selectedActionKey)"
class="text-caption q-mx-none q-mb-sm" class="text-caption q-mb-sm"
:style="{
paddingLeft: '7px',
}"
> >
<q-avatar color="primary"> <q-avatar color="primary">
<q-icon <q-icon
color="white" color="white"
:name="getActionProps(action, 'icon') || 'touch_app'" :name="operationsMap[selectedActionKey].icon || 'touch_app'"
size="14px" size="14px"
/> />
</q-avatar> </q-avatar>
<div class="q-mx-sm">{{ action.label }}</div> <div class="q-mx-sm">
{{ operationsMap[selectedActionKey].label }}
</div>
</q-chip> </q-chip>
<div class="row items-start q-gutter-xs"> <div class="row items-start q-gutter-xs">
<q-btn <q-btn
@ -65,115 +71,104 @@
dense dense
color="primary" color="primary"
icon="south" icon="south"
v-show="index !== selectedActions.length - 1" v-show="index !== selectedActionKeys.length - 1"
@click="moveAction(index, 1)" @click="moveAction(index, 1)"
size="xs" size="xs"
class="move-btn" class="move-btn"
/> />
</div> </div>
</div> </div>
<div v-if="getActionProps(action, 'config')"> <div
<UBrowserOperation v-if="operationsMap[selectedActionKey].config"
:configs="configs" class="operation-config"
:action="action.value" >
:fields="getActionProps(action, 'config')" <ParamInput
@update:configs="$emit('update:configs', $event)" :configs="operationsMap[selectedActionKey].config"
:values="selectedActionArgs[index]"
@update="
(argvIndex, argvVal) =>
updateActionArgs(argvIndex, argvVal, index)
"
/> />
</div> </div>
</div> </div>
</q-list> </q-list>
</div> </div>
</div>
</template> </template>
<script> <script>
import { defineComponent } from "vue"; import { ubrowserOperationConfigs } from "js/composer/ubrowserConfig";
import { ubrowserOperationConfigs } from "js/composer/composerConfig"; import ParamInput from "components/composer/param/ParamInput.vue";
import UBrowserOperation from "./operations/UBrowserOperation.vue";
export default defineComponent({ export default {
name: "UBrowserOperations", name: "UBrowserOperations",
components: { components: {
UBrowserOperation, ParamInput,
}, },
props: { props: {
configs: { modelValue: {
type: Object,
required: true,
},
selectedActions: {
type: Array, type: Array,
required: true, required: true,
default: () => [],
}, },
}, },
emits: ["remove-action", "update:selectedActions", "update:configs"], emits: ["update:model-value"],
setup() { data() {
return { return {
ubrowserOperationConfigs, operationsMap: ubrowserOperationConfigs,
}; };
}, },
computed: {
selectedActionKeys() {
return this.modelValue.map((x) => x.key || x.value);
},
selectedActionArgs() {
return this.modelValue.map((x) => x.args);
},
},
methods: { methods: {
moveAction(index, direction) { moveAction(index, direction) {
const newIndex = index + direction; const newIndex = index + direction;
if (newIndex >= 0 && newIndex < this.selectedActions.length) { if (newIndex >= 0 && newIndex < this.selectedActionKeys.length) {
const actions = [...this.selectedActions]; const newOperation = [...this.modelValue];
[actions[index], actions[newIndex]] = [ [newOperation[index], newOperation[newIndex]] = [
actions[newIndex], newOperation[newIndex],
actions[index], newOperation[index],
]; ];
this.$emit("update:selectedActions", actions); this.$emit("update:model-value", newOperation);
} }
}, },
toggleAction(action) { toggleAction(actionKey) {
const index = this.selectedActions.findIndex( const index = this.selectedActionKeys.indexOf(actionKey);
(a) => a.value === action.value let newOperation = [...this.modelValue];
); if (index !== -1) {
if (index === -1) {
//
const newAction = {
...action,
id: this.$root.getUniqueId(),
argv: "",
saveOutput: false,
cmd: action.value || action.cmd,
value: action.value || action.cmd,
};
this.$emit("update:selectedActions", [
...this.selectedActions,
newAction,
]);
//
const { config } = action;
if (config) {
const newConfigs = { ...this.configs };
if (!newConfigs[action.value]) {
newConfigs[action.value] = {};
//
config.forEach((field) => {
if (field.defaultValue !== undefined) {
newConfigs[action.value][field.key] = field.defaultValue;
}
});
this.$emit("update:configs", newConfigs);
}
}
} else {
// //
const newActions = [...this.selectedActions]; newOperation.splice(index, 1);
newActions.splice(index, 1); } else {
this.$emit("update:selectedActions", newActions); //
const { config, value } = this.operationsMap[actionKey];
const args = config?.length
? config.map((field) => field.defaultValue)
: [];
const newOperationItem = { value, args };
if (actionKey !== value) {
newOperationItem.key = actionKey;
} }
newOperation.push(newOperationItem);
}
this.$emit("update:model-value", newOperation);
}, },
getActionProps(action, key) { updateActionArgs(argvIndex, argvVal, actionIndex) {
return this.ubrowserOperationConfigs.find( const newOperation = [...this.modelValue];
(a) => a.value === action.value const newArgs = [...newOperation[actionIndex].args];
)?.[key]; newArgs[argvIndex] = argvVal;
newOperation[actionIndex].args = newArgs;
this.$emit("update:model-value", newOperation);
}, },
}, },
}); };
</script> </script>
<style scoped> <style scoped>
@ -196,14 +191,10 @@ export default defineComponent({
padding: 2px 4px; padding: 2px 4px;
border-color: rgba(0, 0, 0, 0.15); border-color: rgba(0, 0, 0, 0.15);
} }
/*
.operation-item:hover {
background: rgba(0, 0, 0, 0.05);
}
.body--dark .operation-item:hover { .operation-config {
background: rgba(0, 0, 0, 0.25); width: 100%;
} */ }
.move-btn { .move-btn {
opacity: 0.6; opacity: 0.6;
@ -214,32 +205,9 @@ export default defineComponent({
opacity: 1; opacity: 1;
} }
.delete-btn {
opacity: 0.6;
transition: opacity 0.3s;
}
.operation-item:hover .delete-btn {
opacity: 1;
}
.text-subtitle2 {
font-size: 0.9rem;
font-weight: 500;
}
.q-item-section {
transition: all 0.3s;
}
.operation-item:hover .q-item-section {
opacity: 1;
}
.action-card { .action-card {
transition: all 0.3s ease; transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
/* min-height: 42px; */
} }
.action-card:hover { .action-card:hover {

View File

@ -1,33 +1,14 @@
<template> <template>
<div class="ubrowser-run">
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<!-- 窗口显示控制 --> <!-- 窗口显示控制 -->
<div class="col-12"> <div class="col-12">
<div class="row items-center q-gutter-x-md"> <div class="row items-center q-gutter-x-md">
<q-checkbox <q-checkbox v-model="fields.show" label="显示窗口" />
:model-value="localConfigs.run.show" <q-checkbox v-model="fields.center" label="居中显示" />
label="显示窗口" <q-checkbox v-model="fields.alwaysOnTop" label="总在最前" />
@update:model-value="updateConfig('show', $event)" <q-checkbox v-model="fields.fullscreen" label="全屏显示" />
/> <q-checkbox v-model="fields.fullscreenable" label="允许全屏" />
<q-checkbox
:model-value="localConfigs.run.center"
label="居中显示"
@update:model-value="updateConfig('center', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.alwaysOnTop"
label="总在最前"
@update:model-value="updateConfig('alwaysOnTop', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.fullscreen"
label="全屏显示"
@update:model-value="updateConfig('fullscreen', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.fullscreenable"
label="允许全屏"
@update:model-value="updateConfig('fullscreenable', $event)"
/>
</div> </div>
</div> </div>
@ -35,32 +16,16 @@
<div class="col-12"> <div class="col-12">
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-3"> <div class="col-3">
<NumberInput <NumberInput v-model="fields.width" label="窗口宽度" />
v-model="localConfigs.run.width"
label="窗口宽度"
@update:model-value="updateConfig('width', $event)"
/>
</div> </div>
<div class="col-3"> <div class="col-3">
<NumberInput <NumberInput v-model="fields.height" label="窗口高度" />
v-model="localConfigs.run.height"
label="窗口高度"
@update:model-value="updateConfig('height', $event)"
/>
</div> </div>
<div class="col-3"> <div class="col-3">
<NumberInput <NumberInput v-model="fields.x" label="X坐标" />
v-model="localConfigs.run.x"
label="X坐标"
@update:model-value="updateConfig('x', $event)"
/>
</div> </div>
<div class="col-3"> <div class="col-3">
<NumberInput <NumberInput v-model="fields.y" label="Y坐标" />
v-model="localConfigs.run.y"
label="Y坐标"
@update:model-value="updateConfig('y', $event)"
/>
</div> </div>
</div> </div>
</div> </div>
@ -69,32 +34,16 @@
<div class="col-12"> <div class="col-12">
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-3"> <div class="col-3">
<NumberInput <NumberInput v-model="fields.minWidth" label="最小宽度" />
v-model="localConfigs.run.minWidth"
label="最小宽度"
@update:model-value="updateConfig('minWidth', $event)"
/>
</div> </div>
<div class="col-3"> <div class="col-3">
<NumberInput <NumberInput v-model="fields.minHeight" label="最小高度" />
v-model="localConfigs.run.minHeight"
label="最小高度"
@update:model-value="updateConfig('minHeight', $event)"
/>
</div> </div>
<div class="col-3"> <div class="col-3">
<NumberInput <NumberInput v-model="fields.maxWidth" label="最大宽度" />
v-model="localConfigs.run.maxWidth"
label="最大宽度"
@update:model-value="updateConfig('maxWidth', $event)"
/>
</div> </div>
<div class="col-3"> <div class="col-3">
<NumberInput <NumberInput v-model="fields.maxHeight" label="最大高度" />
v-model="localConfigs.run.maxHeight"
label="最大高度"
@update:model-value="updateConfig('maxHeight', $event)"
/>
</div> </div>
</div> </div>
</div> </div>
@ -102,30 +51,13 @@
<!-- 窗口行为控制 --> <!-- 窗口行为控制 -->
<div class="col-12"> <div class="col-12">
<div class="row items-center q-gutter-x-md"> <div class="row items-center q-gutter-x-md">
<q-checkbox v-model="fields.resizable" label="可调整大小" />
<q-checkbox v-model="fields.movable" label="可移动" />
<q-checkbox v-model="fields.minimizable" label="可最小化" />
<q-checkbox v-model="fields.maximizable" label="可最大化" />
<q-checkbox <q-checkbox
:model-value="localConfigs.run.resizable" v-model="fields.enableLargerThanScreen"
label="可调整大小"
@update:model-value="updateConfig('resizable', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.movable"
label="可移动"
@update:model-value="updateConfig('movable', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.minimizable"
label="可最小化"
@update:model-value="updateConfig('minimizable', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.maximizable"
label="可最大化"
@update:model-value="updateConfig('maximizable', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.enableLargerThanScreen"
label="允许超出屏幕" label="允许超出屏幕"
@update:model-value="updateConfig('enableLargerThanScreen', $event)"
/> />
</div> </div>
</div> </div>
@ -135,59 +67,86 @@
<div class="row items-center q-px-sm" style="height: 36px"> <div class="row items-center q-px-sm" style="height: 36px">
<div class="q-mr-md" style="font-size: 12px">透明度</div> <div class="q-mr-md" style="font-size: 12px">透明度</div>
<q-slider <q-slider
v-model="fields.opacity"
class="col" class="col"
v-model="localConfigs.run.opacity"
:min="0" :min="0"
:max="1" :max="1"
:step="0.1" :step="0.1"
label label
color="primary" color="primary"
dense dense
@update:model-value="updateConfig('opacity', $event)"
> >
<template v-slot:thumb-label> <template v-slot:thumb-label>
{{ localConfigs.run.opacity.toFixed(1) }} {{ fields.opacity.toFixed(1) }}
</template> </template>
</q-slider> </q-slider>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
import { defineComponent, ref, computed } from "vue";
import NumberInput from "components/composer/common/NumberInput.vue"; import NumberInput from "components/composer/common/NumberInput.vue";
export default defineComponent({ const defaultValues = {
show: true,
center: false,
alwaysOnTop: false,
fullscreen: false,
fullscreenable: true,
width: 800,
height: 600,
x: undefined,
y: undefined,
minWidth: 0,
minHeight: 0,
maxWidth: undefined,
maxHeight: undefined,
resizable: true,
movable: true,
minimizable: true,
maximizable: true,
enableLargerThanScreen: false,
opacity: 1,
};
export default {
name: "UBrowserRun", name: "UBrowserRun",
components: { components: {
NumberInput, NumberInput,
}, },
props: { props: {
configs: { modelValue: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
emits: ["update:configs"], emits: ["update:model-value"],
setup(props, { emit }) { computed: {
const localConfigs = ref(window.lodashM.cloneDeep(props.configs)); fields() {
return Object.keys(defaultValues).reduce((acc, field) => {
// configs Object.defineProperty(acc, field, {
const watchConfigs = computed(() => props.configs); get: () => this.modelValue[field] ?? defaultValues[field],
watchConfigs.value && set: (value) => this.updateField(field, value),
(localConfigs.value = window.lodashM.cloneDeep(props.configs)); });
return acc;
// }, {});
const updateConfig = (key, value) => {
localConfigs.value.run[key] = value;
emit("update:configs", window.lodashM.cloneDeep(localConfigs.value));
};
return {
localConfigs,
updateConfig,
};
}, },
}); },
methods: {
updateField(field, value) {
this.$emit("update:model-value", {
...this.modelValue,
[field]: value,
});
},
},
};
</script> </script>
<style scoped>
.ubrowser-run {
width: 100%;
}
</style>

View File

@ -1,105 +0,0 @@
<template>
<div>
<div class="row q-col-gutter-sm">
<div
v-for="(cookie, index) in modelValue || [{}]"
:key="index"
class="col-12"
>
<div class="row items-center q-gutter-x-sm">
<div class="col">
<VariableInput
:model-value="cookie.name"
label="名称"
icon="label"
@update:model-value="
(value) => handleUpdate(index, 'name', value)
"
/>
</div>
<div class="col">
<VariableInput
:model-value="cookie.value"
label="值"
icon="edit"
@update:model-value="
(value) => handleUpdate(index, 'value', value)
"
/>
</div>
<div class="col-auto">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeCookie(index)"
/>
</div>
</div>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加Cookie"
@click="addCookie"
class="q-mt-xs"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/composer/common/VariableInput.vue";
import { newVarInputVal } from "js/composer/varInputValManager";
export default defineComponent({
name: "UBrowserCookieList",
components: {
VariableInput,
},
props: {
modelValue: {
type: Array,
default: () => [
{
name: newVarInputVal("str"),
value: newVarInputVal("str"),
},
],
},
},
emits: ["update:modelValue"],
methods: {
addCookie() {
const newValue = [
...this.modelValue,
{
name: newVarInputVal("str"),
value: newVarInputVal("str"),
},
];
this.$emit("update:modelValue", newValue);
},
removeCookie(index) {
const newValue = [...this.modelValue];
newValue.splice(index, 1);
if (newValue.length === 0) {
newValue.push({
name: newVarInputVal("str"),
value: newVarInputVal("str"),
});
}
this.$emit("update:modelValue", newValue);
},
handleUpdate(index, field, value) {
const newValue = [...this.modelValue];
newValue[index] = { ...newValue[index], [field]: value };
this.$emit("update:modelValue", newValue);
},
},
});
</script>

View File

@ -1,72 +0,0 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col">
<VariableInput
icon="label"
:model-value="modelValue"
:label="label"
@update:model-value="$emit('update:modelValue', $event)"
/>
</div>
<div class="col-auto">
<q-select
v-model="selectedDevice"
:options="deviceOptions"
label="常用设备"
dense
filled
emit-value
map-options
options-dense
style="min-width: 150px"
@update:model-value="handleDeviceSelect"
>
<template v-slot:prepend>
<q-icon name="list" />
</template>
</q-select>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { deviceName } from "js/options/httpOptions";
import VariableInput from "components/composer/common/VariableInput.vue";
import { newVarInputVal } from "js/composer/varInputValManager";
export default defineComponent({
name: "UBrowserDeviceName",
components: {
VariableInput,
},
props: {
modelValue: {
type: Object,
default: () => newVarInputVal("str"),
},
label: {
type: String,
required: true,
},
icon: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
data() {
return {
selectedDevice: null,
deviceOptions: deviceName,
};
},
methods: {
handleDeviceSelect(value) {
if (value) {
this.$emit("update:modelValue", value);
this.selectedDevice = null;
}
},
},
});
</script>

View File

@ -1,79 +0,0 @@
<template>
<div>
<div class="row q-col-gutter-sm">
<div
v-for="(file, index) in modelValue || []"
:key="index"
class="col-12"
>
<div class="row q-col-gutter-sm">
<div class="col">
<VariableInput
:model-value="modelValue[index]"
label="文件路径"
:command="{ icon: 'folder' }"
@update:model-value="(value) => handleUpdate(index, value)"
/>
</div>
<div class="col-auto">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeFile(index)"
/>
</div>
</div>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加文件"
@click="addFile"
class="q-mt-xs"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/composer/common/VariableInput.vue";
import { newVarInputVal } from "js/composer/varInputValManager";
export default defineComponent({
name: "UBrowserFileList",
components: {
VariableInput,
},
props: {
modelValue: {
type: Array,
default: () => [],
},
},
emits: ["update:modelValue"],
methods: {
addFile() {
const newValue = [
...(this.modelValue || []),
newVarInputVal("str"),
];
this.$emit("update:modelValue", newValue);
},
removeFile(index) {
const newValue = [...this.modelValue];
newValue.splice(index, 1);
this.$emit("update:modelValue", newValue);
},
handleUpdate(index, value) {
const newValue = [...this.modelValue];
newValue[index] = value;
this.$emit("update:modelValue", newValue);
},
},
});
</script>

View File

@ -1,237 +0,0 @@
<template>
<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">
<q-select
v-model="localParams"
use-input
use-chips
multiple
dense
borderless
hide-dropdown-icon
options-dense
input-debounce="0"
new-value-mode="add-unique"
label="参数"
@update:model-value="updateParams"
@input-value="handleInput"
@blur="handleBlur"
ref="paramSelect"
>
<template v-slot:prepend>
<div class="text-primary func-symbol">(</div>
</template>
<template v-slot:append>
<div class="text-primary func-symbol">)</div>
</template>
</q-select>
</div>
<div class="col-9">
<q-input
v-model="localFunction"
:label="label"
type="textarea"
dense
borderless
style="font-family: Consolas, Monaco, 'Courier New'"
autogrow
@update:model-value="updateFunction"
>
<template v-slot:prepend>
<div class="text-primary func-symbol">=> {</div>
</template>
<template v-slot:append>
<div class="text-primary func-symbol">}</div>
</template>
</q-input>
</div>
</div>
</div>
<template v-if="localParams.length">
<div v-for="param in localParams" :key="param" class="col-12">
<div class="row q-col-gutter-sm items-center">
<div class="col-3">
<q-chip
dense
color="primary"
text-color="white"
removable
@remove="removeParam(param)"
>
{{ param }}
</q-chip>
</div>
<div class="col-9">
<q-input
v-model="paramValues[param]"
:label="`传递给参数 ${param} 的值`"
dense
filled
@update:model-value="updateParamValue(param, $event)"
/>
</div>
</div>
</div>
</template>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserFunctionInput",
props: {
function: {
type: String,
default: "",
},
args: {
type: Array,
default: () => [],
},
label: {
type: String,
default: "函数内容",
},
icon: {
type: String,
default: "code",
},
},
emits: ["update:function", "update:args"],
data() {
return {
localFunction: "",
localParams: [],
paramValues: {},
newParamName: "",
};
},
created() {
//
this.localFunction = this.function;
this.localParams = this.args?.map((arg) => arg.name) || [];
this.paramValues = Object.fromEntries(
this.args?.map((arg) => [arg.name, arg.value]) || []
);
},
methods: {
updateFunction(value) {
this.localFunction = value;
this.emitUpdate();
},
updateParams(value) {
this.localParams = value;
this.emitUpdate();
},
removeParam(param) {
const index = this.localParams.indexOf(param);
if (index > -1) {
this.localParams.splice(index, 1);
delete this.paramValues[param];
this.emitUpdate();
}
},
updateParamValue(param, value) {
this.paramValues[param] = value;
this.emitUpdate();
},
emitUpdate() {
this.$emit("update:function", this.localFunction);
this.$emit(
"update:args",
this.localParams.map((name) => ({
name,
value: this.paramValues[name] || "",
}))
);
},
handleInput(val) {
if (!val) return;
this.newParamName = val;
if (val.includes(",") || val.includes(" ")) {
const params = val
.split(/[,\s]+/)
.map((p) => p.trim())
.filter((p) => p);
params.forEach((param) => {
if (param && !this.localParams.includes(param)) {
this.localParams = [...this.localParams, param];
this.paramValues[param] = "";
}
});
this.newParamName = "";
this.emitUpdate();
this.$refs.paramSelect.updateInputValue("");
}
},
handleBlur() {
if (this.newParamName && !this.localParams.includes(this.newParamName)) {
this.localParams = [...this.localParams, this.newParamName];
this.paramValues[this.newParamName] = "";
this.newParamName = "";
this.emitUpdate();
this.$refs.paramSelect.updateInputValue("");
}
},
},
watch: {
function: {
handler(newValue) {
this.localFunction = newValue;
},
},
args: {
deep: true,
handler(newValue) {
this.localParams = newValue?.map((arg) => arg.name) || [];
this.paramValues = Object.fromEntries(
newValue?.map((arg) => [arg.name, arg.value]) || []
);
},
},
},
});
</script>
<style scoped>
.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

@ -1,87 +0,0 @@
<template>
<div>
<div class="text-caption q-mb-sm">{{ label }}</div>
<div
v-for="(param, index) in modelValue || []"
:key="index"
class="row q-col-gutter-sm q-mb-sm"
>
<div class="col-5">
<VariableInput
:model-value="param.name"
label="参数名"
icon="label"
@update:model-value="(value) => handleUpdate(index, 'name', value)"
/>
</div>
<div class="col-5">
<VariableInput
:model-value="param.value"
label="传递给参数的值"
icon="edit"
@update:model-value="(value) => handleUpdate(index, 'value', value)"
/>
</div>
<div class="col-2">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeParam(index)"
/>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加参数"
@click="addParam"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/composer/common/VariableInput.vue";
import { newVarInputVal } from "js/composer/varInputValManager";
export default defineComponent({
name: "UBrowserNamedParamList",
components: {
VariableInput,
},
props: {
modelValue: {
type: Array,
default: () => [newVarInputVal("str"), newVarInputVal("str")],
},
label: String,
},
emits: ["update:modelValue"],
methods: {
addParam() {
const newValue = [
...(this.modelValue || []),
{
name: newVarInputVal("str"),
value: newVarInputVal("str"),
},
];
this.$emit("update:modelValue", newValue);
},
removeParam(index) {
const newValue = [...this.modelValue];
newValue.splice(index, 1);
this.$emit("update:modelValue", newValue);
},
handleUpdate(index, field, value) {
const newValue = [...this.modelValue];
newValue[index] = { ...newValue[index], [field]: value };
this.$emit("update:modelValue", newValue);
},
},
});
</script>

View File

@ -1,266 +0,0 @@
<template>
<div class="row q-col-gutter-sm items-center">
<template v-for="field in fields" :key="field.key">
<div
v-if="!field.showWhen || fieldValue[field.showWhen] === field.showValue"
:class="['col', field.width ? `col-${field.width}` : 'col-12']"
>
<!-- 复选框组 -->
<template v-if="field.type === 'checkbox-group'">
<q-option-group
:model-value="
Array.isArray(fieldValue[field.key]) ? fieldValue[field.key] : []
"
:options="field.options"
type="checkbox"
class="row items-center"
inline
dense
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- /否选择 -->
<template v-else-if="field.type === 'boolean-toggle'">
<div class="row items-center no-wrap">
<q-badge class="q-pa-xs">{{ field.label }}</q-badge>
<q-btn-toggle
:model-value="fieldValue[field.key] ? 'true' : 'false'"
:options="[
{ label: '是', value: 'true' },
{ label: '否', value: 'false' },
]"
dense
flat
no-caps
spread
class="button-group"
@update:model-value="updateValue(field.key, $event === 'true')"
/>
</div>
</template>
<!-- 基本输入类型的处理 -->
<template v-if="field.type === 'varInput'">
<!-- 设备名称特殊处理 -->
<template v-if="field.key === 'deviceName'">
<UBrowserDeviceName
v-model="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 普通输入框 -->
<template v-else>
<VariableInput
:model-value="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
</template>
<!-- 数字输入框 -->
<template v-else-if="field.type === 'numInput'">
<NumberInput
v-model="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 选择框 -->
<template v-else-if="field.type === 'select'">
<q-select
:model-value="fieldValue[field.key]"
:label="field.label"
:options="field.options"
dense
filled
emit-value
map-options
@update:model-value="updateValue(field.key, $event)"
>
<template v-slot:prepend>
<q-icon :name="field.icon" />
</template>
</q-select>
</template>
<!-- Cookie列表 -->
<template v-else-if="field.type === 'cookie-list'">
<UBrowserCookieList
v-model="fieldValue[field.key]"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 命名参数列表 -->
<template v-else-if="field.type === 'named-param-list'">
<UBrowserNamedParamList
v-model="fieldValue[field.key]"
:label="field.label"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 文件列表 -->
<template v-else-if="field.type === 'file-list'">
<UBrowserFileList
v-model="fieldValue[field.key]"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 按钮组 -->
<template v-else-if="field.type === 'button-toggle'">
<div class="row items-center no-wrap">
<q-badge class="q-pa-xs">{{ field.label }}</q-badge>
<q-btn-toggle
:model-value="fieldValue[field.key]"
:options="field.options"
dense
flat
no-caps
spread
class="button-group"
@update:model-value="updateValue(field.key, $event)"
/>
</div>
</template>
<!-- 设备尺寸 -->
<template v-else-if="field.type === 'device-size'">
<div class="row q-col-gutter-sm">
<VariableInput
v-model.number="fieldValue.size[key]"
class="col-6"
v-for="key in ['width', 'height']"
:key="key"
label="宽度"
icon="width"
@update:model-value="
updateValue(field.key, {
...fieldValue.size,
[key]: $event,
})
"
/>
</div>
</template>
<!-- 带参数的函数输入 -->
<template v-else-if="field.type === 'function-with-params'">
<UBrowserFunctionInput
v-model:function="fieldValue.function"
v-model:args="fieldValue.args"
:label="field.label"
:icon="field.icon"
@update:function="(value) => updateValue('function', value)"
@update:args="(value) => updateValue('args', value)"
/>
</template>
</div>
</template>
</div>
</template>
<script>
import { defineComponent, computed, ref, onMounted } from "vue";
import { get, set } from "lodash";
import UBrowserFunctionInput from "./UBrowserFunctionInput.vue";
import UBrowserFileList from "./UBrowserFileList.vue";
import UBrowserCookieList from "./UBrowserCookieList.vue";
import UBrowserNamedParamList from "./UBrowserNamedParamList.vue";
import UBrowserDeviceName from "./UBrowserDeviceName.vue";
import VariableInput from "components/composer/common/VariableInput.vue";
import NumberInput from "components/composer/common/NumberInput.vue";
export default defineComponent({
name: "UBrowserOperation",
components: {
UBrowserFunctionInput,
UBrowserFileList,
UBrowserCookieList,
UBrowserNamedParamList,
UBrowserDeviceName,
VariableInput,
NumberInput,
},
props: {
configs: {
type: Object,
required: true,
},
action: {
type: String,
required: true,
},
fields: {
type: Array,
required: true,
},
},
emits: ["update:configs"],
setup(props, { emit }) {
const fieldValue = ref({});
//
onMounted(() => {
props.fields.forEach((field) => {
const value = get(props.configs[props.action], field.key);
//
if (field.type === "function-with-params") {
fieldValue.value.function = value?.function || "";
fieldValue.value.args = value?.args || [];
return;
}
const defaultValue =
field.type === "checkbox-group"
? []
: field.type === "checkbox"
? field.defaultValue || false
: field.defaultValue;
fieldValue.value[field.key] = Array.isArray(value)
? value
: defaultValue;
});
});
//
const updateValue = (key, value) => {
fieldValue.value[key] = value;
const newConfigs = { ...props.configs };
if (!newConfigs[props.action]) {
newConfigs[props.action] = {};
}
set(newConfigs[props.action], key, value);
emit("update:configs", newConfigs);
};
return {
fieldValue,
updateValue,
};
},
});
</script>
<style scoped>
.button-group {
flex: 1;
padding: 0 10px;
}
.button-group :deep(.q-btn) {
min-height: 24px;
font-size: 12px;
}
</style>

View File

@ -375,7 +375,8 @@ export const browserCommands = {
placeholder: "输入JavaScript代码使用return返回结果", placeholder: "输入JavaScript代码使用return返回结果",
}, },
{ {
topLabel: "要传递的参数", label: "要传递的参数",
isCollapse: false,
component: "DictEditor", component: "DictEditor",
icon: "data_array", icon: "data_array",
width: 12, width: 12,

View File

@ -1,361 +1,102 @@
/** /**
* 生成 UBrowser 代码 * 生成 UBrowser 代码
* @param {Object} configs UBrowser 配置对象 * @param {Object} argvs UBrowser 配置对象
* @param {Array} selectedActions 已选择的操作列表
* @returns {string} 生成的代码 * @returns {string} 生成的代码
*/ */
import { stringifyVarInputVal } from "./varInputValManager";
import { stringifyArgv } from "./formatString"; import { stringifyArgv } from "./formatString";
// 生成 goto 参数字符串 // ubrowser 默认运行配置
function generateGotoArgs(goto) { const defaultRunConfigs = {
const args = [];
// URL
const urlStr = stringifyArgv(goto.url);
args.push(urlStr);
// Headers
if (goto.headers?.Referer?.value || goto.headers?.userAgent?.value) {
const headers = {};
if (goto.headers.Referer?.value) {
headers.Referer = goto.headers.Referer;
}
if (goto.headers.userAgent?.value) {
headers.userAgent = goto.headers.userAgent;
}
console.log("Headers:", JSON.stringify(headers, null, 2));
args.push(stringifyArgv(headers, true));
}
// Timeout
if (goto.timeout !== 60000) {
args.push(goto.timeout);
}
return args.join(", ");
}
// 生成 run 参数字符串
function generateRunArgs(run) {
const options = {};
const defaultValues = {
show: true, show: true,
width: 800,
height: 600,
center: true, center: true,
alwaysOnTop: false, minWidth: 0,
fullscreen: false, minHeight: 0,
fullscreenable: true,
resizable: true, resizable: true,
movable: true, movable: true,
minimizable: true, minimizable: true,
maximizable: true, maximizable: true,
alwaysOnTop: false,
fullscreen: false,
fullscreenable: true,
enableLargerThanScreen: false, enableLargerThanScreen: false,
opacity: 1, opacity: 1,
}; };
// 窗口显示控制
if (run.show !== undefined && run.show !== defaultValues.show)
options.show = run.show;
if (run.center !== undefined && run.center !== defaultValues.center)
options.center = run.center;
if (
run.alwaysOnTop !== undefined &&
run.alwaysOnTop !== defaultValues.alwaysOnTop
)
options.alwaysOnTop = run.alwaysOnTop;
if (
run.fullscreen !== undefined &&
run.fullscreen !== defaultValues.fullscreen
)
options.fullscreen = run.fullscreen;
if (
run.fullscreenable !== undefined &&
run.fullscreenable !== defaultValues.fullscreenable
)
options.fullscreenable = run.fullscreenable;
// 窗口尺寸和位置 - 只有设置了值才添加
if (run.width !== undefined && run.width > 0) options.width = run.width;
if (run.height !== undefined && run.height > 0) options.height = run.height;
if (run.x !== undefined && run.x !== 0) options.x = run.x;
if (run.y !== undefined && run.y !== 0) options.y = run.y;
// 最大最小尺寸 - 只有设置了值才添加
if (run.minWidth !== undefined && run.minWidth > 0)
options.minWidth = run.minWidth;
if (run.minHeight !== undefined && run.minHeight > 0)
options.minHeight = run.minHeight;
if (run.maxWidth !== undefined && run.maxWidth > 0)
options.maxWidth = run.maxWidth;
if (run.maxHeight !== undefined && run.maxHeight > 0)
options.maxHeight = run.maxHeight;
// 窗口行为控制
if (run.resizable !== undefined && run.resizable !== defaultValues.resizable)
options.resizable = run.resizable;
if (run.movable !== undefined && run.movable !== defaultValues.movable)
options.movable = run.movable;
if (
run.minimizable !== undefined &&
run.minimizable !== defaultValues.minimizable
)
options.minimizable = run.minimizable;
if (
run.maximizable !== undefined &&
run.maximizable !== defaultValues.maximizable
)
options.maximizable = run.maximizable;
if (
run.enableLargerThanScreen !== undefined &&
run.enableLargerThanScreen !== defaultValues.enableLargerThanScreen
)
options.enableLargerThanScreen = run.enableLargerThanScreen;
// 透明度 - 只有不是1时才添加
if (run.opacity !== undefined && run.opacity !== defaultValues.opacity)
options.opacity = run.opacity;
// 其他参数 - 只有设置了值才添加
if (run.headless) options.headless = run.headless;
if (run.devtools) options.devtools = run.devtools;
if (run.timeout && run.timeout !== 60000) options.timeout = run.timeout;
if (run.proxy) options.proxy = run.proxy;
if (run.viewport) options.viewport = run.viewport;
return Object.keys(options).length ? stringifyArgv(options) : "";
}
// 生成操作参数字符串
function generateActionArgs(action, config) {
console.log(
"Generating args for action:",
action,
"config:",
JSON.stringify(config, null, 2)
);
if (!config) return "";
let result;
switch (action) {
case "wait":
result = generateWaitArgs(config);
break;
case "click":
case "mousedown":
case "mouseup":
case "focus":
result = stringifyArgv(config.selector);
break;
case "css":
case "paste":
result = stringifyArgv(config.value);
break;
case "press":
result = generatePressArgs(config);
break;
case "screenshot":
result = generateScreenshotArgs(config);
break;
case "pdf":
result = generatePdfArgs(config);
break;
case "device":
result = generateDeviceArgs(config);
break;
case "cookies":
case "removeCookies":
result = stringifyArgv(config.name);
break;
case "setCookies":
result = generateSetCookiesArgs(config);
break;
case "evaluate":
result = generateEvaluateArgs(config);
break;
case "when":
result = generateWhenArgs(config);
break;
case "file":
result = generateFileArgs(config);
break;
case "value":
result = generateValueArgs(config);
break;
case "check":
result = generateCheckArgs(config);
break;
case "scroll":
result = generateScrollArgs(config);
break;
case "download":
result = generateDownloadArgs(config);
break;
case "devTools":
result = stringifyArgv(config.mode);
break;
default:
result = "";
}
console.log(
"Generated args for action:",
action,
"result:",
JSON.stringify(result)
);
return result;
}
// 生成 wait 参数字符串
function generateWaitArgs(config) {
switch (config.type) {
case "selector":
return stringifyArgv(config.selector);
case "function":
return config.function;
case "time":
return config.time;
default:
return "";
}
}
// 生成 press 参数字符串
function generatePressArgs(config) {
const args = [stringifyArgv(config.key)];
if (config.modifiers?.length) {
args.push(JSON.stringify(config.modifiers));
}
return args.join(", ");
}
// 生成 screenshot 参数字符串
function generateScreenshotArgs(config) {
const args = [];
if (config.rect) {
args.push(stringifyArgv(config.rect));
} else if (config.selector) {
args.push(stringifyArgv(config.selector));
}
if (config.savePath) {
args.push(stringifyArgv(config.savePath));
}
return args.join(", ");
}
// 生成 pdf 参数字符串
function generatePdfArgs(config) {
const args = [];
if (config.savePath) {
args.push(stringifyArgv(config.savePath));
}
if (config.options) {
args.push(stringifyArgv(config.options));
}
return args.join(", ");
}
// 生成 device 参数字符串
function generateDeviceArgs(config) {
if (config.type === "preset") {
return stringifyArgv(config.deviceName);
} else {
const options = {};
if (config.size) options.size = config.size;
if (config.useragent) options.userAgent = config.useragent;
return stringifyArgv(options);
}
}
// 生成 setCookies 参数字符串
function generateSetCookiesArgs(config) {
if (!config.items?.length) return "[]";
return stringifyArgv(config.items);
}
// 生成 evaluate 参数字符串
function generateEvaluateArgs(config) {
const args = [config.function];
if (config.args?.length) {
args.push(...config.args.map(stringifyArgv));
}
return args.join(", ");
}
// 生成 when 参数字符串
function generateWhenArgs(config) {
if (config.type === "function") {
return config.function;
} else {
return stringifyArgv(config.selector);
}
}
// 生成 file 参数字符串
function generateFileArgs(config) {
const args = [stringifyArgv(config.selector)];
if (config.files) {
args.push(stringifyArgv(config.files));
}
return args.join(", ");
}
// 生成 value 参数字符串
function generateValueArgs(config) {
return `${stringifyArgv(config.selector)}, ${stringifyArgv(
config.value
)}`;
}
// 生成 check 参数字符串
function generateCheckArgs(config) {
return `${stringifyArgv(config.selector)}, ${config.checked}`;
}
// 生成 scroll 参数字符串
function generateScrollArgs(config) {
if (config.type === "element") {
return stringifyArgv(config.selector);
} else {
if (config.x !== undefined) {
return `${config.x}, ${config.y}`;
} else {
return String(config.y);
}
}
}
// 生成 download 参数字符串
function generateDownloadArgs(config) {
const args = [stringifyArgv(config.url)];
if (config.savePath) {
args.push(stringifyArgv(config.savePath));
}
return args.join(", ");
}
// 生成完整的 ubrowser 代码 // 生成完整的 ubrowser 代码
export function generateUBrowserCode(configs, selectedActions) { export function generateUBrowserCode(argvs) {
const lines = []; const lines = ["utools.ubrowser"];
const indent = " ";
// 添加 goto 参数 // 首先添加 goto 操作
if (configs.goto) { if (argvs.goto) {
const gotoArgs = generateGotoArgs(configs.goto); const args = [];
lines.push(`${indent}goto(${gotoArgs}),`); // url
if (argvs.goto.url) {
args.push(stringifyVarInputVal(argvs.goto.url));
} }
// 添加选中的操作 // headers
if (selectedActions?.length) { const headers = {};
selectedActions.forEach((action) => { // 处理标准headers
const args = generateActionArgs(action.value, configs[action.value]); Object.entries(argvs.goto.headers || {}).forEach(([key, value]) => {
lines.push(`${indent}${action.value}(${args}),`); if (value?.value) {
headers[key] = value.value;
}
});
// 处理其他headers
Object.entries(argvs.goto.otherHeaders || {}).forEach(([key, value]) => {
if (value?.value) {
headers[key] = value.value;
}
});
if (Object.keys(headers).length > 0) {
args.push(stringifyArgv(headers));
}
// timeout
if (argvs.goto.timeout) {
if (args.length === 1) {
args.push("undefined");
}
args.push(argvs.goto.timeout);
}
lines[0] += `.goto(${args.join(", ")})`;
}
// 添加其他操作
if (argvs.operations?.length) {
argvs.operations.forEach(({ value, args }) => {
if (!args?.length) return;
const stringifiedArgs = args
.map((arg) => stringifyArgv(arg))
.filter(Boolean);
lines.push(` .${value}(${stringifiedArgs.join(", ")})`);
}); });
} }
// 添加 run 参数 // 最后添加 run 配置(只包含非默认值)
const runArgs = generateRunArgs(configs.run || {}); if (argvs.run) {
const runLine = runArgs ? `${indent}run(${runArgs})` : `${indent}run()`; const runOptions = {};
lines.push(runLine); Object.entries(argvs.run).forEach(([key, value]) => {
if (value !== defaultRunConfigs[key]) {
runOptions[key] = value;
}
});
// 生成最终代码 if (Object.keys(runOptions).length > 0) {
return `utools.ubrowser\n${lines.join("\n")}`; lines.push(
` .run(${JSON.stringify(runOptions, null, 2).replace(/\n/g, "\n ")})`
);
} else {
lines.push(" .run()");
}
}
return lines.join("\n");
} }

View File

@ -1,549 +1,591 @@
import { newVarInputVal } from "js/composer/varInputValManager"; import { deviceName, userAgent } from "js/options/httpOptions";
// ubrowser 浏览器操作配置 // ubrowser 浏览器操作配置
export const ubrowserOperationConfigs = [ export const ubrowserOperationConfigs = {
{ waitTime: {
value: "wait", value: "wait",
label: "等待", label: "等待时间",
icon: "timer",
config: [ config: [
{ {
key: "type",
label: "等待类型",
type: "button-toggle",
options: [
{ label: "等待时间", value: "time" },
{ label: "等待元素", value: "selector" },
{ label: "等待条件", value: "function" },
],
defaultValue: "time",
},
{
key: "time",
label: "等待时间(ms)", label: "等待时间(ms)",
icon: "timer", icon: "timer",
component: "NumberInput", component: "NumberInput",
min: 0,
step: 100,
width: 12, width: 12,
showWhen: "type",
showValue: "time",
}, },
],
},
waitElement: {
value: "wait",
label: "等待元素",
icon: "find_in_page",
config: [
{ {
key: "selector",
label: "等待元素的CSS选择器", label: "等待元素的CSS选择器",
icon: "find_in_page", icon: "find_in_page",
component: "VariableInput", component: "VariableInput",
width: 12, width: 12,
showWhen: "type",
showValue: "selector",
}, },
{ ],
key: "function", },
label: "等待条件(返回 true 时结束等待)", waitCondition: {
value: "wait",
label: "等待条件",
icon: "code", icon: "code",
type: "function-with-params", config: [
{
label: "等待条件",
icon: "code",
component: "FunctionInput",
placeholder: "返回true时结束等待",
width: 12, width: 12,
showWhen: "type",
showValue: "function",
}, },
{ {
key: "timeout",
label: "超时时间(ms)", label: "超时时间(ms)",
icon: "timer_off", icon: "timer_off",
component: "NumberInput", component: "NumberInput",
width: 12, width: 12,
defaultValue: 60000, defaultValue: 60000,
showWhen: "type", },
showValue: ["selector", "function"], {
topLabel: "传递给函数的参数值",
component: "ArrayEditor",
}, },
], ],
icon: "timer",
}, },
{ click: {
value: "click", value: "click",
label: "点击", label: "点击",
icon: "mouse",
config: [ config: [
{ {
key: "selector",
label: "点击元素的CSS选择器", label: "点击元素的CSS选择器",
icon: "mouse", icon: "mouse",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
], ],
icon: "mouse",
}, },
{ css: {
value: "css", value: "css",
label: "注入CSS", label: "注入CSS",
icon: "style",
config: [ config: [
{ {
key: "value",
label: "注入的CSS样式", label: "注入的CSS样式",
icon: "style", icon: "style",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
], ],
icon: "style",
}, },
{ press: {
value: "press", value: "press",
label: "按键", label: "按键",
icon: "keyboard",
config: [ config: [
{ {
key: "key",
label: "按键", label: "按键",
icon: "keyboard", icon: "keyboard",
component: "VariableInput", component: "VariableInput",
width: 5, width: 4,
}, },
{ {
key: "modifiers", component: "CheckGroup",
label: "修饰键",
type: "checkbox-group",
options: [ options: [
{ label: "Ctrl", value: "ctrl" }, { label: "Ctrl", value: "ctrl" },
{ label: "Shift", value: "shift" }, { label: "Shift", value: "shift" },
{ label: "Alt", value: "alt" }, { label: "Alt", value: "alt" },
{ label: "Meta", value: "meta" }, { label: "Meta", value: "meta" },
], ],
defaultValue: [], width: 8,
width: 7,
}, },
], ],
icon: "keyboard",
}, },
{ paste: {
value: "paste", value: "paste",
label: "粘贴", label: "粘贴",
icon: "content_paste",
config: [ config: [
{ {
key: "text",
label: "粘贴内容", label: "粘贴内容",
icon: "content_paste", icon: "content_paste",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
], ],
icon: "content_paste",
}, },
{ viewport: {
value: "viewport", value: "viewport",
label: "视窗", label: "视窗",
icon: "crop",
config: [ config: [
{ {
key: "width",
label: "视窗宽度", label: "视窗宽度",
icon: "width", icon: "swap_horiz",
component: "NumberInput", component: "NumberInput",
min: 0,
step: 100,
width: 6, width: 6,
}, },
{ {
key: "height",
label: "视窗高度", label: "视窗高度",
icon: "height", icon: "height",
component: "NumberInput", component: "NumberInput",
min: 0,
step: 100,
width: 6, width: 6,
}, },
], ],
icon: "crop",
}, },
{ screenshotElement: {
value: "screenshot", value: "screenshot",
label: "截图", label: "元素截图",
icon: "picture_as_pdf",
config: [ config: [
{ {
key: "selector",
label: "元素选择器", label: "元素选择器",
icon: "crop", icon: "crop",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
{ {
key: "rect.x", label: "保存路径",
icon: "save",
component: "VariableInput",
options: {
dialog: {
type: "save",
options: {
defaultPath: "screenshot.png",
},
},
},
width: 12,
},
],
},
screenshotPosition: {
value: "screenshot",
label: "区域截图",
icon: "crop",
config: [
{
component: "OptionEditor",
options: {
x: {
label: "X坐标", label: "X坐标",
icon: "drag_handle", icon: "drag_handle",
component: "NumberInput", component: "NumberInput",
min: 0,
step: 100,
width: 3, width: 3,
}, },
{ y: {
key: "rect.y",
label: "Y坐标", label: "Y坐标",
icon: "drag_handle", icon: "drag_handle",
component: "NumberInput", component: "NumberInput",
min: 0,
step: 100,
width: 3, width: 3,
}, },
{ width: {
key: "rect.width",
label: "宽度", label: "宽度",
icon: "width", icon: "swap_horiz",
component: "NumberInput", component: "NumberInput",
min: 0,
step: 100,
width: 3, width: 3,
}, },
{ height: {
key: "rect.height",
label: "高度", label: "高度",
icon: "height", icon: "height",
component: "NumberInput", component: "NumberInput",
min: 0,
step: 100,
width: 3, width: 3,
}, },
},
},
{ {
key: "savePath",
label: "保存路径", label: "保存路径",
icon: "save", icon: "save",
component: "VariableInput", options: {
dialog: {
type: "save",
options: {
defaultPath: "screenshot.png",
},
},
},
width: 12,
}, },
], ],
icon: "picture_as_pdf",
}, },
{ pdf: {
value: "pdf", value: "pdf",
label: "导出PDF", label: "导出PDF",
icon: "picture_as_pdf",
config: [ config: [
{ {
key: "options.marginsType", component: "OptionEditor",
label: "边距类型", options: {
component: "q-select", format: {
label: "格式",
component: "QSelect",
options: [ options: [
{ label: "默认边距", value: 0 }, "A0",
{ label: "无边距", value: 1 }, "A1",
{ label: "最小边距", value: 2 }, "A2",
"A3",
"A4",
"A5",
"A6",
"Legal",
"Letter",
"Tabloid",
"Ledger",
], ],
width: 6, width: 3,
},
landscape: {
label: "横向打印",
component: "CheckButton",
width: 3,
},
pageRanges: {
label: "页码范围",
component: "VariableInput",
placeholder: "1-5, 8",
width: 3,
},
scale: {
label: "缩放",
component: "NumberInput",
min: 0,
step: 0.1,
width: 3,
},
},
defaultValue: {
format: "A4",
landscape: false,
pageRanges: "",
scale: 1,
},
}, },
{ {
key: "options.pageSize",
label: "页面大小",
component: "q-select",
options: ["A3", "A4", "A5", "Legal", "Letter", "Tabloid"],
width: 6,
},
{
key: "savePath",
label: "保存路径", label: "保存路径",
icon: "save", icon: "save",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
], ],
icon: "devices",
}, },
{ device: {
value: "device", value: "device",
label: "模拟设备", label: "模拟设备",
icon: "phone_iphone",
config: [ config: [
{ {
key: "type", component: "OptionEditor",
label: "设备类型", options: {
type: "button-toggle", size: {
options: [ component: "DictEditor",
{ label: "特定设备", value: "preset" }, options: {
{ label: "自定义设备", value: "custom" }, fixedKeys: ["width", "height"],
], disableAdd: true,
defaultValue: "preset",
}, },
{
key: "deviceName",
label: "设备名称",
icon: "smartphone",
component: "VariableInput",
width: 12,
showWhen: "type",
showValue: "preset",
}, },
{ userAgent: {
key: "size",
label: "设备尺寸",
type: "device-size",
width: 12,
showWhen: "type",
showValue: "custom",
},
{
key: "useragent",
label: "User-Agent", label: "User-Agent",
icon: "devices",
component: "VariableInput", component: "VariableInput",
width: 12, options: {
showWhen: "type", items: userAgent,
showValue: "custom", },
},
},
}, },
], ],
icon: "phone_iphone",
}, },
{ cookies: {
value: "cookies", value: "cookies",
label: "获取Cookie", label: "获取Cookie",
icon: "cookie",
config: [ config: [
{ {
key: "name",
label: "Cookie名称", label: "Cookie名称",
icon: "cookie", icon: "cookie",
component: "VariableInput", component: "VariableInput",
width: 12, width: 12,
}, },
], ],
icon: "cookie",
}, },
{ setCookies: {
value: "setCookies", value: "setCookies",
label: "设置Cookie", label: "设置Cookie",
config: [{ key: "items", label: "Cookie列表", type: "cookie-list" }],
icon: "cookie", icon: "cookie",
},
{
value: "removeCookies",
label: "删除Cookie",
config: [ config: [
{ {
key: "name", label: "Cookie列表",
component: "ArrayEditor",
columns: {
name: {
label: "名称",
},
value: {
label: "值",
},
},
},
],
},
removeCookies: {
value: "removeCookies",
label: "删除Cookie",
icon: "cookie",
config: [
{
label: "Cookie名称", label: "Cookie名称",
icon: "cookie", icon: "cookie",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
], ],
icon: "cookie",
}, },
{ clearCookies: {
value: "clearCookies", value: "clearCookies",
label: "清空Cookie", label: "清空Cookie",
icon: "cookie",
config: [ config: [
{ {
key: "url",
label: "URL(可选)", label: "URL(可选)",
icon: "link", icon: "link",
component: "VariableInput", component: "VariableInput",
},
],
icon: "cookie",
},
{
value: "evaluate",
label: "执行代码",
config: [
{
key: "function",
label: "执行的代码",
icon: "code",
type: "function-with-params",
width: 12, width: 12,
}, },
], ],
icon: "code",
}, },
{ evaluate: {
value: "when", value: "evaluate",
label: "条件判断", label: "执行代码",
icon: "code",
config: [ config: [
{ {
key: "type", label: "执行的代码",
label: "条件类型", icon: "code",
type: "button-toggle", component: "FunctionInput",
options: [ width: 12,
{ label: "等待元素", value: "selector" },
{ label: "等待条件", value: "function" },
],
defaultValue: "selector",
}, },
{ {
key: "selector", topLabel: "传递给函数的参数值",
label: "等待元素的CSS选择器", component: "ArrayEditor",
},
],
},
whenElement: {
value: "when",
label: "判断元素",
icon: "rule",
config: [
{
label: "判断元素的CSS选择器",
icon: "find_in_page", icon: "find_in_page",
component: "VariableInput", component: "VariableInput",
width: 12, width: 12,
showWhen: "type",
showValue: "selector",
},
{
key: "function",
label: "等待条件(返回 true 时结束等待)",
icon: "code",
type: "function-with-params",
width: 12,
showWhen: "type",
showValue: "function",
},
{
key: "timeout",
label: "超时时间(ms)",
icon: "timer_off",
component: "NumberInput",
width: 12,
defaultValue: 60000,
showWhen: "type",
showValue: ["selector", "function"],
}, },
], ],
},
whenCondition: {
value: "when",
label: "判断条件",
icon: "rule", icon: "rule",
},
{
value: "end",
label: "结束条件",
config: [],
icon: "stop",
},
{
value: "mousedown",
label: "按下鼠标",
config: [ config: [
{ {
key: "selector", label: "判断条件",
icon: "code",
component: "FunctionInput",
width: 12,
placeholder: "返回true时结束判断",
},
{
topLabel: "传递给函数的参数值",
component: "ArrayEditor",
},
],
},
end: {
value: "end",
label: "结束判断",
icon: "stop",
config: [],
},
mousedown: {
value: "mousedown",
label: "按下鼠标",
icon: "mouse",
config: [
{
label: "按下元素选择器", label: "按下元素选择器",
icon: "mouse", icon: "mouse",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
], ],
icon: "mouse",
}, },
{ mouseup: {
value: "mouseup", value: "mouseup",
label: "释放鼠标", label: "释放鼠标",
icon: "mouse",
config: [ config: [
{ {
key: "selector",
label: "释放元素选择器", label: "释放元素选择器",
icon: "mouse", icon: "mouse",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
], ],
icon: "mouse",
}, },
{ file: {
value: "file", value: "file",
label: "上传文件", label: "上传文件",
icon: "upload_file",
config: [ config: [
{ {
key: "selector",
label: "文件输入框选择器", label: "文件输入框选择器",
icon: "upload_file", icon: "upload_file",
component: "VariableInput", component: "VariableInput",
}, placeholder: "必须是可选择文件的输入元素 input[type=file]",
{ key: "files", label: "文件列表", type: "file-list", width: 12 }, width: 12,
],
icon: "upload_file",
}, },
{ {
value: "value", label: "文件列表",
component: "VariableInput",
icon: "image",
width: 12,
options: {
dialog: {
type: "open",
options: {
title: "选择文件",
properties: ["openFile", "multiSelections"],
},
},
},
},
],
},
setValue: {
value: "setValue",
label: "设置值", label: "设置值",
icon: "check_box",
config: [ config: [
{ {
key: "selector",
label: "元素选择器", label: "元素选择器",
icon: "varInput", icon: "varInput",
component: "VariableInput", component: "VariableInput",
width: 6, width: 6,
}, },
{ {
key: "value",
label: "设置的值", label: "设置的值",
icon: "edit", icon: "edit",
component: "VariableInput", component: "VariableInput",
width: 6, width: 6,
}, },
], ],
icon: "check_box",
}, },
{ check: {
value: "check", value: "check",
label: "设置选中", label: "设置选中",
icon: "center_focus_strong",
config: [ config: [
{ {
key: "selector",
label: "复选框/选框选择器", label: "复选框/选框选择器",
icon: "check_box", icon: "check_box",
component: "VariableInput", component: "VariableInput",
width: 8, width: 8,
}, },
{ {
key: "checked",
label: "选中状态", label: "选中状态",
type: "boolean-toggle", component: "CheckButton",
defaultValue: false, defaultValue: false,
width: 4, width: 4,
}, },
], ],
icon: "center_focus_strong",
}, },
{ focus: {
value: "focus", value: "focus",
label: "聚焦元素", label: "聚焦元素",
icon: "swap_vert",
config: [ config: [
{ {
key: "selector",
label: "元素选择器", label: "元素选择器",
icon: "center_focus_strong", icon: "center_focus_strong",
component: "VariableInput", component: "VariableInput",
width: 12,
}, },
], ],
icon: "swap_vert",
}, },
{ scrollToElement: {
value: "scroll", value: "scroll",
label: "滚动", label: "滚动到元素",
icon: "download",
config: [ config: [
{ {
key: "type",
label: "滚动类型",
type: "button-toggle",
options: [
{ label: "滚动到元素", value: "element" },
{ label: "滚动到坐标", value: "position" },
],
defaultValue: "element",
},
{
key: "selector",
label: "目标元素选择器", label: "目标元素选择器",
icon: "swap_vert", icon: "swap_vert",
component: "VariableInput", component: "VariableInput",
width: 12, width: 12,
showWhen: "type",
showValue: "element",
}, },
],
},
scrollToPosition: {
value: "scroll",
label: "滚动到坐标",
icon: "download",
config: [
{ {
key: "x",
label: "X坐标", label: "X坐标",
icon: "drag_handle", icon: "drag_handle",
component: "NumberInput", component: "NumberInput",
width: 6, width: 6,
showWhen: "type",
showValue: "position",
}, },
{ {
key: "y",
label: "Y坐标", label: "Y坐标",
icon: "drag_handle", icon: "drag_handle",
component: "NumberInput", component: "NumberInput",
width: 6, width: 6,
showWhen: "type",
showValue: "position",
}, },
], ],
icon: "download",
}, },
{ download: {
value: "download", value: "download",
label: "下载", label: "下载",
icon: "download",
config: [ config: [
{ {
key: "url",
label: "下载URL", label: "下载URL",
icon: "link", icon: "link",
component: "VariableInput", component: "VariableInput",
width: 6, width: 6,
}, },
{ {
key: "savePath",
label: "保存路径", label: "保存路径",
icon: "save", icon: "save",
component: "VariableInput", component: "VariableInput",
width: 6, width: 6,
}, },
], ],
icon: "download",
}, },
{ devTools: {
value: "devTools", value: "devTools",
label: "开发工具", label: "开发工具",
icon: "developer_board",
config: [ config: [
{ {
key: "mode", component: "ButtonGroup",
label: "开发工具位置",
type: "button-toggle",
options: [ options: [
{ label: "右侧", value: "right" }, { label: "右侧", value: "right" },
{ label: "底部", value: "bottom" }, { label: "底部", value: "bottom" },
@ -551,142 +593,20 @@ export const ubrowserOperationConfigs = [
{ label: "分离", value: "detach" }, { label: "分离", value: "detach" },
], ],
defaultValue: "right", defaultValue: "right",
width: 12,
}, },
], ],
icon: "developer_board",
}, },
{ hide: {
value: "hide", value: "hide",
label: "隐藏", label: "隐藏",
config: [],
icon: "visibility_off", icon: "visibility_off",
config: [],
}, },
{ show: {
value: "show", value: "show",
label: "显示", label: "显示",
config: [],
icon: "visibility", icon: "visibility",
config: [],
}, },
];
// 添加默认运行配置
const defaultUBrowserRunConfigs = {
show: true,
width: 1280,
height: 800,
center: true,
minWidth: 800,
minHeight: 600,
resizable: true,
movable: true,
minimizable: true,
maximizable: true,
alwaysOnTop: false,
fullscreen: false,
fullscreenable: true,
enableLargerThanScreen: false,
opacity: 1,
};
// ubrowser 默认配置 基础参数-浏览器操作-运行参数
export const defaultUBrowserConfigs = {
// 基础参数
goto: {
url: newVarInputVal("str"),
headers: {
Referer: newVarInputVal("str"),
userAgent: newVarInputVal("str"),
},
timeout: 60000,
},
// 浏览器操作
wait: {
value: "",
timeout: 60000,
},
click: {
selector: newVarInputVal("str"),
},
css: {
value: newVarInputVal("str"),
},
press: {
key: newVarInputVal("str"),
modifiers: [],
},
paste: {
text: newVarInputVal("str"),
},
screenshot: {
selector: newVarInputVal("str"),
rect: { x: 0, y: 0, width: 0, height: 0 },
savePath: newVarInputVal("str"),
},
pdf: {
options: {
marginsType: 0,
pageSize: "A4",
},
savePath: newVarInputVal("str"),
},
device: {
size: { width: 1280, height: 800 },
useragent: newVarInputVal("str"),
},
cookies: {
name: newVarInputVal("str"),
},
setCookies: {
items: [
{
name: newVarInputVal("str"),
value: newVarInputVal("str"),
},
],
},
removeCookies: {
name: newVarInputVal("str"),
},
clearCookies: {
url: newVarInputVal("str"),
},
evaluate: {
function: "",
params: [],
},
when: {
condition: newVarInputVal("var"),
},
mousedown: {
selector: newVarInputVal("str"),
},
mouseup: {
selector: newVarInputVal("str"),
},
file: {
selector: newVarInputVal("str"),
files: [],
},
value: {
selector: newVarInputVal("str"),
value: newVarInputVal("str"),
},
check: {
selector: newVarInputVal("str"),
checked: false,
},
focus: {
selector: newVarInputVal("str"),
},
scroll: {
target: newVarInputVal("str"),
x: 0,
y: 0,
},
download: {
url: newVarInputVal("str"),
savePath: newVarInputVal("str"),
},
// 运行参数
run: defaultUBrowserRunConfigs,
}; };