编排界面添加ubrowser

This commit is contained in:
fofolee 2024-12-23 00:51:03 +08:00
parent 29ceb3c7ff
commit 52fc7f408d
9 changed files with 1587 additions and 15 deletions

View File

@ -23,17 +23,10 @@
</div>
<!-- 固定底部 -->
<div class="composer-footer q-pa-sm row items-center justify-end">
<q-btn
outline
color="primary"
label="运行"
icon="play_arrow"
class="q-mr-sm"
@click="runCommands"
/>
<q-btn flat label="取消" v-close-popup />
<q-btn unelevated color="primary" label="确认" @click="applyCommands" />
<div class="composer-footer q-pa-sm q-gutter-sm row justify-end">
<q-btn label="取消" v-close-popup />
<q-btn color="primary" label="确认" @click="applyCommands" />
<q-btn color="positive" label="运行" @click="runCommands" />
</div>
</div>
</template>

View File

@ -68,6 +68,13 @@
<template v-else-if="command.hasKeyRecorder">
<KeyEditor v-model="argvLocal" class="col" />
</template>
<!-- UBrowser编辑器 -->
<template v-else-if="command.hasUBrowserEditor">
<UBrowserEditor
v-model="argvLocal"
class="col"
/>
</template>
<!-- 普通参数输入 -->
<template v-else>
<q-input
@ -92,11 +99,13 @@
<script>
import { defineComponent } from "vue";
import KeyEditor from "./KeyEditor.vue";
import UBrowserEditor from './ubrowser/UBrowserEditor.vue';
export default defineComponent({
name: "ComposerCard",
components: {
KeyEditor,
UBrowserEditor
},
props: {
command: {
@ -208,10 +217,10 @@ export default defineComponent({
}
/* 拖拽动画 */
.composer-card:active {
transform: scale(1.02);
transition: transform 0.2s;
}
/* .composer-card:active { */
/* transform: scale(1.02); */
/* transition: transform 0.2s; */
/* } */
.command-item {
transition: all 0.3s ease;

View File

@ -45,6 +45,12 @@ export const commandCategories = [
value: "utools.ubrowser.goto",
label: "用ubrowser打开网址",
desc: "要访问的网址链接",
},
{
value: "ubrowser",
label: "UBrowser浏览器操作",
desc: "配置UBrowser浏览器操作",
hasUBrowserEditor: true
}
]
},
@ -121,6 +127,7 @@ export const commandsWithOutput = {
'open': true,
'locate': true,
'copyTo': true,
'ubrowser': true,
}
// 定义哪些命令可以接收输出

View File

@ -0,0 +1,86 @@
<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"
/>
</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)"
type="number"
label="超时时间(ms)"
icon="timer"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import UBrowserInput from './operations/UBrowserInput.vue';
export default defineComponent({
name: 'UBrowserBasic',
components: {
UBrowserInput
},
props: {
configs: {
type: Object,
required: true
}
},
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);
}
}
});
</script>

View File

@ -0,0 +1,486 @@
<template>
<div class="ubrowser-editor">
<q-stepper
v-model="step"
vertical
color="primary"
header-nav
animated
alternative-labels
flat
class="ubrowser-stepper"
>
<!-- 基础参数步骤 -->
<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>
<!-- 运行参数步骤 -->
<q-step
:name="3"
title="运行参数"
icon="settings_applications"
class="q-pb-md"
>
<UBrowserRun
:configs="configs"
@update:configs="updateConfigs"
/>
</q-step>
</q-stepper>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import UBrowserBasic from './UBrowserBasic.vue';
import UBrowserOperations from './UBrowserOperations.vue';
import UBrowserRun from './UBrowserRun.vue';
export default defineComponent({
name: 'UBrowserEditor',
components: {
UBrowserBasic,
UBrowserOperations,
UBrowserRun
},
props: {
modelValue: {
type: String,
default: ''
}
},
emits: ['update:modelValue'],
data() {
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
}
}
};
},
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);
if (index > -1) {
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.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)})`;
}
break;
case 'download':
if (config.url) {
code += `.download('${config.url}'${config.savePath ? `, '${config.savePath}'` : ''})`;
}
break;
case 'hide':
case 'show':
case 'devTools':
code += `.${action.value}()`;
break;
}
});
//
const runOptions = {};
Object.entries(this.configs.run).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
runOptions[key] = value;
}
});
code += `.run(${Object.keys(runOptions).length ? JSON.stringify(runOptions) : ''})`;
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

@ -0,0 +1,455 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col-12">
<q-list separator class="operation-list">
<div
v-for="(action, index) in selectedActions"
:key="action.value"
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"
color="primary"
/>
<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
:configs="configs"
:action="action.value"
:fields="getOperationConfig(action.value)"
@update:configs="$emit('update:configs', $event)"
/>
</div>
</div>
</q-list>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import UBrowserOperation from "./operations/UBrowserOperation.vue";
export default defineComponent({
name: "UBrowserOperations",
components: {
UBrowserOperation,
},
props: {
configs: {
type: Object,
required: true,
},
selectedActions: {
type: Array,
required: true,
},
},
emits: ["remove-action", "update:selectedActions"],
methods: {
moveAction(index, direction) {
const newIndex = index + direction;
if (newIndex >= 0 && newIndex < this.selectedActions.length) {
const actions = [...this.selectedActions];
const temp = actions[index];
actions[index] = actions[newIndex];
actions[newIndex] = temp;
this.$emit("update:selectedActions", actions);
}
},
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";
},
getOperationConfig(action) {
const configs = {
wait: [
{
key: "value",
label: "等待时间(ms)或CSS选择器",
icon: "timer",
type: "input",
},
{
key: "timeout",
label: "超时时间(ms)",
icon: "timer_off",
type: "input",
inputType: "number",
},
],
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" },
{
key: "modifiers",
type: "checkbox-group",
options: [
{ label: "Ctrl", value: "ctrl" },
{ label: "Shift", value: "shift" },
{ label: "Alt", value: "alt" },
{ label: "Meta", value: "meta" },
],
},
],
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: 6,
},
{
key: "rect.y",
label: "Y坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
},
{
key: "rect.width",
label: "宽度",
icon: "width",
type: "input",
inputType: "number",
width: 6,
},
{
key: "rect.height",
label: "高度",
icon: "height",
type: "input",
inputType: "number",
width: 6,
},
{ key: "savePath", label: "保存路径", icon: "save", type: "input" },
],
pdf: [
{
key: "options.marginsType",
label: "边距类型",
type: "select",
options: [
{ label: "默认边距", value: 0 },
{ label: "无边距", value: 1 },
{ label: "最小边距", value: 2 },
],
},
{
key: "options.pageSize",
label: "页面大小",
type: "select",
options: ["A3", "A4", "A5", "Legal", "Letter", "Tabloid"],
},
{ 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" },
],
value: [
{
key: "selector",
label: "元素选择器",
icon: "input",
type: "input",
},
{ key: "value", label: "设置的值", icon: "edit", type: "input" },
],
check: [
{
key: "selector",
label: "复选框/选框选择器",
icon: "check_box",
type: "input",
},
{ key: "checked", label: "选中状态", type: "checkbox" },
],
focus: [
{
key: "selector",
label: "元素选择器",
icon: "center_focus_strong",
type: "input",
},
],
scroll: [
{
key: "target",
label: "目标元素选择器(可选)",
icon: "swap_vert",
type: "input",
},
{
key: "x",
label: "X坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
},
{
key: "y",
label: "Y坐标",
icon: "drag_handle",
type: "input",
inputType: "number",
width: 6,
},
],
download: [
{ key: "url", label: "下载URL", icon: "link", type: "input" },
{ key: "savePath", label: "保存路径", icon: "save", type: "input" },
],
};
return configs[action];
},
},
});
</script>
<style scoped>
.operation-list {
min-height: 50px;
display: flex;
flex-direction: column;
gap: 4px;
}
.operation-list :deep(.q-field) div,
.operation-list :deep(div.q-checkbox__label) {
font-size: 12px !important;
}
.operation-item {
transition: all 0.3s;
border-radius: 4px;
margin: 0;
padding: 2px 4px;
border-color: rgba(0, 0, 0, 0.15);
}
.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);
}
.body--dark .operation-item:hover {
background: rgba(0, 0, 0, 0.25);
}
.move-btn {
opacity: 0.6;
transition: opacity 0.3s;
}
.operation-item:hover .move-btn {
opacity: 1;
}
.delete-btn {
opacity: 0.6;
transition: opacity 0.3s;
}
.operation-item:hover .delete-btn {
opacity: 1;
}
.text-subtitle2 {
font-size: 0.9rem;
font-weight: 500;
}
.q-item-section {
transition: all 0.3s;
}
.operation-item:hover .q-item-section {
opacity: 1;
}
</style>

View File

@ -0,0 +1,201 @@
<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"
/>
<UBrowserInput
:value="configs.run.height"
@update:modelValue="updateConfig('run.height', $event)"
type="number"
label="高度"
:width="6"
icon="height"
/>
</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"
/>
<UBrowserInput
:value="configs.run.y"
@update:modelValue="updateConfig('run.y', $event)"
type="number"
label="Y坐标"
:width="6"
icon="drag_handle"
/>
</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"
/>
<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"
/>
</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>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import UBrowserInput from './operations/UBrowserInput.vue';
export default defineComponent({
name: 'UBrowserRun',
components: {
UBrowserInput
},
props: {
configs: {
type: Object,
required: true
}
},
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);
}
}
});
</script>

View File

@ -0,0 +1,65 @@
<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>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserInput",
inheritAttrs: false,
props: {
modelValue: {
type: [String, Number],
default: "",
},
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,
},
},
emits: ["update:modelValue"],
methods: {
handleInput(value) {
this.$emit("update:modelValue", value);
},
},
});
</script>

View File

@ -0,0 +1,270 @@
<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 || {}"
/>
</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"
/>
</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>
<!-- 文本域 -->
<template v-if="field.type === 'textarea'">
<UBrowserInput
:value="getFieldValue(field.key)"
@update:modelValue="updateFieldValue(field.key, $event)"
:label="field.label"
:icon="field.icon"
type="textarea"
autogrow
class="col-12"
/>
</template>
<!-- Cookie列表 -->
<template v-if="field.type === 'cookie-list'">
<div class="col-12">
<div
v-for="(item, idx) in getFieldValue(field.key)"
:key="idx"
class="row q-col-gutter-sm q-mb-sm"
>
<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>
</div>
<q-btn
outline
color="primary"
label="添加Cookie"
icon="add"
@click="addCookie(field.key)"
class="q-mt-sm"
/>
</div>
</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"
/>
<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>
</div>
</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>
</div>
</div>
</template>
</template>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import UBrowserInput from "./UBrowserInput.vue";
export default defineComponent({
name: "UBrowserOperation",
components: {
UBrowserInput,
},
data() {
return {
newParam: "",
newFile: "",
};
},
props: {
configs: {
type: Object,
required: true,
},
action: {
type: String,
required: true,
},
title: {
type: String,
},
fields: {
type: Array,
required: true,
},
},
emits: ["update:configs"],
methods: {
addCookie(key) {
const items = [...(this.getFieldValue(key) || [])];
items.push({ name: "", value: "" });
this.updateFieldValue(key, items);
},
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 = "";
}
},
removeParam(key, index) {
const params = [...this.getFieldValue(key)];
params.splice(index, 1);
this.updateFieldValue(key, params);
},
addFile(key) {
if (this.newFile) {
const files = [...(this.getFieldValue(key) || [])];
files.push(this.newFile);
this.updateFieldValue(key, files);
this.newFile = "";
}
},
removeFile(key, index) {
const files = [...this.getFieldValue(key)];
files.splice(index, 1);
this.updateFieldValue(key, files);
},
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,
},
});
},
},
});
</script>
<style scoped>
.operation-item-argv {
padding: 0 4px;
}
</style>