完善ubrowser各项参数及UI

This commit is contained in:
fofolee 2024-12-23 21:01:05 +08:00
parent c8a9a63dcb
commit eeda8e66d9
24 changed files with 2002 additions and 1148 deletions

View File

@ -195,6 +195,11 @@ export default {
},
insertText(text) {
this.$refs.editor.repacleEditorSelection(text);
this.$refs.editor.formatDocument();
},
replaceText(text) {
this.$refs.editor.setEditorValue(text);
this.$refs.editor.formatDocument();
},
handleComposer({ type, code }) {
switch (type) {
@ -203,7 +208,7 @@ export default {
case "insert":
return this.insertText(code);
case "apply":
return this.$refs.editor.setEditorValue(code);
return this.replaceText(code);
}
},
//

View File

@ -221,10 +221,10 @@ export default {
"program-changed",
"run",
"save",
"show-recorder",
"show-actions",
// "show-recorder",
// "show-actions",
"show-help",
"add-action",
"use-composer",
],
computed: {
programLanguages() {

View File

@ -66,6 +66,16 @@ export default {
minimap: {
enabled: false,
},
formatOnType: true,
formatOnPaste: true,
autoIndent: "full",
// JavaScript
"javascript.format.insertSpaceAfterSemicolonInForStatements": true,
"javascript.format.insertSpaceBeforeAndAfterBinaryOperators": true,
"javascript.format.insertSpaceAfterConstructor": true,
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": true,
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": true,
"javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": true,
};
this.editor = monaco.editor.create(
document.getElementById("monacoEditor"),
@ -212,6 +222,9 @@ export default {
};
this.rawEditor().executeEdits("my-source", [op]);
},
formatDocument() {
this.rawEditor().getAction("editor.action.formatDocument").run();
},
listenEditorValue() {
this.rawEditor().focus();
this.rawEditor().onDidChangeModelContent(() => {

View File

@ -61,6 +61,7 @@ export default defineComponent({
availableCommands,
};
},
emits: ["use-composer", "update:modelValue"],
methods: {
addCommand(action) {
this.commandFlow.push({
@ -84,8 +85,9 @@ export default defineComponent({
outputVars.set(index, varName);
line += `let ${varName} = `;
}
if (cmd.useOutput !== null) {
if (cmd.value === "ubrowser") {
line += cmd.argv;
} else if (cmd.useOutput !== null) {
const inputVar = outputVars.get(cmd.useOutput);
line += `${cmd.value}(${inputVar})`;
} else {

View File

@ -1,3 +1,9 @@
export {
ubrowserActionIcons,
ubrowserOperationConfigs,
defaultUBrowserConfigs,
} from "./ubrowser/ubrowserConfig";
// 定义命令图标映射
export const commandIcons = {
open: "folder_open",
@ -137,58 +143,3 @@ export const commandsAcceptOutput = {
send: true,
copyTo: true,
};
// 添加 ubrowser 操作图标映射
export const ubrowserActionIcons = {
wait: "timer",
click: "mouse",
css: "style",
press: "keyboard",
paste: "content_paste",
screenshot: "photo_camera",
pdf: "picture_as_pdf",
device: "devices",
cookies: "cookie",
evaluate: "code",
when: "rule",
mousedown: "mouse",
mouseup: "mouse",
file: "upload_file",
value: "edit",
check: "check_box",
focus: "center_focus_strong",
scroll: "swap_vert",
download: "download",
hide: "visibility_off",
show: "visibility",
devTools: "developer_board",
};
// 添加 ubrowser 可用操作列表
export const ubrowserAvailableActions = [
{ label: "等待", value: "wait" },
{ label: "点击", value: "click" },
{ label: "注入CSS", value: "css" },
{ label: "按键", value: "press" },
{ label: "粘贴", value: "paste" },
{ label: "截图", value: "screenshot" },
{ label: "导出PDF", value: "pdf" },
{ label: "模拟设备", value: "device" },
{ label: "获取Cookie", value: "cookies" },
{ label: "设置Cookie", value: "setCookies" },
{ label: "删除Cookie", value: "removeCookies" },
{ label: "清除Cookie", value: "clearCookies" },
{ label: "执行脚本", value: "evaluate" },
{ label: "条件判断", value: "when" },
{ label: "鼠标按下", value: "mousedown" },
{ label: "鼠标释放", value: "mouseup" },
{ label: "上传文件", value: "file" },
{ label: "设置值", value: "value" },
{ label: "选中状态", value: "check" },
{ label: "获取焦点", value: "focus" },
{ label: "滚动", value: "scroll" },
{ label: "下载", value: "download" },
{ label: "隐藏", value: "hide" },
{ label: "显示", value: "show" },
{ label: "开发工具", value: "devTools" },
];

View File

@ -15,22 +15,6 @@
</q-input>
</div>
<!-- 超时配置 -->
<div class="col-12">
<q-input
v-model.number="localConfigs.goto.timeout"
type="number"
label="超时时间(ms)"
dense
outlined
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="timer" />
</template>
</q-input>
</div>
<!-- Headers配置 -->
<div class="col-12">
<div class="row q-col-gutter-sm">
@ -48,20 +32,57 @@
</q-input>
</div>
<div class="col-12">
<q-input
v-model="localConfigs.goto.headers.userAgent"
label="User-Agent"
dense
outlined
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="computer" />
</template>
</q-input>
<div class="row q-col-gutter-sm">
<div class="col">
<q-input
v-model="localConfigs.goto.headers.userAgent"
label="User-Agent"
dense
outlined
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="devices" />
</template>
</q-input>
</div>
<div class="col-auto">
<q-select
v-model="selectedUA"
:options="userAgentOptions"
label="常用 UA"
dense
outlined
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">
<q-input
v-model.number="localConfigs.goto.timeout"
type="number"
label="超时时间(ms)"
dense
outlined
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="timer" />
</template>
</q-input>
</div>
</div>
</template>
@ -76,10 +97,14 @@ export default defineComponent({
required: true,
},
},
emits: ["update:configs"],
data() {
return {
selectedUA: null,
localConfigs: {
useragent: {
preset: null,
value: "",
},
goto: {
url: "",
headers: {
@ -89,27 +114,83 @@ export default defineComponent({
timeout: 60000,
},
},
userAgentOptions: [
{
label: "Chrome (Windows)",
value:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
},
{
label: "Chrome (macOS)",
value:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
},
{
label: "Chrome (Linux)",
value:
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
},
{
label: "IE 11",
value:
"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko",
},
{
label: "微信 (Android)",
value:
"Mozilla/5.0 (Linux; Android 14; Pixel 8 Build/UQ1A.240205.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/122.0.6261.64 Mobile Safari/537.36 XWEB/1160027 MMWEBSDK/20231202 MMWEBID/2308 MicroMessenger/8.0.47.2560(0x28002F35) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64",
},
{
label: "微信 (iOS)",
value:
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.47(0x18002f2c) NetType/WIFI Language/zh_CN",
},
{
label: "iPhone",
value:
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
},
{
label: "iPad",
value:
"Mozilla/5.0 (iPad; CPU OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
},
{
label: "Android Phone",
value:
"Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
},
{
label: "Android Tablet",
value:
"Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
},
],
};
},
created() {
//
this.localConfigs = JSON.parse(JSON.stringify(this.configs));
this.localConfigs = _.cloneDeep(this.configs);
},
methods: {
updateConfigs() {
this.$emit(
"update:configs",
JSON.parse(JSON.stringify(this.localConfigs))
);
this.$emit("update:configs", _.cloneDeep(this.localConfigs));
},
},
watch: {
configs: {
deep: true,
handler(newConfigs) {
this.localConfigs = JSON.parse(JSON.stringify(newConfigs));
this.localConfigs = _.cloneDeep(newConfigs);
},
},
selectedUA(value) {
if (value) {
this.localConfigs.goto.headers.userAgent = value;
this.updateConfigs();
this.selectedUA = null;
}
},
},
});
</script>

View File

@ -66,6 +66,8 @@ import { defineComponent } from "vue";
import UBrowserBasic from "./UBrowserBasic.vue";
import UBrowserOperations from "./UBrowserOperations.vue";
import UBrowserRun from "./UBrowserRun.vue";
import { defaultUBrowserConfigs } from "./ubrowserConfig";
import { generateUBrowserCode } from "./generateUBrowserCode";
export default defineComponent({
name: "UBrowserEditor",
@ -85,142 +87,7 @@ export default defineComponent({
return {
step: 1,
selectedActions: [],
configs: {
//
useragent: {
value: "",
},
goto: {
url: "",
headers: {
Referer: "",
userAgent: "",
},
timeout: 60000,
},
//
wait: {
value: "",
timeout: 60000,
},
click: {
selector: "",
},
css: {
value: "",
},
press: {
key: "",
modifiers: [],
},
paste: {
text: "",
},
screenshot: {
selector: "",
rect: { x: 0, y: 0, width: 0, height: 0 },
savePath: "",
},
pdf: {
options: {
marginsType: 0,
pageSize: "A4",
},
savePath: "",
},
device: {
size: { width: 1280, height: 800 },
useragent: "",
},
cookies: {
name: "",
},
setCookies: {
items: [{ name: "", value: "" }],
},
removeCookies: {
name: "",
},
clearCookies: {
url: "",
},
evaluate: {
function: "",
params: [],
},
when: {
condition: "",
},
mousedown: {
selector: "",
},
mouseup: {
selector: "",
},
file: {
selector: "",
files: [],
},
value: {
selector: "",
value: "",
},
check: {
selector: "",
checked: false,
},
focus: {
selector: "",
},
scroll: {
target: "",
x: 0,
y: 0,
},
download: {
url: "",
savePath: "",
},
//
run: {
show: true,
width: 1280,
height: 800,
x: undefined,
y: undefined,
center: true,
minWidth: 800,
minHeight: 600,
maxWidth: undefined,
maxHeight: undefined,
resizable: true,
movable: true,
minimizable: true,
maximizable: true,
alwaysOnTop: false,
fullscreen: false,
fullscreenable: true,
enableLargerThanScreen: false,
opacity: 1,
},
},
defaultRunConfigs: {
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,
},
configs: _.cloneDeep(defaultUBrowserConfigs),
};
},
methods: {
@ -235,249 +102,31 @@ export default defineComponent({
this.selectedActions.splice(index, 1);
}
},
generateCode() {
let code = "utools.ubrowser";
//
if (this.configs.useragent.value) {
code += `.useragent('${this.configs.useragent.value}')`;
}
if (this.configs.goto.url) {
const gotoOptions = {};
if (this.configs.goto.headers.Referer) {
gotoOptions.headers = gotoOptions.headers || {};
gotoOptions.headers.Referer = this.configs.goto.headers.Referer;
}
if (this.configs.goto.headers.userAgent) {
gotoOptions.headers = gotoOptions.headers || {};
gotoOptions.headers["User-Agent"] =
this.configs.goto.headers.userAgent;
}
if (this.configs.goto.timeout !== 60000) {
gotoOptions.timeout = this.configs.goto.timeout;
}
code += `.goto('${this.configs.goto.url}'${
Object.keys(gotoOptions).length
? `, ${JSON.stringify(gotoOptions)}`
: ""
})`;
}
//
this.selectedActions.forEach((action) => {
const config = this.configs[action.value];
switch (action.value) {
case "wait":
if (config.value) {
code += `.wait('${config.value}'${
config.timeout !== 60000 ? `, ${config.timeout}` : ""
})`;
}
break;
case "click":
if (config.selector) {
code += `.click('${config.selector}')`;
}
break;
case "css":
if (config.value) {
code += `.css('${config.value}')`;
}
break;
case "press":
if (config.key) {
const modifiers = config.modifiers.length
? `, ${JSON.stringify(config.modifiers)}`
: "";
code += `.press('${config.key}'${modifiers})`;
}
break;
case "paste":
if (config.text) {
code += `.paste('${config.text}')`;
}
break;
case "screenshot":
if (config.selector || config.savePath) {
const options = {};
if (config.selector) options.selector = config.selector;
if (config.rect.width && config.rect.height) {
options.rect = config.rect;
}
code += `.screenshot('${config.savePath}'${
Object.keys(options).length
? `, ${JSON.stringify(options)}`
: ""
})`;
}
break;
case "pdf":
if (config.savePath) {
code += `.pdf('${config.savePath}'${
config.options ? `, ${JSON.stringify(config.options)}` : ""
})`;
}
break;
case "device":
if (config.size.width && config.size.height) {
const options = {
size: config.size,
};
if (config.useragent) options.useragent = config.useragent;
code += `.device(${JSON.stringify(options)})`;
}
break;
case "cookies":
if (config.name) {
code += `.cookies('${config.name}')`;
}
break;
case "setCookies":
if (config.items?.length) {
code += `.setCookies(${JSON.stringify(config.items)})`;
}
break;
case "removeCookies":
if (config.name) {
code += `.removeCookies('${config.name}')`;
}
break;
case "clearCookies":
code += `.clearCookies(${config.url ? `'${config.url}'` : ""})`;
break;
case "evaluate":
if (config.function) {
const params = config.params.length
? `, ${JSON.stringify(config.params)}`
: "";
code += `.evaluate(\`${config.function}\`${params})`;
}
break;
case "when":
if (config.condition) {
code += `.when('${config.condition}')`;
}
break;
case "mousedown":
case "mouseup":
if (config.selector) {
code += `.${action.value}('${config.selector}')`;
}
break;
case "file":
if (config.selector && config.files?.length) {
code += `.file('${config.selector}', ${JSON.stringify(
config.files
)})`;
}
break;
case "value":
if (config.selector) {
code += `.value('${config.selector}', '${config.value}')`;
}
break;
case "check":
if (config.selector) {
code += `.check('${config.selector}'${
config.checked !== undefined ? `, ${config.checked}` : ""
})`;
}
break;
case "focus":
if (config.selector) {
code += `.focus('${config.selector}')`;
}
break;
case "scroll":
if (config.type === "element" && config.selector) {
code += `.scroll('${config.selector}')`;
} else if (config.type === "position") {
if (config.x !== undefined && config.y !== undefined) {
code += `.scroll(${config.x}, ${config.y})`;
} else if (config.y !== undefined) {
code += `.scroll(${config.y})`;
}
}
break;
case "download":
if (config.url) {
code += `.download('${config.url}'${
config.savePath ? `, '${config.savePath}'` : ""
})`;
}
break;
case "hide":
case "show":
code += `.${action.value}()`;
break;
case "devTools":
if (config.mode) {
code += `.devTools('${config.mode}')`;
} else {
code += `.devTools()`;
}
break;
}
});
//
const runOptions = {};
Object.entries(this.configs.run).forEach(([key, value]) => {
if (
value !== undefined &&
value !== null &&
value !== this.defaultRunConfigs[key]
) {
runOptions[key] = value;
}
});
code += `.run(${
Object.keys(runOptions).length ? JSON.stringify(runOptions) : ""
})`;
this.$emit("update:modelValue", code);
},
},
watch: {
configs: {
deep: true,
handler() {
this.generateCode();
this.$emit(
"update:modelValue",
generateUBrowserCode(this.configs, this.selectedActions)
);
},
},
selectedActions: {
handler() {
this.generateCode();
this.$emit(
"update:modelValue",
generateUBrowserCode(this.configs, this.selectedActions)
);
},
},
step: {
handler() {
this.generateCode();
this.$emit(
"update:modelValue",
generateUBrowserCode(this.configs, this.selectedActions)
);
},
},
},

View File

@ -4,8 +4,10 @@
<!-- 操作选择网格 -->
<div class="row q-col-gutter-xs">
<div
v-for="action in availableActions"
:key="action.value"
v-for="[actionName, { label }] in Object.entries(
ubrowserOperationConfigs
)"
:key="actionName"
class="col-2"
>
<q-card
@ -14,13 +16,13 @@
class="action-card cursor-pointer"
:class="{
'action-selected': selectedActions.some(
(a) => a.value === action.value
(a) => a.value === actionName
),
}"
@click="toggleAction(action)"
@click="toggleAction({ value: actionName, label: label })"
>
<div class="q-pa-xs text-caption text-wrap text-center">
{{ action.label }}
{{ label }}
</div>
</q-card>
</div>
@ -43,7 +45,9 @@
<q-avatar color="primary">
<q-icon
color="white"
:name="getActionIcon(action.value)"
:name="
ubrowserOperationConfigs[action.value].icon || 'touch_app'
"
size="14px"
/>
</q-avatar>
@ -72,11 +76,11 @@
/>
</div>
</div>
<div v-if="getOperationConfig(action.value)">
<div v-if="ubrowserOperationConfigs[action.value].config">
<UBrowserOperation
:configs="configs"
:action="action.value"
:fields="getOperationConfig(action.value)"
:fields="ubrowserOperationConfigs[action.value].config"
@update:configs="$emit('update:configs', $event)"
/>
</div>
@ -88,10 +92,7 @@
<script>
import { defineComponent } from "vue";
import {
ubrowserActionIcons,
ubrowserAvailableActions,
} from "../composerConfig";
import { ubrowserOperationConfigs } from "../composerConfig";
import UBrowserOperation from "./operations/UBrowserOperation.vue";
export default defineComponent({
@ -109,12 +110,12 @@ export default defineComponent({
required: true,
},
},
emits: ["remove-action", "update:selectedActions", "update:configs"],
computed: {
availableActions() {
return ubrowserAvailableActions;
},
data() {
return {
ubrowserOperationConfigs: ubrowserOperationConfigs,
};
},
emits: ["remove-action", "update:selectedActions", "update:configs"],
methods: {
moveAction(index, direction) {
const newIndex = index + direction;
@ -126,341 +127,6 @@ export default defineComponent({
this.$emit("update:selectedActions", actions);
}
},
getActionIcon(action) {
return ubrowserActionIcons[action] || "touch_app";
},
getOperationConfig(action) {
const configs = {
wait: [
{
key: "value",
label: "等待时间(ms)或CSS选择器",
icon: "timer",
type: "input",
width: 8,
},
{
key: "timeout",
label: "超时时间(ms)",
icon: "timer_off",
type: "input",
inputType: "number",
width: 4,
},
],
click: [
{
key: "selector",
label: "点击元素的CSS选择器",
icon: "mouse",
type: "input",
},
],
css: [
{
key: "value",
label: "注入的CSS样式",
icon: "style",
type: "textarea",
},
],
press: [
{
key: "key",
label: "按键",
icon: "keyboard",
type: "input",
width: 5,
},
{
key: "modifiers",
label: "修饰键",
type: "checkbox-group",
options: [
{ label: "Ctrl", value: "ctrl" },
{ label: "Shift", value: "shift" },
{ label: "Alt", value: "alt" },
{ label: "Meta", value: "meta" },
],
defaultValue: [],
width: 7,
},
],
paste: [
{
key: "text",
label: "粘贴内容",
icon: "content_paste",
type: "input",
},
],
viewport: [
{
key: "width",
label: "视窗宽度",
icon: "width",
type: "input",
inputType: "number",
width: 6,
},
{
key: "height",
label: "视窗高度",
icon: "height",
type: "input",
inputType: "number",
width: 6,
},
],
screenshot: [
{ key: "selector", label: "元素选择器", icon: "crop", type: "input" },
{
key: "rect.x",
label: "X坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 3,
},
{
key: "rect.y",
label: "Y坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 3,
},
{
key: "rect.width",
label: "宽度",
icon: "width",
type: "input",
inputType: "number",
width: 3,
},
{
key: "rect.height",
label: "高度",
icon: "height",
type: "input",
inputType: "number",
width: 3,
},
{ key: "savePath", label: "保存路径", icon: "save", type: "input" },
],
pdf: [
{
key: "options.marginsType",
label: "边距类型",
type: "select",
options: [
{ label: "默认边距", value: 0 },
{ label: "无边距", value: 1 },
{ label: "最小边距", value: 2 },
],
width: 6,
},
{
key: "options.pageSize",
label: "页面大小",
type: "select",
options: ["A3", "A4", "A5", "Legal", "Letter", "Tabloid"],
width: 6,
},
{ key: "savePath", label: "保存路径", icon: "save", type: "input" },
],
device: [
{
key: "size.width",
label: "设备宽度",
icon: "width",
type: "input",
inputType: "number",
width: 6,
},
{
key: "size.height",
label: "设备高度",
icon: "height",
type: "input",
inputType: "number",
width: 6,
},
{
key: "useragent",
label: "设备User-Agent",
icon: "phone_android",
type: "input",
},
],
cookies: [
{ key: "name", label: "Cookie名称", icon: "cookie", type: "input" },
],
setCookies: [
{ key: "items", label: "Cookie列表", type: "cookie-list" },
],
removeCookies: [
{ key: "name", label: "Cookie名称", icon: "cookie", type: "input" },
],
clearCookies: [
{ key: "url", label: "URL(可选)", icon: "link", type: "input" },
],
evaluate: [
{
key: "function",
label: "JavaScript代码",
icon: "code",
type: "textarea",
},
{ key: "params", label: "参数列表", type: "param-list" },
],
when: [
{
key: "condition",
label: "条件(JavaScript表达式或选择器)",
icon: "rule",
type: "textarea",
},
],
mousedown: [
{
key: "selector",
label: "按下元素选择器",
icon: "mouse",
type: "input",
},
],
mouseup: [
{
key: "selector",
label: "释放元素选择器",
icon: "mouse",
type: "input",
},
],
file: [
{
key: "selector",
label: "文件输入框选择器",
icon: "upload_file",
type: "input",
},
{ key: "files", label: "文件列表", type: "file-list", width: 12 },
],
value: [
{
key: "selector",
label: "元素选择器",
icon: "input",
type: "input",
width: 6,
},
{
key: "value",
label: "设置的值",
icon: "edit",
type: "input",
width: 6,
},
],
check: [
{
key: "selector",
label: "复选框/选框选择器",
icon: "check_box",
type: "input",
width: 8,
},
{
key: "checked",
label: "选中状态",
type: "checkbox",
defaultValue: false,
width: 4,
},
],
focus: [
{
key: "selector",
label: "元素选择器",
icon: "center_focus_strong",
type: "input",
},
],
scroll: [
{
key: "type",
label: "滚动类型",
type: "button-toggle",
options: [
{ label: "滚动到元素", value: "element" },
{ label: "滚动到坐标", value: "position" },
],
defaultValue: "element",
},
{
key: "selector",
label: "目标元素选择器",
icon: "swap_vert",
type: "input",
width: 12,
showWhen: "type",
showValue: "element",
},
{
key: "x",
label: "X坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
showWhen: "type",
showValue: "position",
},
{
key: "y",
label: "Y坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
showWhen: "type",
showValue: "position",
},
],
download: [
{
key: "url",
label: "下载URL",
icon: "link",
type: "input",
width: 6,
},
{
key: "savePath",
label: "保存路径",
icon: "save",
type: "input",
width: 6,
},
],
devTools: [
{
key: "mode",
label: "开发工具位置",
type: "button-toggle",
options: [
{ label: "右侧", value: "right" },
{ label: "底部", value: "bottom" },
{ label: "独立", value: "undocked" },
{ label: "分离", value: "detach" },
],
defaultValue: "right",
},
],
};
return configs[action];
},
toggleAction(action) {
const index = this.selectedActions.findIndex(
(a) => a.value === action.value
@ -481,7 +147,7 @@ export default defineComponent({
]);
//
const config = this.getOperationConfig(action.value);
const { config } = this.ubrowserOperationConfigs[action.value];
if (config) {
const newConfigs = { ...this.configs };
if (!newConfigs[action.value]) {

View File

@ -198,49 +198,20 @@ export default defineComponent({
emits: ["update:configs"],
data() {
return {
localConfigs: {
run: {
show: true,
width: 1280,
height: 800,
x: undefined,
y: undefined,
center: true,
minWidth: 800,
minHeight: 600,
maxWidth: undefined,
maxHeight: undefined,
resizable: true,
movable: true,
minimizable: true,
maximizable: true,
alwaysOnTop: false,
fullscreen: false,
fullscreenable: true,
enableLargerThanScreen: false,
opacity: 1,
},
},
localConfigs: _.cloneDeep(this.configs),
};
},
created() {
//
this.localConfigs = JSON.parse(JSON.stringify(this.configs));
},
methods: {
updateConfig(key, value) {
this.localConfigs.run[key] = value;
this.$emit(
"update:configs",
JSON.parse(JSON.stringify(this.localConfigs))
);
this.$emit("update:configs", _.cloneDeep(this.localConfigs));
},
},
watch: {
configs: {
deep: true,
handler(newConfigs) {
this.localConfigs = JSON.parse(JSON.stringify(newConfigs));
this.localConfigs = _.cloneDeep(newConfigs);
},
},
},

View File

@ -0,0 +1,264 @@
/**
* 生成 UBrowser 代码
* @param {Object} configs UBrowser 配置对象
* @param {Array} selectedActions 已选择的操作列表
* @returns {string} 生成的代码
*/
import { defaultUBrowserConfigs } from "./ubrowserConfig";
export function generateUBrowserCode(configs, selectedActions) {
let code = "utools.ubrowser";
// 基础参数
if (configs.useragent.value) {
code += `\n .useragent('${configs.useragent.value}')`;
}
if (configs.goto.url) {
const gotoOptions = {};
if (configs.goto.headers.Referer) {
gotoOptions.headers = gotoOptions.headers || {};
gotoOptions.headers.Referer = configs.goto.headers.Referer;
}
if (configs.goto.headers.userAgent) {
gotoOptions.headers = gotoOptions.headers || {};
gotoOptions.headers["User-Agent"] = configs.goto.headers.userAgent;
}
if (configs.goto.timeout !== 60000) {
gotoOptions.timeout = configs.goto.timeout;
}
code += `\n .goto('${configs.goto.url}'${
Object.keys(gotoOptions).length
? `,\n${JSON.stringify(gotoOptions, null, 2).replace(/\n/g, "\n ")}`
: ""
})`;
}
// 浏览器操作
selectedActions.forEach((action) => {
const config = configs[action.value];
switch (action.value) {
case "wait":
if (config.type === "time" && config.time) {
code += `\n .wait(${config.time})`;
} else if (config.type === "selector" && config.selector) {
code += `\n .wait('${config.selector}'${
config.timeout !== 60000 ? `, ${config.timeout}` : ""
})`;
} else if (config.type === "function" && config.function) {
const functionBody = config.function.trim();
if (config.args?.length) {
const params = config.args.map((arg) => arg.name).join(", ");
const functionCode = `(${params}) => {\n ${functionBody} \n}`;
const args = `, ${config.timeout || 60000}, ${config.args
.map((arg) => JSON.stringify(arg.value))
.join(", ")}`;
code += `\n .wait(${functionCode}${args})`;
} else {
const functionCode = `() => {\n ${functionBody} \n}`;
code += `\n .wait(${functionCode}${
config.timeout !== 60000 ? `, ${config.timeout}` : ""
})`;
}
}
break;
case "click":
if (config.selector) {
code += `\n .click('${config.selector}')`;
}
break;
case "css":
if (config.value) {
code += `\n .css('${config.value}')`;
}
break;
case "press":
if (config.key) {
const modifiers = config.modifiers.length
? `, ${JSON.stringify(config.modifiers)}`
: "";
code += `\n .press('${config.key}'${modifiers})`;
}
break;
case "paste":
if (config.text) {
code += `\n .paste('${config.text}')`;
}
break;
case "screenshot":
if (config.selector || config.savePath) {
const options = {};
if (config.selector) options.selector = config.selector;
if (config.rect.width && config.rect.height) {
options.rect = config.rect;
}
code += `\n .screenshot('${config.savePath}'${
Object.keys(options).length ? `, ${JSON.stringify(options)}` : ""
})`;
}
break;
case "pdf":
if (config.savePath) {
code += `\n .pdf('${config.savePath}'${
config.options ? `, ${JSON.stringify(config.options)}` : ""
})`;
}
break;
case "device":
if (config.type === "preset" && config.deviceName) {
code += `\n .device('${config.deviceName}')`;
} else if (config.type === "custom") {
const options = {
size: config.size,
};
if (config.useragent) options.useragent = config.useragent;
code += `\n .device(${JSON.stringify(options, null, 2).replace(
/\n/g,
"\n "
)})`;
}
break;
case "cookies":
if (config.name) {
code += `\n .cookies('${config.name}')`;
}
break;
case "setCookies":
if (config.items?.length) {
code += `\n .setCookies(${JSON.stringify(config.items)})`;
}
break;
case "removeCookies":
if (config.name) {
code += `\n .removeCookies('${config.name}')`;
}
break;
case "clearCookies":
code += `\n .clearCookies(${config.url ? `'${config.url}'` : ""})`;
break;
case "evaluate":
if (config.function) {
const functionBody = config.function.trim();
if (config.args?.length) {
const params = config.args.map((arg) => arg.name).join(", ");
const functionCode = `(${params}) => {\n ${functionBody} \n}`;
const args = `, ${config.args
.map((arg) => JSON.stringify(arg.value))
.join(", ")}`;
code += `\n .evaluate(${functionCode}${args})`;
} else {
const functionCode = `() => {\n ${functionBody} \n}`;
code += `\n .evaluate(${functionCode})`;
}
}
break;
case "when":
if (config.condition) {
code += `\n .when('${config.condition}')`;
}
break;
case "mousedown":
case "mouseup":
if (config.selector) {
code += `\n .${action.value}('${config.selector}')`;
}
break;
case "file":
if (config.selector && config.files?.length) {
code += `\n .file('${config.selector}', ${JSON.stringify(
config.files
)})`;
}
break;
case "value":
if (config.selector) {
code += `\n .value('${config.selector}', '${config.value}')`;
}
break;
case "check":
if (config.selector) {
code += `\n .check('${config.selector}'${
config.checked !== undefined ? `, ${config.checked}` : ""
})`;
}
break;
case "focus":
if (config.selector) {
code += `\n .focus('${config.selector}')`;
}
break;
case "scroll":
if (config.type === "element" && config.selector) {
code += `\n .scroll('${config.selector}')`;
} else if (config.type === "position") {
if (config.x !== undefined && config.y !== undefined) {
code += `\n .scroll(${config.x}, ${config.y})`;
} else if (config.y !== undefined) {
code += `\n .scroll(${config.y})`;
}
}
break;
case "download":
if (config.url) {
code += `\n .download('${config.url}'${
config.savePath ? `, '${config.savePath}'` : ""
})`;
}
break;
case "hide":
case "show":
code += `\n .${action.value}()`;
break;
case "devTools":
if (config.mode) {
code += `\n .devTools('${config.mode}')`;
} else {
code += `\n .devTools()`;
}
break;
}
});
// 运行参数
const runOptions = {};
Object.entries(configs.run).forEach(([key, value]) => {
if (
value !== undefined &&
value !== null &&
value !== defaultUBrowserConfigs.run[key]
) {
runOptions[key] = value;
}
});
code += `\n .run(${
Object.keys(runOptions).length
? `\n${JSON.stringify(runOptions, null, 2).replace(/\n/g, "\n ")}`
: ""
})`;
return code;
}

View File

@ -0,0 +1,50 @@
<template>
<div class="row items-center no-wrap">
<q-badge class="q-pa-xs">{{ label }}</q-badge>
<q-btn-toggle
:model-value="modelValue"
:options="options"
dense
flat
no-caps
spread
class="button-group"
@update:model-value="$emit('update:modelValue', $event)"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserButtonToggle",
props: {
modelValue: {
type: [String, Number, Boolean],
required: true,
},
label: {
type: String,
required: true,
},
options: {
type: Array,
required: true,
},
},
emits: ["update:modelValue"],
});
</script>
<style scoped>
.button-group {
flex: 1;
padding: 0 10px;
}
.button-group :deep(.q-btn) {
min-height: 24px;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div class="row items-center no-wrap">
<q-badge class="q-pa-xs">{{ label }}</q-badge>
<q-btn-toggle
:model-value="modelValue ? 'true' : 'false'"
:options="[
{ label: '是', value: 'true' },
{ label: '否', value: 'false' },
]"
dense
flat
no-caps
spread
class="button-group"
@update:model-value="$emit('update:modelValue', $event === 'true')"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserCheckbox",
props: {
modelValue: {
type: Boolean,
default: false,
},
label: {
type: String,
required: true,
},
},
emits: ["update:modelValue"],
});
</script>

View File

@ -0,0 +1,31 @@
<template>
<div class="row items-center">
<q-option-group
:model-value="modelValue"
:options="options"
type="checkbox"
inline
dense
@update:model-value="$emit('update:modelValue', $event)"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserCheckboxGroup",
props: {
modelValue: {
type: Array,
default: () => [],
},
options: {
type: Array,
required: true,
},
},
emits: ["update:modelValue"],
});
</script>

View File

@ -0,0 +1,89 @@
<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">
<q-input
:model-value="cookie.name"
label="名称"
dense
outlined
@update:model-value="
(value) => handleUpdate(index, 'name', value)
"
/>
</div>
<div class="col">
<q-input
:model-value="cookie.value"
label="值"
dense
outlined
@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";
export default defineComponent({
name: "UBrowserCookieList",
props: {
modelValue: {
type: Array,
default: () => [{ name: "", value: "" }],
},
},
emits: ["update:modelValue"],
methods: {
addCookie() {
const newValue = [...this.modelValue, { name: "", value: "" }];
this.$emit("update:modelValue", newValue);
},
removeCookie(index) {
const newValue = [...this.modelValue];
newValue.splice(index, 1);
if (newValue.length === 0) {
newValue.push({ name: "", value: "" });
}
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

@ -0,0 +1,83 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col">
<q-input
:model-value="modelValue"
:label="label"
dense
outlined
@update:model-value="$emit('update:modelValue', $event)"
>
<template v-slot:prepend>
<q-icon :name="icon" />
</template>
</q-input>
</div>
<div class="col-auto">
<q-select
v-model="selectedDevice"
:options="deviceOptions"
label="常用设备"
dense
outlined
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";
export default defineComponent({
name: "UBrowserDeviceName",
props: {
modelValue: {
type: String,
default: "",
},
label: {
type: String,
required: true,
},
icon: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
data() {
return {
selectedDevice: null,
deviceOptions: [
{ label: "iPhone 11", value: "iPhone 11" },
{ label: "iPhone X", value: "iPhone X" },
{ label: "iPad", value: "iPad" },
{ label: "iPhone 6/7/8 Plus", value: "iPhone 6/7/8 Plus" },
{ label: "iPhone 6/7/8", value: "iPhone 6/7/8" },
{ label: "iPhone 5/SE", value: "iPhone 5/SE" },
{ label: "HUAWEI Mate10", value: "HUAWEI Mate10" },
{ label: "HUAWEI Mate20", value: "HUAWEI Mate20" },
{ label: "HUAWEI Mate30", value: "HUAWEI Mate30" },
{ label: "HUAWEI Mate30 Pro", value: "HUAWEI Mate30 Pro" },
],
};
},
methods: {
handleDeviceSelect(value) {
if (value) {
this.$emit("update:modelValue", value);
this.selectedDevice = null;
}
},
},
});
</script>

View File

@ -0,0 +1,68 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-input
v-model.number="size.width"
type="number"
label="宽度"
dense
outlined
@update:model-value="handleUpdate"
>
<template v-slot:prepend>
<q-icon name="width" />
</template>
</q-input>
</div>
<div class="col-6">
<q-input
v-model.number="size.height"
type="number"
label="高度"
dense
outlined
@update:model-value="handleUpdate"
>
<template v-slot:prepend>
<q-icon name="height" />
</template>
</q-input>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserDeviceSize",
props: {
modelValue: {
type: Object,
default: () => ({ width: 0, height: 0 }),
},
},
emits: ["update:modelValue"],
data() {
return {
size: {
width: this.modelValue.width,
height: this.modelValue.height,
},
};
},
methods: {
handleUpdate() {
this.$emit("update:modelValue", { ...this.size });
},
},
watch: {
modelValue: {
deep: true,
handler(newValue) {
this.size = { ...newValue };
},
},
},
});
</script>

View File

@ -0,0 +1,73 @@
<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">
<q-input
:model-value="modelValue[index]"
label="文件路径"
dense
outlined
@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";
export default defineComponent({
name: "UBrowserFileList",
props: {
modelValue: {
type: Array,
default: () => [],
},
},
emits: ["update:modelValue"],
methods: {
addFile() {
const newValue = [...(this.modelValue || []), ""];
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

@ -0,0 +1,197 @@
<template>
<div class="row q-col-gutter-sm">
<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
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 text-bold">(</div>
</template>
<template v-slot:append>
<div class="text-primary text-bold">)</div>
</template>
</q-select>
</div>
<div class="col-9">
<q-input
v-model="localFunction"
:label="label"
type="textarea"
dense
outlined
autogrow
@update:model-value="updateFunction"
>
<template v-slot:prepend>
<div class="text-primary text-bold">=> {</div>
</template>
<template v-slot:append>
<div class="text-primary text-bold">}</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
outlined
@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>

View File

@ -1,26 +1,16 @@
<template>
<div class="row q-col-gutter-sm">
<div :class="fullWidth ? 'col-12' : 'col-' + width">
<q-input
:value="modelValue"
v-bind="$attrs"
filled
square
:dense="!large"
@update:modelValue="handleInput"
>
<template v-if="$slots.prepend" v-slot:prepend>
<slot name="prepend" />
</template>
<template v-else-if="icon" v-slot:prepend>
<q-icon :name="icon" />
</template>
<template v-if="$slots.append" v-slot:append>
<slot name="append" />
</template>
</q-input>
</div>
</div>
<q-input
:model-value="modelValue"
:label="label"
:type="inputType"
dense
outlined
@update:model-value="$emit('update:modelValue', $event)"
>
<template v-slot:prepend>
<q-icon :name="icon" />
</template>
</q-input>
</template>
<script>
@ -28,38 +18,24 @@ import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserInput",
inheritAttrs: false,
props: {
modelValue: {
type: [String, Number],
default: "",
},
label: {
type: String,
required: true,
},
icon: {
type: String,
default: "",
},
width: {
type: [Number, String],
default: 12,
},
fullWidth: {
type: Boolean,
default: true,
},
flat: {
type: Boolean,
default: false,
},
large: {
type: Boolean,
default: false,
inputType: {
type: String,
default: "text",
},
},
emits: ["update:modelValue"],
methods: {
handleInput(value) {
this.$emit("update:modelValue", value);
},
},
});
</script>

View File

@ -0,0 +1,82 @@
<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">
<q-input
:model-value="param.name"
label="参数名"
dense
outlined
@update:model-value="(value) => handleUpdate(index, 'name', value)"
/>
</div>
<div class="col-5">
<q-input
:model-value="param.value"
label="传递给参数的值"
dense
outlined
@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";
export default defineComponent({
name: "UBrowserNamedParamList",
props: {
modelValue: {
type: Array,
default: () => [],
},
label: {
type: String,
required: true,
},
},
emits: ["update:modelValue"],
methods: {
addParam() {
const newValue = [...(this.modelValue || []), { name: "", value: "" }];
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

@ -7,240 +7,119 @@
>
<!-- 复选框组 -->
<template v-if="field.type === 'checkbox-group'">
<div class="row items-center">
<!-- <div class="text-caption q-mb-sm">{{ field.label }}</div> -->
<q-option-group
:model-value="fieldValue[field.key] || []"
:options="field.options"
type="checkbox"
inline
dense
@update:model-value="updateValue(field.key, $event)"
/>
</div>
<UBrowserCheckboxGroup
v-model="fieldValue[field.key]"
:options="field.options"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 单个复选框 -->
<template v-else-if="field.type === 'checkbox'">
<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>
<UBrowserCheckbox
v-model="fieldValue[field.key]"
:label="field.label"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 文本输入 -->
<template v-else-if="field.type === 'input'">
<q-input
:model-value="fieldValue[field.key]"
:label="field.label"
:type="field.inputType || 'text'"
dense
outlined
@update:model-value="updateValue(field.key, $event)"
>
<template v-slot:prepend>
<q-icon :name="field.icon" />
</template>
</q-input>
<!-- 基本输入类型的处理 -->
<template v-if="field.type === 'input'">
<!-- 设备名称特殊处理 -->
<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>
<UBrowserInput
v-model="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
:input-type="field.inputType"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
</template>
<!-- 文本区域 -->
<template v-else-if="field.type === 'textarea'">
<q-input
:model-value="fieldValue[field.key]"
<UBrowserTextarea
v-model="fieldValue[field.key]"
:label="field.label"
type="textarea"
dense
outlined
autogrow
:icon="field.icon"
@update:model-value="updateValue(field.key, $event)"
>
<template v-slot:prepend>
<q-icon :name="field.icon" />
</template>
</q-input>
/>
</template>
<!-- 选择框 -->
<template v-else-if="field.type === 'select'">
<q-select
:model-value="fieldValue[field.key]"
<UBrowserSelect
v-model="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
:options="field.options"
dense
outlined
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'">
<div class="row q-col-gutter-sm">
<div
v-for="(cookie, index) in fieldValue[field.key] || [{}]"
:key="index"
class="col-12"
>
<div class="row items-center q-gutter-x-sm">
<div class="col">
<q-input
v-model="cookie.name"
label="名称"
dense
outlined
@update:model-value="updateCookieList(field.key)"
/>
</div>
<div class="col">
<q-input
v-model="cookie.value"
label="值"
dense
outlined
@update:model-value="updateCookieList(field.key)"
/>
</div>
<div class="col-auto">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeCookie(field.key, index)"
/>
</div>
</div>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加Cookie"
@click="addCookie(field.key)"
class="q-mt-xs"
<UBrowserCookieList
v-model="fieldValue[field.key]"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 参数列表 -->
<template v-else-if="field.type === 'param-list'">
<div class="text-caption q-mb-sm">{{ field.label }}</div>
<div
v-for="(param, index) in fieldValue[field.key] || []"
:key="index"
class="row q-col-gutter-sm q-mb-sm"
>
<div class="col-10">
<q-input
v-model="fieldValue[field.key][index]"
label="参数值"
dense
outlined
@update:model-value="
updateValue(field.key, fieldValue[field.key])
"
/>
</div>
<div class="col-2">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeParam(field.key, index)"
/>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加参数"
@click="addParam(field.key)"
<!-- 命名参数列表 -->
<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'">
<div class="row q-col-gutter-sm">
<div
v-for="(file, index) in fieldValue[field.key] || []"
:key="index"
class="col-12"
>
<div class="row q-col-gutter-sm">
<div class="col">
<q-input
v-model="fieldValue[field.key][index]"
label="文件路径"
dense
outlined
@update:model-value="
updateValue(field.key, fieldValue[field.key])
"
/>
</div>
<div class="col-auto">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeFile(field.key, index)"
/>
</div>
</div>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加文件"
@click="addFile(field.key)"
class="q-mt-xs"
<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>
<UBrowserButtonToggle
v-model="fieldValue[field.key]"
:label="field.label"
:options="field.options"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 设备尺寸 -->
<template v-else-if="field.type === 'device-size'">
<UBrowserDeviceSize
v-model="fieldValue.size"
@update:model-value="updateValue(field.key, $event)"
/>
</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>
@ -250,9 +129,35 @@
<script>
import { defineComponent } from "vue";
import { get, set } from "lodash";
import UBrowserFunctionInput from "./UBrowserFunctionInput.vue";
import UBrowserCheckbox from "./UBrowserCheckbox.vue";
import UBrowserFileList from "./UBrowserFileList.vue";
import UBrowserCookieList from "./UBrowserCookieList.vue";
import UBrowserButtonToggle from "./UBrowserButtonToggle.vue";
import UBrowserDeviceSize from "./UBrowserDeviceSize.vue";
import UBrowserNamedParamList from "./UBrowserNamedParamList.vue";
import UBrowserSelect from "./UBrowserSelect.vue";
import UBrowserDeviceName from "./UBrowserDeviceName.vue";
import UBrowserTextarea from "./UBrowserTextarea.vue";
import UBrowserInput from "./UBrowserInput.vue";
import UBrowserCheckboxGroup from "./UBrowserCheckboxGroup.vue";
export default defineComponent({
name: "UBrowserOperation",
components: {
UBrowserFunctionInput,
UBrowserCheckbox,
UBrowserFileList,
UBrowserCookieList,
UBrowserButtonToggle,
UBrowserDeviceSize,
UBrowserNamedParamList,
UBrowserSelect,
UBrowserDeviceName,
UBrowserTextarea,
UBrowserInput,
UBrowserCheckboxGroup,
},
props: {
configs: {
type: Object,
@ -283,6 +188,11 @@ export default defineComponent({
defaultValue = field.defaultValue || [];
} else if (field.type === "checkbox") {
defaultValue = field.defaultValue || false;
} else if (field.type === "function-with-params") {
// function-with-params
this.fieldValue.function = value?.function || "";
this.fieldValue.args = value?.args || [];
return; //
} else {
defaultValue = field.defaultValue;
}
@ -306,51 +216,6 @@ export default defineComponent({
//
this.$emit("update:configs", newConfigs);
},
// Cookie
addCookie(key) {
if (!this.fieldValue[key]) {
this.fieldValue[key] = [];
}
this.fieldValue[key].push({ name: "", value: "" });
this.updateValue(key, this.fieldValue[key]);
},
removeCookie(key, index) {
this.fieldValue[key].splice(index, 1);
if (this.fieldValue[key].length === 0) {
this.fieldValue[key].push({ name: "", value: "" });
}
this.updateValue(key, this.fieldValue[key]);
},
updateCookieList(key) {
this.updateValue(key, this.fieldValue[key]);
},
//
addParam(key) {
if (!this.fieldValue[key]) {
this.fieldValue[key] = [];
}
this.fieldValue[key].push("");
this.updateValue(key, this.fieldValue[key]);
},
removeParam(key, index) {
this.fieldValue[key].splice(index, 1);
this.updateValue(key, this.fieldValue[key]);
},
//
addFile(key) {
if (!this.fieldValue[key]) {
this.fieldValue[key] = [];
}
this.fieldValue[key].push("");
this.updateValue(key, this.fieldValue[key]);
},
removeFile(key, index) {
this.fieldValue[key].splice(index, 1);
this.updateValue(key, this.fieldValue[key]);
},
},
watch: {
//
@ -359,6 +224,13 @@ export default defineComponent({
handler() {
this.fields.forEach((field) => {
const value = get(this.configs[this.action], field.key);
if (field.type === "function-with-params") {
// function-with-params
this.fieldValue.function =
value?.function || this.fieldValue.function || "";
this.fieldValue.args = value?.args || this.fieldValue.args || [];
return;
}
if (value !== undefined) {
this.fieldValue[field.key] = value;
}
@ -368,19 +240,3 @@ export default defineComponent({
},
});
</script>
<style scoped>
.button-group-container {
position: relative;
}
.button-group {
flex: 1;
padding: 0 10px;
}
.button-group :deep(.q-btn) {
min-height: 24px;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<q-select
:model-value="modelValue"
:label="label"
:options="options"
dense
outlined
emit-value
map-options
@update:model-value="$emit('update:modelValue', $event)"
>
<template v-slot:prepend>
<q-icon :name="icon" />
</template>
</q-select>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserSelect",
props: {
modelValue: {
type: [String, Number],
default: "",
},
label: {
type: String,
required: true,
},
options: {
type: Array,
required: true,
},
icon: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
});
</script>

View File

@ -0,0 +1,38 @@
<template>
<q-input
:model-value="modelValue"
:label="label"
type="textarea"
dense
outlined
autogrow
@update:model-value="$emit('update:modelValue', $event)"
>
<template v-slot:prepend>
<q-icon :name="icon" />
</template>
</q-input>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserTextarea",
props: {
modelValue: {
type: String,
default: "",
},
label: {
type: String,
required: true,
},
icon: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
});
</script>

View File

@ -0,0 +1,629 @@
// ubrowser 浏览器操作配置
export const ubrowserOperationConfigs = {
wait: {
label: "等待",
config: [
{
key: "type",
label: "等待类型",
type: "button-toggle",
options: [
{ label: "等待时间", value: "time" },
{ label: "等待元素", value: "selector" },
{ label: "等待条件", value: "function" },
],
defaultValue: "time",
},
{
key: "time",
label: "等待时间(ms)",
icon: "timer",
type: "input",
inputType: "number",
width: 12,
showWhen: "type",
showValue: "time",
},
{
key: "selector",
label: "等待元素的CSS选择器",
icon: "find_in_page",
type: "input",
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",
type: "input",
inputType: "number",
width: 12,
defaultValue: 60000,
showWhen: "type",
showValue: ["selector", "function"],
},
],
icon: "timer",
},
click: {
label: "点击",
config: [
{
key: "selector",
label: "点击元素的CSS选择器",
icon: "mouse",
type: "input",
},
],
icon: "mouse",
},
css: {
label: "注入CSS",
config: [
{
key: "value",
label: "注入的CSS样式",
icon: "style",
type: "textarea",
},
],
icon: "style",
},
press: {
label: "按键",
config: [
{
key: "key",
label: "按键",
icon: "keyboard",
type: "input",
width: 5,
},
{
key: "modifiers",
label: "修饰键",
type: "checkbox-group",
options: [
{ label: "Ctrl", value: "ctrl" },
{ label: "Shift", value: "shift" },
{ label: "Alt", value: "alt" },
{ label: "Meta", value: "meta" },
],
defaultValue: [],
width: 7,
},
],
icon: "keyboard",
},
paste: {
label: "粘贴",
config: [
{
key: "text",
label: "粘贴内容",
icon: "content_paste",
type: "input",
},
],
icon: "content_paste",
},
viewport: {
label: "视窗",
config: [
{
key: "width",
label: "视窗宽度",
icon: "width",
type: "input",
inputType: "number",
width: 6,
},
{
key: "height",
label: "视窗高度",
icon: "height",
type: "input",
inputType: "number",
width: 6,
},
],
icon: "crop",
},
screenshot: {
label: "截图",
config: [
{ key: "selector", label: "元素选择器", icon: "crop", type: "input" },
{
key: "rect.x",
label: "X坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 3,
},
{
key: "rect.y",
label: "Y坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 3,
},
{
key: "rect.width",
label: "宽度",
icon: "width",
type: "input",
inputType: "number",
width: 3,
},
{
key: "rect.height",
label: "高度",
icon: "height",
type: "input",
inputType: "number",
width: 3,
},
{ key: "savePath", label: "保存路径", icon: "save", type: "input" },
],
icon: "picture_as_pdf",
},
pdf: {
label: "导出PDF",
config: [
{
key: "options.marginsType",
label: "边距类型",
type: "select",
options: [
{ label: "默认边距", value: 0 },
{ label: "无边距", value: 1 },
{ label: "最小边距", value: 2 },
],
width: 6,
},
{
key: "options.pageSize",
label: "页面大小",
type: "select",
options: ["A3", "A4", "A5", "Legal", "Letter", "Tabloid"],
width: 6,
},
{ key: "savePath", label: "保存路径", icon: "save", type: "input" },
],
icon: "devices",
},
device: {
label: "模拟设备",
config: [
{
key: "type",
label: "设备类型",
type: "button-toggle",
options: [
{ label: "特定设备", value: "preset" },
{ label: "自定义设备", value: "custom" },
],
defaultValue: "preset",
},
{
key: "deviceName",
label: "设备名称",
icon: "smartphone",
type: "input",
width: 12,
showWhen: "type",
showValue: "preset",
},
{
key: "size",
label: "设备尺寸",
type: "device-size",
width: 12,
showWhen: "type",
showValue: "custom",
},
{
key: "useragent",
label: "User-Agent",
icon: "devices",
type: "input",
width: 12,
showWhen: "type",
showValue: "custom",
},
],
icon: "cookie",
},
setCookies: {
label: "设置Cookie",
config: [{ key: "items", label: "Cookie列表", type: "cookie-list" }],
icon: "cookie",
},
removeCookies: {
label: "删除Cookie",
config: [
{ key: "name", label: "Cookie名称", icon: "cookie", type: "input" },
],
icon: "cookie",
},
clearCookies: {
label: "清空Cookie",
config: [{ key: "url", label: "URL(可选)", icon: "link", type: "input" }],
icon: "cookie",
},
evaluate: {
label: "执行代码",
config: [
{
key: "function",
label: "执行的代码",
icon: "code",
type: "function-with-params",
width: 12,
},
],
icon: "code",
},
when: {
label: "条件判断",
config: [
{
key: "type",
label: "条件类型",
type: "button-toggle",
options: [
{ label: "等待元素", value: "selector" },
{ label: "等待条件", value: "function" },
],
defaultValue: "selector",
},
{
key: "selector",
label: "等待元素的CSS选择器",
icon: "find_in_page",
type: "input",
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",
type: "input",
inputType: "number",
width: 12,
defaultValue: 60000,
showWhen: "type",
showValue: ["selector", "function"],
},
],
icon: "rule",
},
end: {
label: "结束条件",
config: [],
icon: "stop",
},
mousedown: {
label: "按下鼠标",
config: [
{
key: "selector",
label: "按下元素选择器",
icon: "mouse",
type: "input",
},
],
icon: "mouse",
},
mouseup: {
label: "释放鼠标",
config: [
{
key: "selector",
label: "释放元素选择器",
icon: "mouse",
type: "input",
},
],
icon: "mouse",
},
file: {
label: "上传文件",
config: [
{
key: "selector",
label: "文件输入框选择器",
icon: "upload_file",
type: "input",
},
{ key: "files", label: "文件列表", type: "file-list", width: 12 },
],
icon: "upload_file",
},
value: {
label: "设置值",
config: [
{
key: "selector",
label: "元素选择器",
icon: "input",
type: "input",
width: 6,
},
{
key: "value",
label: "设置的值",
icon: "edit",
type: "input",
width: 6,
},
],
icon: "check_box",
},
check: {
label: "设置选中",
config: [
{
key: "selector",
label: "复选框/选框选择器",
icon: "check_box",
type: "input",
width: 8,
},
{
key: "checked",
label: "选中状态",
type: "checkbox",
defaultValue: false,
width: 4,
},
],
icon: "center_focus_strong",
},
focus: {
label: "聚焦元素",
config: [
{
key: "selector",
label: "元素选择器",
icon: "center_focus_strong",
type: "input",
},
],
icon: "swap_vert",
},
scroll: {
label: "滚动",
config: [
{
key: "type",
label: "滚动类型",
type: "button-toggle",
options: [
{ label: "滚动到元素", value: "element" },
{ label: "滚动到坐标", value: "position" },
],
defaultValue: "element",
},
{
key: "selector",
label: "目标元素选择器",
icon: "swap_vert",
type: "input",
width: 12,
showWhen: "type",
showValue: "element",
},
{
key: "x",
label: "X坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
showWhen: "type",
showValue: "position",
},
{
key: "y",
label: "Y坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
showWhen: "type",
showValue: "position",
},
],
icon: "download",
},
download: {
label: "下载",
config: [
{
key: "url",
label: "下载URL",
icon: "link",
type: "input",
width: 6,
},
{
key: "savePath",
label: "保存路径",
icon: "save",
type: "input",
width: 6,
},
],
icon: "download",
},
devTools: {
label: "开发工具",
config: [
{
key: "mode",
label: "开发工具位置",
type: "button-toggle",
options: [
{ label: "右侧", value: "right" },
{ label: "底部", value: "bottom" },
{ label: "独立", value: "undocked" },
{ label: "分离", value: "detach" },
],
defaultValue: "right",
},
],
icon: "developer_board",
},
};
// 添加默认运行配置
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 = {
// 基础参数
useragent: {
value: "",
},
goto: {
url: "",
headers: {
Referer: "",
userAgent: "",
},
timeout: 60000,
},
// 浏览器操作
wait: {
value: "",
timeout: 60000,
},
click: {
selector: "",
},
css: {
value: "",
},
press: {
key: "",
modifiers: [],
},
paste: {
text: "",
},
screenshot: {
selector: "",
rect: { x: 0, y: 0, width: 0, height: 0 },
savePath: "",
},
pdf: {
options: {
marginsType: 0,
pageSize: "A4",
},
savePath: "",
},
device: {
size: { width: 1280, height: 800 },
useragent: "",
},
cookies: {
name: "",
},
setCookies: {
items: [{ name: "", value: "" }],
},
removeCookies: {
name: "",
},
clearCookies: {
url: "",
},
evaluate: {
function: "",
params: [],
},
when: {
condition: "",
},
mousedown: {
selector: "",
},
mouseup: {
selector: "",
},
file: {
selector: "",
files: [],
},
value: {
selector: "",
value: "",
},
check: {
selector: "",
checked: false,
},
focus: {
selector: "",
},
scroll: {
target: "",
x: 0,
y: 0,
},
download: {
url: "",
savePath: "",
},
// 运行参数
run: defaultUBrowserRunConfigs,
};
// 定义 ubrowser 操作图标映射
// 格式:{ wait: 'timer', ...}
export const ubrowserActionIcons = _.mapValues(
ubrowserOperationConfigs,
(config) => config.icon
);