完善ubrowser可视化编排

This commit is contained in:
fofolee 2024-12-23 12:18:31 +08:00
parent d0223d3e61
commit a22e1e23e9
6 changed files with 1157 additions and 751 deletions

View File

@ -1,24 +1,24 @@
// 定义命令图标映射
export const commandIcons = {
'open': 'folder_open',
'locate': 'location_on',
'visit': 'language',
'utools.ubrowser.goto': 'public',
'system': 'terminal',
'copyTo': 'content_copy',
'message': 'message',
'alert': 'warning',
'send': 'send',
'utools.redirect': 'alt_route',
'quickcommand.sleep': 'schedule',
'keyTap': 'keyboard'
}
open: "folder_open",
locate: "location_on",
visit: "language",
"utools.ubrowser.goto": "public",
system: "terminal",
copyTo: "content_copy",
message: "message",
alert: "warning",
send: "send",
"utools.redirect": "alt_route",
"quickcommand.sleep": "schedule",
keyTap: "keyboard",
};
// 定义命令分类
export const commandCategories = [
{
label: '文件操作',
icon: 'folder',
label: "文件操作",
icon: "folder",
commands: [
{
value: "open",
@ -29,12 +29,12 @@ export const commandCategories = [
value: "locate",
label: "在文件管理器中定位文件",
desc: "要在文件管理器里显示的文件路径",
}
]
},
],
},
{
label: '网络操作',
icon: 'language',
label: "网络操作",
icon: "language",
commands: [
{
value: "visit",
@ -50,13 +50,13 @@ export const commandCategories = [
value: "ubrowser",
label: "UBrowser浏览器操作",
desc: "配置UBrowser浏览器操作",
hasUBrowserEditor: true
}
]
hasUBrowserEditor: true,
},
],
},
{
label: '系统操作',
icon: 'computer',
label: "系统操作",
icon: "computer",
commands: [
{
value: "system",
@ -67,12 +67,12 @@ export const commandCategories = [
value: "copyTo",
label: "将内容写入剪贴板",
desc: "要写入剪切板的内容",
}
]
},
],
},
{
label: '消息通知',
icon: 'notifications',
label: "消息通知",
icon: "notifications",
commands: [
{
value: "message",
@ -88,12 +88,12 @@ export const commandCategories = [
value: "send",
label: "发送文本到活动窗口",
desc: "要发送到窗口的文本内容",
}
]
},
],
},
{
label: '其他功能',
icon: 'more_horiz',
label: "其他功能",
icon: "more_horiz",
commands: [
{
value: "utools.redirect",
@ -104,36 +104,91 @@ export const commandCategories = [
value: "quickcommand.sleep",
label: "添加延时",
desc: "延迟的毫秒数",
}
]
},
],
},
{
label: '按键操作',
icon: 'keyboard',
label: "按键操作",
icon: "keyboard",
commands: [
{
value: "keyTap",
label: "模拟按键",
desc: "模拟键盘按键",
hasKeyRecorder: true
}
]
}
]
hasKeyRecorder: true,
},
],
},
];
// 定义哪些命令可以产生输出
export const commandsWithOutput = {
'system': true,
'open': true,
'locate': true,
'copyTo': true,
'ubrowser': true,
}
system: true,
open: true,
locate: true,
copyTo: true,
ubrowser: true,
};
// 定义哪些命令可以接收输出
export const commandsAcceptOutput = {
'message': true,
'alert': true,
'send': true,
'copyTo': true,
}
message: true,
alert: true,
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

@ -1,86 +1,115 @@
<template>
<div class="row q-col-gutter-sm">
<!-- UserAgent -->
<!-- 基础配置 -->
<div class="col-12">
<UBrowserInput
:value="configs.useragent.value"
@update:modelValue="updateConfig('useragent.value', $event)"
label="UserAgent"
icon="person"
/>
<q-input
v-model="localConfigs.goto.url"
label="网址"
dense
outlined
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="link" />
</template>
</q-input>
</div>
<!-- URL -->
<!-- 超时配置 -->
<div class="col-12">
<UBrowserInput
:value="configs.goto.url"
@update:modelValue="updateConfig('goto.url', $event)"
label="URL"
icon="link"
/>
</div>
<!-- Headers -->
<div class="col-12">
<div class="text-subtitle2 q-mb-sm">请求头</div>
<UBrowserInput
:value="configs.goto.headers.Referer"
@update:modelValue="updateConfig('goto.headers.Referer', $event)"
label="Referer"
icon="link"
class="q-mb-sm"
/>
<UBrowserInput
:value="configs.goto.headers.userAgent"
@update:modelValue="updateConfig('goto.headers.userAgent', $event)"
label="User-Agent"
icon="person"
/>
</div>
<!-- Timeout -->
<div class="col-12">
<UBrowserInput
:value="configs.goto.timeout"
@update:modelValue="updateConfig('goto.timeout', $event)"
<q-input
v-model.number="localConfigs.goto.timeout"
type="number"
label="超时时间(ms)"
icon="timer"
/>
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">
<div class="col-12">
<q-input
v-model="localConfigs.goto.headers.Referer"
label="Referer"
dense
outlined
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="link" />
</template>
</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>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import UBrowserInput from './operations/UBrowserInput.vue';
import { defineComponent } from "vue";
export default defineComponent({
name: 'UBrowserBasic',
components: {
UBrowserInput
},
name: "UBrowserBasic",
props: {
configs: {
type: Object,
required: true
}
required: true,
},
},
emits: ["update:configs"],
data() {
return {
localConfigs: {
goto: {
url: "",
headers: {
Referer: "",
userAgent: "",
},
timeout: 60000,
},
},
};
},
created() {
//
this.localConfigs = JSON.parse(JSON.stringify(this.configs));
},
emits: ['update:configs'],
methods: {
updateConfig(path, value) {
const newConfigs = { ...this.configs };
const keys = path.split('.');
let current = newConfigs;
for (let i = 0; i < keys.length - 1; i++) {
current[keys[i]] = { ...current[keys[i]] };
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
this.$emit('update:configs', newConfigs);
}
}
updateConfigs() {
this.$emit(
"update:configs",
JSON.parse(JSON.stringify(this.localConfigs))
);
},
},
watch: {
configs: {
deep: true,
handler(newConfigs) {
this.localConfigs = JSON.parse(JSON.stringify(newConfigs));
},
},
},
});
</script>

View File

@ -11,46 +11,18 @@
class="ubrowser-stepper"
>
<!-- 基础参数步骤 -->
<q-step
:name="1"
title="基础参数"
icon="settings"
:done="step > 1"
>
<UBrowserBasic
:configs="configs"
@update:configs="updateConfigs"
/>
<q-step :name="1" title="基础参数" icon="settings" :done="step > 1">
<UBrowserBasic :configs="configs" @update:configs="updateConfigs" />
</q-step>
<!-- 浏览器操作步骤 -->
<q-step
:name="2"
title="浏览器操作"
icon="touch_app"
:done="step > 2"
>
<div class="row q-col-gutter-sm">
<div class="col-12">
<q-select
v-model="selectedActions"
:options="availableActions"
multiple
use-chips
outlined
dense
label="选择操作"
/>
</div>
<div class="col-12">
<UBrowserOperations
:configs="configs"
@update:configs="updateConfigs"
v-model:selected-actions="selectedActions"
@remove-action="removeAction"
/>
</div>
</div>
<q-step :name="2" title="浏览器操作" icon="touch_app" :done="step > 2">
<UBrowserOperations
:configs="configs"
@update:configs="updateConfigs"
v-model:selected-actions="selectedActions"
@remove-action="removeAction"
/>
</q-step>
<!-- 运行参数步骤 -->
@ -60,35 +32,55 @@
icon="settings_applications"
class="q-pb-md"
>
<UBrowserRun
:configs="configs"
@update:configs="updateConfigs"
/>
<UBrowserRun :configs="configs" @update:configs="updateConfigs" />
</q-step>
</q-stepper>
</div>
</template>
<style scoped>
.ubrowser-editor {
width: 100%;
}
.ubrowser-stepper {
box-shadow: none;
background-color: rgba(255, 255, 255, 0.8);
}
.body--dark .ubrowser-stepper {
background-color: rgba(255, 255, 255, 0.05);
}
.ubrowser-stepper :deep(.q-stepper__header) {
cursor: pointer;
}
.ubrowser-stepper :deep(.q-stepper__step-inner) {
padding-bottom: 5px;
}
</style>
<script>
import { defineComponent } from 'vue';
import UBrowserBasic from './UBrowserBasic.vue';
import UBrowserOperations from './UBrowserOperations.vue';
import UBrowserRun from './UBrowserRun.vue';
import { defineComponent } from "vue";
import UBrowserBasic from "./UBrowserBasic.vue";
import UBrowserOperations from "./UBrowserOperations.vue";
import UBrowserRun from "./UBrowserRun.vue";
export default defineComponent({
name: 'UBrowserEditor',
name: "UBrowserEditor",
components: {
UBrowserBasic,
UBrowserOperations,
UBrowserRun
UBrowserRun,
},
props: {
modelValue: {
type: String,
default: ''
}
default: "",
},
},
emits: ['update:modelValue'],
emits: ["update:modelValue"],
data() {
return {
step: 1,
@ -96,98 +88,98 @@ export default defineComponent({
configs: {
//
useragent: {
value: ''
value: "",
},
goto: {
url: '',
url: "",
headers: {
Referer: '',
userAgent: ''
Referer: "",
userAgent: "",
},
timeout: 60000
timeout: 60000,
},
//
wait: {
value: '',
timeout: 60000
value: "",
timeout: 60000,
},
click: {
selector: ''
selector: "",
},
css: {
value: ''
value: "",
},
press: {
key: '',
modifiers: []
key: "",
modifiers: [],
},
paste: {
text: ''
text: "",
},
screenshot: {
selector: '',
selector: "",
rect: { x: 0, y: 0, width: 0, height: 0 },
savePath: ''
savePath: "",
},
pdf: {
options: {
marginsType: 0,
pageSize: 'A4'
pageSize: "A4",
},
savePath: ''
savePath: "",
},
device: {
size: { width: 1280, height: 800 },
useragent: ''
useragent: "",
},
cookies: {
name: ''
name: "",
},
setCookies: {
items: [{ name: '', value: '' }]
items: [{ name: "", value: "" }],
},
removeCookies: {
name: ''
name: "",
},
clearCookies: {
url: ''
url: "",
},
evaluate: {
function: '',
params: []
function: "",
params: [],
},
when: {
condition: ''
condition: "",
},
mousedown: {
selector: ''
selector: "",
},
mouseup: {
selector: ''
selector: "",
},
file: {
selector: '',
files: []
selector: "",
files: [],
},
value: {
selector: '',
value: ''
selector: "",
value: "",
},
check: {
selector: '',
checked: false
selector: "",
checked: false,
},
focus: {
selector: ''
selector: "",
},
scroll: {
target: '',
target: "",
x: 0,
y: 0
y: 0,
},
download: {
url: '',
savePath: ''
url: "",
savePath: "",
},
//
run: {
@ -209,54 +201,42 @@ export default defineComponent({
fullscreen: false,
fullscreenable: true,
enableLargerThanScreen: false,
opacity: 1
}
}
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,
},
};
},
computed: {
availableActions() {
return [
{ 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' }
];
}
},
methods: {
updateConfigs(newConfigs) {
this.configs = newConfigs;
},
removeAction(action) {
const index = this.selectedActions.findIndex(a => a.value === action.value);
const index = this.selectedActions.findIndex(
(a) => a.value === action.value
);
if (index > -1) {
this.selectedActions.splice(index, 1);
}
},
generateCode() {
let code = 'utools.ubrowser';
let code = "utools.ubrowser";
//
if (this.configs.useragent.value) {
@ -271,216 +251,235 @@ export default defineComponent({
}
if (this.configs.goto.headers.userAgent) {
gotoOptions.headers = gotoOptions.headers || {};
gotoOptions.headers['User-Agent'] = this.configs.goto.headers.userAgent;
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)}` : ''})`;
code += `.goto('${this.configs.goto.url}'${
Object.keys(gotoOptions).length
? `, ${JSON.stringify(gotoOptions)}`
: ""
})`;
}
//
this.selectedActions.forEach(action => {
this.selectedActions.forEach((action) => {
const config = this.configs[action.value];
switch (action.value) {
case 'wait':
case "wait":
if (config.value) {
code += `.wait('${config.value}'${config.timeout !== 60000 ? `, ${config.timeout}` : ''})`;
code += `.wait('${config.value}'${
config.timeout !== 60000 ? `, ${config.timeout}` : ""
})`;
}
break;
case 'click':
case "click":
if (config.selector) {
code += `.click('${config.selector}')`;
}
break;
case 'css':
case "css":
if (config.value) {
code += `.css('${config.value}')`;
}
break;
case 'press':
case "press":
if (config.key) {
const modifiers = config.modifiers.length ? `, ${JSON.stringify(config.modifiers)}` : '';
const modifiers = config.modifiers.length
? `, ${JSON.stringify(config.modifiers)}`
: "";
code += `.press('${config.key}'${modifiers})`;
}
break;
case 'paste':
case "paste":
if (config.text) {
code += `.paste('${config.text}')`;
}
break;
case 'screenshot':
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)}` : ''})`;
code += `.screenshot('${config.savePath}'${
Object.keys(options).length
? `, ${JSON.stringify(options)}`
: ""
})`;
}
break;
case 'pdf':
case "pdf":
if (config.savePath) {
code += `.pdf('${config.savePath}'${config.options ? `, ${JSON.stringify(config.options)}` : ''})`;
code += `.pdf('${config.savePath}'${
config.options ? `, ${JSON.stringify(config.options)}` : ""
})`;
}
break;
case 'device':
case "device":
if (config.size.width && config.size.height) {
const options = {
size: config.size
size: config.size,
};
if (config.useragent) options.useragent = config.useragent;
code += `.device(${JSON.stringify(options)})`;
}
break;
case 'cookies':
case "cookies":
if (config.name) {
code += `.cookies('${config.name}')`;
}
break;
case 'setCookies':
case "setCookies":
if (config.items?.length) {
code += `.setCookies(${JSON.stringify(config.items)})`;
}
break;
case 'removeCookies':
case "removeCookies":
if (config.name) {
code += `.removeCookies('${config.name}')`;
}
break;
case 'clearCookies':
code += `.clearCookies(${config.url ? `'${config.url}'` : ''})`;
case "clearCookies":
code += `.clearCookies(${config.url ? `'${config.url}'` : ""})`;
break;
case 'evaluate':
case "evaluate":
if (config.function) {
const params = config.params.length ? `, ${JSON.stringify(config.params)}` : '';
const params = config.params.length
? `, ${JSON.stringify(config.params)}`
: "";
code += `.evaluate(\`${config.function}\`${params})`;
}
break;
case 'when':
case "when":
if (config.condition) {
code += `.when('${config.condition}')`;
}
break;
case 'mousedown':
case 'mouseup':
case "mousedown":
case "mouseup":
if (config.selector) {
code += `.${action.value}('${config.selector}')`;
}
break;
case 'file':
case "file":
if (config.selector && config.files?.length) {
code += `.file('${config.selector}', ${JSON.stringify(config.files)})`;
code += `.file('${config.selector}', ${JSON.stringify(
config.files
)})`;
}
break;
case 'value':
case "value":
if (config.selector) {
code += `.value('${config.selector}', '${config.value}')`;
}
break;
case 'check':
case "check":
if (config.selector) {
code += `.check('${config.selector}'${config.checked !== undefined ? `, ${config.checked}` : ''})`;
code += `.check('${config.selector}'${
config.checked !== undefined ? `, ${config.checked}` : ""
})`;
}
break;
case 'focus':
case "focus":
if (config.selector) {
code += `.focus('${config.selector}')`;
}
break;
case 'scroll':
if (config.x !== undefined || config.y !== undefined) {
const options = {};
if (config.target) options.target = config.target;
if (config.x !== undefined) options.x = config.x;
if (config.y !== undefined) options.y = config.y;
code += `.scroll(${JSON.stringify(options)})`;
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':
case "download":
if (config.url) {
code += `.download('${config.url}'${config.savePath ? `, '${config.savePath}'` : ''})`;
code += `.download('${config.url}'${
config.savePath ? `, '${config.savePath}'` : ""
})`;
}
break;
case 'hide':
case 'show':
case 'devTools':
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) {
if (
value !== undefined &&
value !== null &&
value !== this.defaultRunConfigs[key]
) {
runOptions[key] = value;
}
});
code += `.run(${Object.keys(runOptions).length ? JSON.stringify(runOptions) : ''})`;
code += `.run(${
Object.keys(runOptions).length ? JSON.stringify(runOptions) : ""
})`;
this.$emit('update:modelValue', code);
}
this.$emit("update:modelValue", code);
},
},
watch: {
configs: {
deep: true,
handler() {
this.generateCode();
}
},
},
selectedActions: {
handler() {
this.generateCode();
}
},
},
step: {
handler() {
this.generateCode();
}
}
}
},
},
},
});
</script>
<style scoped>
.ubrowser-editor {
width: 100%;
}
.ubrowser-stepper {
box-shadow: none;
background-color: rgba(255, 255, 255, 0.8);
}
.body--dark .ubrowser-stepper {
background-color: rgba(255, 255, 255, 0.05);
}
.ubrowser-stepper :deep(.q-stepper__header) {
cursor: pointer;
}
</style>

View File

@ -1,54 +1,76 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col-12">
<q-list separator class="operation-list">
<!-- 操作选择网格 -->
<div class="row q-col-gutter-xs">
<div
v-for="action in availableActions"
:key="action.value"
class="col-2"
>
<q-card
flat
bordered
class="action-card cursor-pointer"
:class="{
'action-selected': selectedActions.some(
(a) => a.value === action.value
),
}"
@click="toggleAction(action)"
>
<div class="q-pa-xs text-caption text-wrap text-center">
{{ action.label }}
</div>
</q-card>
</div>
</div>
<!-- 已选操作列表 -->
<q-list separator class="operation-list q-mt-md">
<div
v-for="(action, index) in selectedActions"
:key="action.value"
:key="action.id"
class="operation-item"
>
<div class="row items-center justify-between">
<div class="row items-center">
<q-icon
:name="getActionIcon(action.value)"
size="xs"
class="q-mx-sm"
<q-chip
square
removable
@remove="$emit('remove-action', action)"
class="text-caption q-mx-none q-mb-sm"
>
<q-avatar color="primary">
<q-icon
color="white"
:name="getActionIcon(action.value)"
size="14px"
/>
</q-avatar>
<div class="q-mx-sm">{{ action.label }}</div>
</q-chip>
<div class="row items-start q-gutter-xs">
<q-btn
round
dense
color="primary"
icon="north"
v-show="index !== 0"
@click="moveAction(index, -1)"
size="xs"
class="q-mb-xs move-btn"
/>
<q-btn
round
dense
color="primary"
icon="south"
v-show="index !== selectedActions.length - 1"
@click="moveAction(index, 1)"
size="xs"
class="move-btn"
/>
<div class="text-subtitle1">{{ action.label }}</div>
<div class="row items-center q-ml-md">
<q-btn
flat
round
dense
icon="north"
:disable="index === 0"
@click="moveAction(index, -1)"
size="xs"
class="q-mb-xs move-btn"
/>
<q-btn
flat
round
dense
icon="south"
:disable="index === selectedActions.length - 1"
@click="moveAction(index, 1)"
size="xs"
class="move-btn"
/>
</div>
</div>
<q-btn
flat
round
dense
icon="delete"
color="negative"
size="sm"
@click="$emit('remove-action', action)"
class="delete-btn"
/>
</div>
<div v-if="getOperationConfig(action.value)">
<UBrowserOperation
@ -66,6 +88,10 @@
<script>
import { defineComponent } from "vue";
import {
ubrowserActionIcons,
ubrowserAvailableActions,
} from "../composerConfig";
import UBrowserOperation from "./operations/UBrowserOperation.vue";
export default defineComponent({
@ -83,7 +109,12 @@ export default defineComponent({
required: true,
},
},
emits: ["remove-action", "update:selectedActions"],
emits: ["remove-action", "update:selectedActions", "update:configs"],
computed: {
availableActions() {
return ubrowserAvailableActions;
},
},
methods: {
moveAction(index, direction) {
const newIndex = index + direction;
@ -96,31 +127,7 @@ export default defineComponent({
}
},
getActionIcon(action) {
const iconMap = {
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",
};
return iconMap[action] || "touch_app";
return ubrowserActionIcons[action] || "touch_app";
},
getOperationConfig(action) {
const configs = {
@ -130,6 +137,7 @@ export default defineComponent({
label: "等待时间(ms)或CSS选择器",
icon: "timer",
type: "input",
width: 8,
},
{
key: "timeout",
@ -137,6 +145,7 @@ export default defineComponent({
icon: "timer_off",
type: "input",
inputType: "number",
width: 4,
},
],
click: [
@ -156,9 +165,16 @@ export default defineComponent({
},
],
press: [
{ key: "key", label: "按键", icon: "keyboard", type: "input" },
{
key: "key",
label: "按键",
icon: "keyboard",
type: "input",
width: 5,
},
{
key: "modifiers",
label: "修饰键",
type: "checkbox-group",
options: [
{ label: "Ctrl", value: "ctrl" },
@ -166,6 +182,8 @@ export default defineComponent({
{ label: "Alt", value: "alt" },
{ label: "Meta", value: "meta" },
],
defaultValue: [],
width: 7,
},
],
paste: [
@ -202,7 +220,7 @@ export default defineComponent({
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
width: 3,
},
{
key: "rect.y",
@ -210,7 +228,7 @@ export default defineComponent({
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
width: 3,
},
{
key: "rect.width",
@ -218,7 +236,7 @@ export default defineComponent({
icon: "width",
type: "input",
inputType: "number",
width: 6,
width: 3,
},
{
key: "rect.height",
@ -226,7 +244,7 @@ export default defineComponent({
icon: "height",
type: "input",
inputType: "number",
width: 6,
width: 3,
},
{ key: "savePath", label: "保存路径", icon: "save", type: "input" },
],
@ -240,12 +258,14 @@ export default defineComponent({
{ 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" },
],
@ -325,7 +345,7 @@ export default defineComponent({
icon: "upload_file",
type: "input",
},
{ key: "files", label: "文件列表", type: "file-list" },
{ key: "files", label: "文件列表", type: "file-list", width: 12 },
],
value: [
{
@ -333,8 +353,15 @@ export default defineComponent({
label: "元素选择器",
icon: "input",
type: "input",
width: 6,
},
{
key: "value",
label: "设置的值",
icon: "edit",
type: "input",
width: 6,
},
{ key: "value", label: "设置的值", icon: "edit", type: "input" },
],
check: [
{
@ -342,8 +369,15 @@ export default defineComponent({
label: "复选框/选框选择器",
icon: "check_box",
type: "input",
width: 8,
},
{
key: "checked",
label: "选中状态",
type: "checkbox",
defaultValue: false,
width: 4,
},
{ key: "checked", label: "选中状态", type: "checkbox" },
],
focus: [
{
@ -355,10 +389,23 @@ export default defineComponent({
],
scroll: [
{
key: "target",
label: "目标元素选择器(可选)",
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",
@ -367,6 +414,8 @@ export default defineComponent({
type: "input",
inputType: "number",
width: 6,
showWhen: "type",
showValue: "position",
},
{
key: "y",
@ -375,15 +424,84 @@ export default defineComponent({
type: "input",
inputType: "number",
width: 6,
showWhen: "type",
showValue: "position",
},
],
download: [
{ key: "url", label: "下载URL", icon: "link", type: "input" },
{ key: "savePath", label: "保存路径", icon: "save", type: "input" },
{
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
);
if (index === -1) {
//
this.$emit("update:selectedActions", [
...this.selectedActions,
{
...action,
id: Date.now(),
argv: "",
saveOutput: false,
useOutput: null,
cmd: action.value || action.cmd,
value: action.value || action.cmd,
},
]);
//
const config = this.getOperationConfig(action.value);
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];
newActions.splice(index, 1);
this.$emit("update:selectedActions", newActions);
}
},
},
});
</script>
@ -410,12 +528,7 @@ export default defineComponent({
}
.operation-item:hover {
background: rgba(0, 0, 0, 0.25);
}
.body--dark .operation-item {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.15);
background: rgba(0, 0, 0, 0.05);
}
.body--dark .operation-item:hover {
@ -452,4 +565,51 @@ export default defineComponent({
.operation-item:hover .q-item-section {
opacity: 1;
}
.action-card {
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
/* min-height: 42px; */
}
.action-card:hover {
transform: translateY(-1px);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
background: var(--q-primary-opacity-5);
}
.action-selected {
border-color: var(--q-primary);
background: var(--q-primary-opacity-10);
}
.body--dark .action-selected {
background: var(--q-primary-opacity-40);
}
.body--dark .action-card {
border-color: rgba(255, 255, 255, 0.1);
}
.body--dark .action-card:hover {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
background: var(--q-primary-opacity-20);
}
.text-caption {
font-size: 11px;
line-height: 1.1;
}
.q-card__section {
padding: 4px !important;
}
.row.q-col-gutter-xs {
margin: -2px;
}
.row.q-col-gutter-xs > * {
padding: 2px;
}
</style>

View File

@ -1,201 +1,248 @@
<template>
<div class="row q-col-gutter-sm">
<!-- 基础设置 -->
<!-- 窗口显示控制 -->
<div class="col-12">
<q-checkbox
:value="configs.run.show"
@update:modelValue="updateConfig('run.show', $event)"
label="显示窗口"
/>
</div>
<!-- 窗口大小 -->
<div class="col-12">
<div class="text-subtitle2 q-mb-sm">窗口大小</div>
<div class="row q-col-gutter-sm">
<UBrowserInput
:value="configs.run.width"
@update:modelValue="updateConfig('run.width', $event)"
type="number"
label="宽度"
:width="6"
icon="width"
<div class="row items-center q-gutter-x-md">
<q-checkbox
:model-value="localConfigs.run.show"
label="显示窗口"
@update:model-value="updateConfig('show', $event)"
/>
<UBrowserInput
:value="configs.run.height"
@update:modelValue="updateConfig('run.height', $event)"
type="number"
label="高度"
:width="6"
icon="height"
<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)"
/>
</div>
</div>
<!-- 窗口位置 -->
<!-- 窗口尺寸和位置 -->
<div class="col-12">
<div class="text-subtitle2 q-mb-sm">窗口位置</div>
<div class="row q-col-gutter-sm">
<UBrowserInput
:value="configs.run.x"
@update:modelValue="updateConfig('run.x', $event)"
type="number"
label="X坐标"
:width="6"
icon="drag_handle"
<div class="col-3">
<q-input
v-model.number="localConfigs.run.width"
type="number"
label="窗口宽度"
dense
outlined
@update:model-value="updateConfig('width', $event)"
/>
</div>
<div class="col-3">
<q-input
v-model.number="localConfigs.run.height"
type="number"
label="窗口高度"
dense
outlined
@update:model-value="updateConfig('height', $event)"
/>
</div>
<div class="col-3">
<q-input
v-model.number="localConfigs.run.x"
type="number"
label="X坐标"
dense
outlined
@update:model-value="updateConfig('x', $event)"
/>
</div>
<div class="col-3">
<q-input
v-model.number="localConfigs.run.y"
type="number"
label="Y坐标"
dense
outlined
@update:model-value="updateConfig('y', $event)"
/>
</div>
</div>
</div>
<!-- 最大最小尺寸 -->
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-3">
<q-input
v-model.number="localConfigs.run.minWidth"
type="number"
label="最小宽度"
dense
outlined
@update:model-value="updateConfig('minWidth', $event)"
/>
</div>
<div class="col-3">
<q-input
v-model.number="localConfigs.run.minHeight"
type="number"
label="最小高度"
dense
outlined
@update:model-value="updateConfig('minHeight', $event)"
/>
</div>
<div class="col-3">
<q-input
v-model.number="localConfigs.run.maxWidth"
type="number"
label="最大宽度"
dense
outlined
@update:model-value="updateConfig('maxWidth', $event)"
/>
</div>
<div class="col-3">
<q-input
v-model.number="localConfigs.run.maxHeight"
type="number"
label="最大高度"
dense
outlined
@update:model-value="updateConfig('maxHeight', $event)"
/>
</div>
</div>
</div>
<!-- 窗口行为控制 -->
<div class="col-12">
<div class="row items-center q-gutter-x-md">
<q-checkbox
:model-value="localConfigs.run.resizable"
label="可调整大小"
@update:model-value="updateConfig('resizable', $event)"
/>
<UBrowserInput
:value="configs.run.y"
@update:modelValue="updateConfig('run.y', $event)"
type="number"
label="Y坐标"
:width="6"
icon="drag_handle"
<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)"
/>
</div>
</div>
<!-- 窗口限制 -->
<!-- 特殊功能控-->
<div class="col-12">
<div class="text-subtitle2 q-mb-sm">窗口限制</div>
<div class="row q-col-gutter-sm">
<UBrowserInput
:value="configs.run.minWidth"
@update:modelValue="updateConfig('run.minWidth', $event)"
type="number"
label="最小宽度"
:width="6"
icon="width"
<div class="row items-center q-gutter-x-md">
<q-checkbox
:model-value="localConfigs.run.enableLargerThanScreen"
label="允许超出屏幕"
@update:model-value="updateConfig('enableLargerThanScreen', $event)"
/>
<UBrowserInput
:value="configs.run.minHeight"
@update:modelValue="updateConfig('run.minHeight', $event)"
type="number"
label="最小高度"
:width="6"
icon="height"
/>
<UBrowserInput
:value="configs.run.maxWidth"
@update:modelValue="updateConfig('run.maxWidth', $event)"
type="number"
label="最大宽度"
:width="6"
icon="width"
/>
<UBrowserInput
:value="configs.run.maxHeight"
@update:modelValue="updateConfig('run.maxHeight', $event)"
type="number"
label="最大高度"
:width="6"
icon="height"
<q-checkbox
:model-value="localConfigs.run.fullscreenable"
label="允许全屏"
@update:model-value="updateConfig('fullscreenable', $event)"
/>
</div>
</div>
<!-- 窗口行为 -->
<!-- 透明度控制 -->
<div class="col-12">
<div class="text-subtitle2 q-mb-sm">窗口行为</div>
<div class="row q-col-gutter-sm">
<div class="col-12">
<q-checkbox
:value="configs.run.center"
@update:modelValue="updateConfig('run.center', $event)"
label="居中显示"
/>
</div>
<div class="col-12">
<q-checkbox
:value="configs.run.resizable"
@update:modelValue="updateConfig('run.resizable', $event)"
label="允许调整大小"
/>
</div>
<div class="col-12">
<q-checkbox
:value="configs.run.movable"
@update:modelValue="updateConfig('run.movable', $event)"
label="允许移动"
/>
</div>
<div class="col-12">
<q-checkbox
:value="configs.run.minimizable"
@update:modelValue="updateConfig('run.minimizable', $event)"
label="允许最小化"
/>
</div>
<div class="col-12">
<q-checkbox
:value="configs.run.maximizable"
@update:modelValue="updateConfig('run.maximizable', $event)"
label="允许最大化"
/>
</div>
<div class="col-12">
<q-checkbox
:value="configs.run.alwaysOnTop"
@update:modelValue="updateConfig('run.alwaysOnTop', $event)"
label="总是置顶"
/>
</div>
<div class="col-12">
<q-checkbox
:value="configs.run.fullscreen"
@update:modelValue="updateConfig('run.fullscreen', $event)"
label="全屏显示"
/>
</div>
<div class="col-12">
<q-checkbox
:value="configs.run.fullscreenable"
@update:modelValue="updateConfig('run.fullscreenable', $event)"
label="允许全屏"
/>
</div>
<div class="col-12">
<q-checkbox
:value="configs.run.enableLargerThanScreen"
@update:modelValue="updateConfig('run.enableLargerThanScreen', $event)"
label="允许超出屏幕大小"
/>
</div>
<div class="row items-center">
<div class="q-mr-md">透明度</div>
<q-slider
class="col"
v-model="localConfigs.run.opacity"
:min="0"
:max="1"
:step="0.1"
label
label-always
color="primary"
@update:model-value="updateConfig('opacity', $event)"
>
<template v-slot:thumb-label>
{{ localConfigs.run.opacity.toFixed(1) }}
</template>
</q-slider>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import UBrowserInput from './operations/UBrowserInput.vue';
import { defineComponent } from "vue";
export default defineComponent({
name: 'UBrowserRun',
components: {
UBrowserInput
},
name: "UBrowserRun",
props: {
configs: {
type: Object,
required: true
}
required: true,
},
},
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,
},
},
};
},
created() {
//
this.localConfigs = JSON.parse(JSON.stringify(this.configs));
},
emits: ['update:configs'],
methods: {
updateConfig(path, value) {
const newConfigs = { ...this.configs };
const keys = path.split('.');
let current = newConfigs;
for (let i = 0; i < keys.length - 1; i++) {
current[keys[i]] = { ...current[keys[i]] };
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
this.$emit('update:configs', newConfigs);
}
}
updateConfig(key, value) {
this.localConfigs.run[key] = value;
this.$emit(
"update:configs",
JSON.parse(JSON.stringify(this.localConfigs))
);
},
},
watch: {
configs: {
deep: true,
handler(newConfigs) {
this.localConfigs = JSON.parse(JSON.stringify(newConfigs));
},
},
},
});
</script>

View File

@ -1,189 +1,258 @@
<template>
<div class="operation-item-argv q-mb-md">
<div class="text-subtitle2 q-mb-sm">{{ title }}</div>
<div class="row q-col-gutter-sm">
<template v-for="(field, index) in fields" :key="index">
<!-- 输入框 -->
<template v-if="field.type === 'input'">
<UBrowserInput
:value="getFieldValue(field.key)"
@update:modelValue="updateFieldValue(field.key, $event)"
:label="field.label"
:icon="field.icon"
:type="field.inputType || 'text'"
:width="field.width"
v-bind="field.props || {}"
/>
</template>
<!-- 选择框 -->
<template v-if="field.type === 'select'">
<q-select
:value="getFieldValue(field.key)"
@update:modelValue="updateFieldValue(field.key, $event)"
:options="field.options"
:label="field.label"
outlined
dense
class="col-12"
v-bind="field.props || {}"
/>
<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'">
<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>
</template>
<!-- 单个复选框 -->
<template v-if="field.type === 'checkbox'">
<div class="col-12">
<q-checkbox
:value="getFieldValue(field.key)"
@update:modelValue="updateFieldValue(field.key, $event)"
:label="field.label"
<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>
</template>
<!-- 复选框组 -->
<template v-if="field.type === 'checkbox-group'">
<q-option-group
:value="getFieldValue(field.key)"
@update:modelValue="updateFieldValue(field.key, $event)"
:options="field.options"
type="checkbox"
inline
class="col-12"
/>
<!-- 文本输入 -->
<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>
<!-- 文本域 -->
<template v-if="field.type === 'textarea'">
<UBrowserInput
:value="getFieldValue(field.key)"
@update:modelValue="updateFieldValue(field.key, $event)"
<!-- 文本区域 -->
<template v-else-if="field.type === 'textarea'">
<q-input
:model-value="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
type="textarea"
dense
outlined
autogrow
class="col-12"
/>
@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]"
:label="field.label"
: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-if="field.type === 'cookie-list'">
<div class="col-12">
<template v-else-if="field.type === 'cookie-list'">
<div class="row q-col-gutter-sm">
<div
v-for="(item, idx) in getFieldValue(field.key)"
:key="idx"
class="row q-col-gutter-sm q-mb-sm"
v-for="(cookie, index) in fieldValue[field.key] || [{}]"
:key="index"
class="col-12"
>
<UBrowserInput
:value="item.name"
@update:modelValue="
updateCookieField(field.key, idx, 'name', $event)
"
label="名称"
:width="5"
icon="label"
/>
<UBrowserInput
:value="item.value"
@update:modelValue="
updateCookieField(field.key, idx, 'value', $event)
"
label="值"
:width="5"
icon="edit"
/>
<div class="col-2 flex items-center">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeCookie(field.key, idx)"
v-if="getFieldValue(field.key).length > 1"
/>
<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>
<q-btn
outline
color="primary"
label="添加Cookie"
icon="add"
@click="addCookie(field.key)"
class="q-mt-sm"
/>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加Cookie"
@click="addCookie(field.key)"
class="q-mt-xs"
/>
</template>
<!-- 参数列表 -->
<template v-if="field.type === 'param-list'">
<div class="col-12">
<div class="row items-center q-gutter-sm q-mb-sm">
<UBrowserInput
v-model="newParam"
label="参数"
:width="10"
icon="functions"
<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])
"
/>
<q-btn flat round dense icon="add" @click="addParam(field.key)" />
</div>
<div v-if="getFieldValue(field.key).length > 0" class="q-mt-sm">
<q-chip
v-for="(param, idx) in getFieldValue(field.key)"
:key="idx"
removable
@remove="removeParam(field.key, idx)"
>
{{ param }}
</q-chip>
<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>
<!-- 文件列表 -->
<template v-if="field.type === 'file-list'">
<div class="col-12">
<div class="row items-center q-gutter-sm q-mb-sm">
<UBrowserInput
v-model="newFile"
label="文件路径"
:width="10"
icon="upload_file"
/>
<q-btn flat round dense icon="add" @click="addFile(field.key)" />
</div>
<div v-if="getFieldValue(field.key).length > 0" class="q-mt-sm">
<q-chip
v-for="(file, idx) in getFieldValue(field.key)"
:key="idx"
removable
@remove="removeFile(field.key, idx)"
>
{{ file }}
</q-chip>
<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"
/>
</template>
</template>
</div>
<!-- 按钮组 -->
<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>
</div>
</template>
</div>
</template>
<script>
import { defineComponent } from "vue";
import UBrowserInput from "./UBrowserInput.vue";
import { get, set } from "lodash";
export default defineComponent({
name: "UBrowserOperation",
components: {
UBrowserInput,
},
data() {
return {
newParam: "",
newFile: "",
};
},
props: {
configs: {
type: Object,
@ -193,78 +262,125 @@ export default defineComponent({
type: String,
required: true,
},
title: {
type: String,
},
fields: {
type: Array,
required: true,
},
},
emits: ["update:configs"],
data() {
return {
fieldValue: {},
};
},
created() {
//
this.fields.forEach((field) => {
const value = get(this.configs[this.action], field.key);
//
let defaultValue;
if (field.type === "checkbox-group") {
defaultValue = field.defaultValue || [];
} else if (field.type === "checkbox") {
defaultValue = field.defaultValue || false;
} else {
defaultValue = field.defaultValue;
}
this.fieldValue[field.key] = value !== undefined ? value : defaultValue;
});
},
methods: {
updateValue(key, value) {
//
this.fieldValue[key] = value;
//
const newConfigs = { ...this.configs };
if (!newConfigs[this.action]) {
newConfigs[this.action] = {};
}
// 使 lodash set
set(newConfigs[this.action], key, value);
//
this.$emit("update:configs", newConfigs);
},
// Cookie
addCookie(key) {
const items = [...(this.getFieldValue(key) || [])];
items.push({ name: "", value: "" });
this.updateFieldValue(key, items);
if (!this.fieldValue[key]) {
this.fieldValue[key] = [];
}
this.fieldValue[key].push({ name: "", value: "" });
this.updateValue(key, this.fieldValue[key]);
},
removeCookie(key, index) {
const items = [...this.getFieldValue(key)];
items.splice(index, 1);
this.updateFieldValue(key, items);
},
updateCookieField(key, index, field, value) {
const items = [...this.getFieldValue(key)];
items[index] = {
...items[index],
[field]: value,
};
this.updateFieldValue(key, items);
},
addParam(key) {
if (this.newParam) {
const params = [...(this.getFieldValue(key) || [])];
params.push(this.newParam);
this.updateFieldValue(key, params);
this.newParam = "";
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) {
const params = [...this.getFieldValue(key)];
params.splice(index, 1);
this.updateFieldValue(key, params);
this.fieldValue[key].splice(index, 1);
this.updateValue(key, this.fieldValue[key]);
},
//
addFile(key) {
if (this.newFile) {
const files = [...(this.getFieldValue(key) || [])];
files.push(this.newFile);
this.updateFieldValue(key, files);
this.newFile = "";
if (!this.fieldValue[key]) {
this.fieldValue[key] = [];
}
this.fieldValue[key].push("");
this.updateValue(key, this.fieldValue[key]);
},
removeFile(key, index) {
const files = [...this.getFieldValue(key)];
files.splice(index, 1);
this.updateFieldValue(key, files);
this.fieldValue[key].splice(index, 1);
this.updateValue(key, this.fieldValue[key]);
},
getFieldValue(key) {
return this.configs[this.action][key];
},
updateFieldValue(key, value) {
this.$emit("update:configs", {
...this.configs,
[this.action]: {
...this.configs[this.action],
[key]: value,
},
});
},
watch: {
//
configs: {
deep: true,
handler() {
this.fields.forEach((field) => {
const value = get(this.configs[this.action], field.key);
if (value !== undefined) {
this.fieldValue[field.key] = value;
}
});
},
},
},
});
</script>
<style scoped>
.operation-item-argv {
padding: 0 4px;
.button-group-container {
position: relative;
}
.button-group {
flex: 1;
padding: 0 10px;
}
.button-group :deep(.q-btn) {
min-height: 24px;
font-size: 12px;
}
</style>