组件位置调整

This commit is contained in:
fofolee
2024-12-30 21:18:59 +08:00
parent c8479373ee
commit 2330638693
30 changed files with 23 additions and 23 deletions

View File

@@ -0,0 +1,129 @@
<template>
<div class="row q-col-gutter-sm">
<!-- 基础配置 -->
<div class="col-12">
<VariableInput
v-model="localConfigs.goto.url"
label="网址"
:command="{ icon: 'link' }"
@update:model-value="updateConfigs"
/>
</div>
<!-- Headers配置 -->
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-12">
<VariableInput
v-model="localConfigs.goto.headers.Referer"
label="Referer"
:command="{ icon: 'link' }"
@update:model-value="updateConfigs"
/>
</div>
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col">
<VariableInput
v-model="localConfigs.goto.headers.userAgent"
label="User-Agent"
:command="{ icon: 'devices' }"
@update:model-value="updateConfigs"
/>
</div>
<div class="col-auto">
<q-select
v-model="selectedUA"
:options="userAgentOptions"
label="常用 UA"
dense
filled
emit-value
map-options
options-dense
style="min-width: 150px"
>
<template v-slot:prepend>
<q-icon name="list" />
</template>
</q-select>
</div>
</div>
</div>
</div>
</div>
<!-- 超时配置 -->
<div class="col-12">
<VariableInput
v-model="localConfigs.goto.timeout"
:command="{ icon: 'timer', inputType: 'number' }"
label="超时时间(ms)"
@update:model-value="updateConfigs"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { userAgent } from "js/options/httpHeaders";
import VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "UBrowserBasic",
components: {
VariableInput,
},
props: {
configs: {
type: Object,
required: true,
},
},
data() {
return {
selectedUA: null,
localConfigs: {
useragent: {
preset: null,
value: "",
},
goto: {
url: "",
headers: {
Referer: "",
userAgent: "",
},
timeout: 60000,
},
},
userAgentOptions: userAgent,
};
},
created() {
// 初始化本地配置
this.localConfigs = window.lodashM.cloneDeep(this.configs);
},
methods: {
updateConfigs() {
this.$emit("update:configs", window.lodashM.cloneDeep(this.localConfigs));
},
},
watch: {
configs: {
deep: true,
handler(newConfigs) {
this.localConfigs = window.lodashM.cloneDeep(newConfigs);
},
},
selectedUA(value) {
if (value) {
this.localConfigs.goto.headers.userAgent = value;
this.updateConfigs();
this.selectedUA = null;
}
},
},
});
</script>

View File

@@ -0,0 +1,143 @@
<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">
<UBrowserOperations
:configs="configs"
@update:configs="updateConfigs"
v-model:selected-actions="selectedActions"
@remove-action="removeAction"
/>
</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>
<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 { defaultUBrowserConfigs } from "js/composer/ubrowserConfig";
import { generateUBrowserCode } from "js/composer/generateUBrowserCode";
export default defineComponent({
name: "UBrowserEditor",
components: {
UBrowserBasic,
UBrowserOperations,
UBrowserRun,
},
props: {
modelValue: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
data() {
return {
step: 1,
selectedActions: [],
configs: window.lodashM.cloneDeep(defaultUBrowserConfigs),
};
},
methods: {
updateConfigs(newConfigs) {
this.configs = newConfigs;
},
removeAction(action) {
const newActions = this.selectedActions.filter((a) => a.id !== action.id);
this.selectedActions = newActions;
const newConfigs = { ...this.configs };
delete newConfigs[action.value];
this.configs = newConfigs;
},
},
watch: {
configs: {
deep: true,
handler() {
this.$emit(
"update:modelValue",
generateUBrowserCode(this.configs, this.selectedActions)
);
},
},
selectedActions: {
handler() {
this.$emit(
"update:modelValue",
generateUBrowserCode(this.configs, this.selectedActions)
);
},
},
step: {
handler() {
this.$emit(
"update:modelValue",
generateUBrowserCode(this.configs, this.selectedActions)
);
},
},
},
});
</script>
<style scoped>
.ubrowser-editor :deep(.q-stepper) {
padding: 0 !important;
}
.ubrowser-editor :deep(.q-stepper__tab) {
padding: 5px 25px;
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col-12">
<!-- 操作选择网格 -->
<div class="row q-col-gutter-xs">
<div
v-for="action in ubrowserOperationConfigs"
: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.id"
class="operation-item"
>
<div class="row items-center justify-between">
<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="getActionProps(action, 'icon') || 'touch_app'"
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>
</div>
<div v-if="getActionProps(action, 'config')">
<UBrowserOperation
:configs="configs"
:action="action.value"
:fields="getActionProps(action, 'config')"
@update:configs="$emit('update:configs', $event)"
/>
</div>
</div>
</q-list>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { ubrowserOperationConfigs } from "js/composer/composerConfig";
import UBrowserOperation from "./operations/UBrowserOperation.vue";
export default defineComponent({
name: "UBrowserOperations",
components: {
UBrowserOperation,
},
props: {
configs: {
type: Object,
required: true,
},
selectedActions: {
type: Array,
required: true,
},
},
data() {
return {
ubrowserOperationConfigs: ubrowserOperationConfigs,
};
},
emits: ["remove-action", "update:selectedActions", "update:configs"],
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);
}
},
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 } = action;
if (config) {
const newConfigs = { ...this.configs };
if (!newConfigs[action.value]) {
newConfigs[action.value] = {};
}
// 设置默认值
config.forEach((field) => {
if (field.defaultValue !== undefined) {
newConfigs[action.value][field.key] = field.defaultValue;
}
});
this.$emit("update:configs", newConfigs);
}
} else {
// 移除操作
const newActions = [...this.selectedActions];
newActions.splice(index, 1);
this.$emit("update:selectedActions", newActions);
}
},
getActionProps(action, key) {
return this.ubrowserOperationConfigs.find(
(a) => a.value === action.value
)[key];
},
},
});
</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.05);
}
.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;
}
.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

@@ -0,0 +1,202 @@
<template>
<div class="row q-col-gutter-sm">
<!-- 窗口显示控制 -->
<div class="col-12">
<div class="row items-center q-gutter-x-md">
<q-checkbox
:model-value="localConfigs.run.show"
label="显示窗口"
@update:model-value="updateConfig('show', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.center"
label="居中显示"
@update:model-value="updateConfig('center', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.alwaysOnTop"
label="总在最前"
@update:model-value="updateConfig('alwaysOnTop', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.fullscreen"
label="全屏显示"
@update:model-value="updateConfig('fullscreen', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.fullscreenable"
label="允许全屏"
@update:model-value="updateConfig('fullscreenable', $event)"
/>
</div>
</div>
<!-- 窗口尺寸和位置 -->
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-3">
<VariableInput
v-model="localConfigs.run.width"
label="窗口宽度"
:command="{ inputType: 'number' }"
@update:model-value="updateConfig('width', $event)"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfigs.run.height"
label="窗口高度"
:command="{ inputType: 'number' }"
@update:model-value="updateConfig('height', $event)"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfigs.run.x"
label="X坐标"
:command="{ inputType: 'number' }"
@update:model-value="updateConfig('x', $event)"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfigs.run.y"
label="Y坐标"
:command="{ inputType: 'number' }"
@update:model-value="updateConfig('y', $event)"
/>
</div>
</div>
</div>
<!-- 最大最小尺寸 -->
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-3">
<VariableInput
v-model="localConfigs.run.minWidth"
label="最小宽度"
:command="{ inputType: 'number' }"
@update:model-value="updateConfig('minWidth', $event)"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfigs.run.minHeight"
label="最小高度"
:command="{ inputType: 'number' }"
@update:model-value="updateConfig('minHeight', $event)"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfigs.run.maxWidth"
label="最大宽度"
:command="{ inputType: 'number' }"
@update:model-value="updateConfig('maxWidth', $event)"
/>
</div>
<div class="col-3">
<VariableInput
v-model="localConfigs.run.maxHeight"
label="最大高度"
:command="{ inputType: 'number' }"
@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)"
/>
<q-checkbox
:model-value="localConfigs.run.movable"
label="可移动"
@update:model-value="updateConfig('movable', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.minimizable"
label="可最小化"
@update:model-value="updateConfig('minimizable', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.maximizable"
label="可最大化"
@update:model-value="updateConfig('maximizable', $event)"
/>
<q-checkbox
:model-value="localConfigs.run.enableLargerThanScreen"
label="允许超出屏幕"
@update:model-value="updateConfig('enableLargerThanScreen', $event)"
/>
</div>
</div>
<!-- 透明度控制 -->
<div class="col-12">
<div class="row items-center" style="height: 36px">
<div class="q-mr-md" style="font-size: 12px">透明度</div>
<q-slider
class="col"
v-model="localConfigs.run.opacity"
:min="0"
:max="1"
:step="0.1"
label
color="primary"
switch-label-side
dense
@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 VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "UBrowserRun",
components: {
VariableInput,
},
props: {
configs: {
type: Object,
required: true,
},
},
emits: ["update:configs"],
data() {
return {
localConfigs: window.lodashM.cloneDeep(this.configs),
};
},
methods: {
updateConfig(key, value) {
this.localConfigs.run[key] = value;
this.$emit("update:configs", window.lodashM.cloneDeep(this.localConfigs));
},
},
watch: {
configs: {
deep: true,
handler(newConfigs) {
this.localConfigs = window.lodashM.cloneDeep(newConfigs);
},
},
},
});
</script>

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,91 @@
<template>
<div>
<div class="row q-col-gutter-sm">
<div
v-for="(cookie, index) in modelValue || [{}]"
:key="index"
class="col-12"
>
<div class="row items-center q-gutter-x-sm">
<div class="col">
<VariableInput
:model-value="cookie.name"
label="名称"
:command="{ icon: 'label' }"
@update:model-value="
(value) => handleUpdate(index, 'name', value)
"
/>
</div>
<div class="col">
<VariableInput
:model-value="cookie.value"
label=""
:command="{ icon: 'edit' }"
@update:model-value="
(value) => handleUpdate(index, 'value', value)
"
/>
</div>
<div class="col-auto">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeCookie(index)"
/>
</div>
</div>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加Cookie"
@click="addCookie"
class="q-mt-xs"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "UBrowserCookieList",
components: {
VariableInput,
},
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,72 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col">
<VariableInput
:command="{ icon: icon }"
:model-value="modelValue"
:label="label"
@update:model-value="$emit('update:modelValue', $event)"
/>
</div>
<div class="col-auto">
<q-select
v-model="selectedDevice"
:options="deviceOptions"
label="常用设备"
dense
filled
emit-value
map-options
options-dense
style="min-width: 150px"
@update:model-value="handleDeviceSelect"
>
<template v-slot:prepend>
<q-icon name="list" />
</template>
</q-select>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { deviceName } from "js/options/httpHeaders";
import VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "UBrowserDeviceName",
components: {
VariableInput,
},
props: {
modelValue: {
type: String,
default: "",
},
label: {
type: String,
required: true,
},
icon: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
data() {
return {
selectedDevice: null,
deviceOptions: deviceName,
};
},
methods: {
handleDeviceSelect(value) {
if (value) {
this.$emit("update:modelValue", value);
this.selectedDevice = null;
}
},
},
});
</script>

View File

@@ -0,0 +1,60 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col-6">
<VariableInput
v-model.number="size.width"
label="宽度"
:command="{ icon: 'width', inputType: 'number' }"
@update:model-value="handleUpdate"
/>
</div>
<div class="col-6">
<VariableInput
v-model.number="size.height"
label="高度"
:command="{ icon: 'height', inputType: 'number' }"
@update:model-value="handleUpdate"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "UBrowserDeviceSize",
components: {
VariableInput,
},
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,76 @@
<template>
<div>
<div class="row q-col-gutter-sm">
<div
v-for="(file, index) in modelValue || []"
:key="index"
class="col-12"
>
<div class="row q-col-gutter-sm">
<div class="col">
<VariableInput
:model-value="modelValue[index]"
label="文件路径"
:command="{ icon: 'folder' }"
@update:model-value="(value) => handleUpdate(index, value)"
/>
</div>
<div class="col-auto">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeFile(index)"
/>
</div>
</div>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加文件"
@click="addFile"
class="q-mt-xs"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "UBrowserFileList",
components: {
VariableInput,
},
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,237 @@
<template>
<div class="row q-col-gutter-sm ubrowser-function-input">
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-3">
<q-select
v-model="localParams"
use-input
use-chips
multiple
dense
borderless
hide-dropdown-icon
options-dense
input-debounce="0"
new-value-mode="add-unique"
label="参数"
@update:model-value="updateParams"
@input-value="handleInput"
@blur="handleBlur"
ref="paramSelect"
>
<template v-slot:prepend>
<div class="text-primary func-symbol">(</div>
</template>
<template v-slot:append>
<div class="text-primary func-symbol">)</div>
</template>
</q-select>
</div>
<div class="col-9">
<q-input
v-model="localFunction"
:label="label"
type="textarea"
dense
borderless
style="font-family: monospace, monoca, consola"
autogrow
@update:model-value="updateFunction"
>
<template v-slot:prepend>
<div class="text-primary func-symbol">=> {</div>
</template>
<template v-slot:append>
<div class="text-primary func-symbol">}</div>
</template>
</q-input>
</div>
</div>
</div>
<template v-if="localParams.length">
<div v-for="param in localParams" :key="param" class="col-12">
<div class="row q-col-gutter-sm items-center">
<div class="col-3">
<q-chip
dense
color="primary"
text-color="white"
removable
@remove="removeParam(param)"
>
{{ param }}
</q-chip>
</div>
<div class="col-9">
<q-input
v-model="paramValues[param]"
:label="`传递给参数 ${param} 的值`"
dense
filled
@update:model-value="updateParamValue(param, $event)"
/>
</div>
</div>
</div>
</template>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "UBrowserFunctionInput",
props: {
function: {
type: String,
default: "",
},
args: {
type: Array,
default: () => [],
},
label: {
type: String,
default: "函数内容",
},
icon: {
type: String,
default: "code",
},
},
emits: ["update:function", "update:args"],
data() {
return {
localFunction: "",
localParams: [],
paramValues: {},
newParamName: "",
};
},
created() {
// 初始化本地数据
this.localFunction = this.function;
this.localParams = this.args?.map((arg) => arg.name) || [];
this.paramValues = Object.fromEntries(
this.args?.map((arg) => [arg.name, arg.value]) || []
);
},
methods: {
updateFunction(value) {
this.localFunction = value;
this.emitUpdate();
},
updateParams(value) {
this.localParams = value;
this.emitUpdate();
},
removeParam(param) {
const index = this.localParams.indexOf(param);
if (index > -1) {
this.localParams.splice(index, 1);
delete this.paramValues[param];
this.emitUpdate();
}
},
updateParamValue(param, value) {
this.paramValues[param] = value;
this.emitUpdate();
},
emitUpdate() {
this.$emit("update:function", this.localFunction);
this.$emit(
"update:args",
this.localParams.map((name) => ({
name,
value: this.paramValues[name] || "",
}))
);
},
handleInput(val) {
if (!val) return;
this.newParamName = val;
if (val.includes(",") || val.includes(" ")) {
const params = val
.split(/[,\s]+/)
.map((p) => p.trim())
.filter((p) => p);
params.forEach((param) => {
if (param && !this.localParams.includes(param)) {
this.localParams = [...this.localParams, param];
this.paramValues[param] = "";
}
});
this.newParamName = "";
this.emitUpdate();
this.$refs.paramSelect.updateInputValue("");
}
},
handleBlur() {
if (this.newParamName && !this.localParams.includes(this.newParamName)) {
this.localParams = [...this.localParams, this.newParamName];
this.paramValues[this.newParamName] = "";
this.newParamName = "";
this.emitUpdate();
this.$refs.paramSelect.updateInputValue("");
}
},
},
watch: {
function: {
handler(newValue) {
this.localFunction = newValue;
},
},
args: {
deep: true,
handler(newValue) {
this.localParams = newValue?.map((arg) => arg.name) || [];
this.paramValues = Object.fromEntries(
newValue?.map((arg) => [arg.name, arg.value]) || []
);
},
},
},
});
</script>
<style scoped>
.ubrowser-function-input :deep(.q-field__control) .text-primary.func-symbol {
font-size: 24px !important;
}
.ubrowser-function-input :deep(.q-select__input) {
display: flex !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
.ubrowser-function-input :deep(.q-select .q-field__native) {
display: flex !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
.ubrowser-function-input :deep(.q-select .q-field__native > div) {
display: flex !important;
flex-wrap: nowrap !important;
flex: 0 0 auto !important;
}
.ubrowser-function-input :deep(.q-select .q-chip) {
flex: 0 0 auto !important;
margin-right: 4px !important;
}
.ubrowser-function-input :deep(.q-select__input::-webkit-scrollbar),
.ubrowser-function-input :deep(.q-select .q-field__native::-webkit-scrollbar) {
display: none !important;
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div>
<div class="text-caption q-mb-sm">{{ label }}</div>
<div
v-for="(param, index) in modelValue || []"
:key="index"
class="row q-col-gutter-sm q-mb-sm"
>
<div class="col-5">
<VariableInput
:model-value="param.name"
label="参数名"
:command="{ icon: 'label' }"
@update:model-value="(value) => handleUpdate(index, 'name', value)"
/>
</div>
<div class="col-5">
<VariableInput
:model-value="param.value"
label="传递给参数的值"
:command="{ icon: 'edit' }"
@update:model-value="(value) => handleUpdate(index, 'value', value)"
/>
</div>
<div class="col-2">
<q-btn
flat
round
dense
color="negative"
icon="remove"
@click="removeParam(index)"
/>
</div>
</div>
<q-btn
flat
dense
color="primary"
icon="add"
label="添加参数"
@click="addParam"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/composer/ui/VariableInput.vue";
export default defineComponent({
name: "UBrowserNamedParamList",
components: {
VariableInput,
},
props: {
modelValue: {
type: Array,
default: () => [{ name: "", value: "" }],
},
label: String,
},
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

@@ -0,0 +1,244 @@
<template>
<div class="row q-col-gutter-sm items-center">
<template v-for="field in fields" :key="field.key">
<div
v-if="!field.showWhen || fieldValue[field.showWhen] === field.showValue"
:class="['col', field.width ? `col-${field.width}` : 'col-12']"
>
<!-- 复选框组 -->
<template v-if="field.type === 'checkbox-group'">
<UBrowserCheckboxGroup
v-model="fieldValue[field.key]"
:options="field.options"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 单个复选框 -->
<template v-else-if="field.type === 'checkbox'">
<UBrowserCheckbox
v-model="fieldValue[field.key]"
:label="field.label"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 基本输入类型的处理 -->
<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>
<VariableInput
v-model="fieldValue[field.key]"
:label="field.label"
:command="{
icon: field.icon,
inputType: field.inputType,
}"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
</template>
<!-- 文本区域 -->
<template v-else-if="field.type === 'textarea'">
<UBrowserTextarea
v-model="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 选择框 -->
<template v-else-if="field.type === 'select'">
<UBrowserSelect
v-model="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
:options="field.options"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- Cookie列表 -->
<template v-else-if="field.type === 'cookie-list'">
<UBrowserCookieList
v-model="fieldValue[field.key]"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 命名参数列表 -->
<template v-else-if="field.type === 'named-param-list'">
<UBrowserNamedParamList
v-model="fieldValue[field.key]"
:label="field.label"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 文件列表 -->
<template v-else-if="field.type === 'file-list'">
<UBrowserFileList
v-model="fieldValue[field.key]"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
<!-- 按钮组 -->
<template v-else-if="field.type === 'button-toggle'">
<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>
</div>
</template>
<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 VariableInput from "components/composer/ui/VariableInput.vue";
import UBrowserCheckboxGroup from "./UBrowserCheckboxGroup.vue";
export default defineComponent({
name: "UBrowserOperation",
components: {
UBrowserFunctionInput,
UBrowserCheckbox,
UBrowserFileList,
UBrowserCookieList,
UBrowserButtonToggle,
UBrowserDeviceSize,
UBrowserNamedParamList,
UBrowserSelect,
UBrowserDeviceName,
UBrowserTextarea,
VariableInput,
UBrowserCheckboxGroup,
},
props: {
configs: {
type: Object,
required: true,
},
action: {
type: String,
required: true,
},
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 if (field.type === "function-with-params") {
// 为function-with-params类型设置特殊的默认值结构
this.fieldValue.function = value?.function || "";
this.fieldValue.args = value?.args || [];
return; // 跳过后续的赋值
} 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);
},
},
watch: {
// 监听配置变化
configs: {
deep: true,
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;
}
});
},
},
},
});
</script>

View File

@@ -0,0 +1,43 @@
<template>
<q-select
:model-value="modelValue"
:label="label"
:options="options"
dense
filled
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
filled
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>