各种调整

This commit is contained in:
fofolee 2024-12-27 23:35:09 +08:00
parent cdfb2b502f
commit e8d12b95d4
25 changed files with 2454 additions and 1274 deletions

8
.gitattributes vendored Normal file
View File

@ -0,0 +1,8 @@
# 自动识别文本文件并进行换行符标准化
* text=auto
# 指定特定文件类型的换行符处理
*.txt text
*.js text
*.css text
*.html text

2626
plugin/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
{
"dependencies": {
"axios": "^0.24.0",
"iconv-lite": "^0.6.3",
"lodash": "^4.17.21",
"ses": "^0.15.15",
"sharp": "^0.33.5",
"tree-kill": "^1.2.2"
}
"dependencies": {
"axios": "^0.24.0",
"iconv-lite": "^0.6.3",
"jimp": "^0.22.12",
"lodash": "^4.17.21",
"ses": "^0.15.15",
"tree-kill": "^1.2.2"
}
}

View File

@ -12,8 +12,7 @@ const url = require("url");
const kill = require("tree-kill");
const crypto = require("crypto");
require("ses");
const sharp = require("sharp");
const Jimp = require("jimp");
const md5 = (input) => {
return crypto.createHash("md5").update(input, "utf8").digest("hex");
};
@ -789,17 +788,19 @@ window.quickcommandHttpServer = () => {
};
};
// 处理背景图片
window.imageProcessor = async (imagePath) => {
try {
// 读取图片
let image = sharp(imagePath);
let metadata = await image.metadata();
const image = await Jimp.read(imagePath);
// 设置固定目标尺寸
// 获取原始尺寸
const originalWidth = image.getWidth();
const originalHeight = image.getHeight();
const ratio = originalWidth / originalHeight;
// 设置目标尺寸
let targetWidth = 1280;
let targetHeight = 720;
const ratio = metadata.width / metadata.height;
if (ratio > 16 / 9) {
targetHeight = Math.min(720, Math.round(targetWidth / ratio));
@ -808,15 +809,14 @@ window.imageProcessor = async (imagePath) => {
}
// 调整大小并压缩
let processedBuffer = await image
.resize(targetWidth, targetHeight, {
fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.jpeg({ quality: 80, progressive: true })
.toBuffer();
await image
.resize(targetWidth, targetHeight, Jimp.RESIZE_BICUBIC)
.quality(80);
return `data:image/jpeg;base64,${processedBuffer.toString("base64")}`;
// 转换为 base64
const base64 = await image.getBase64Async(Jimp.MIME_JPEG);
return base64;
} catch (error) {
console.error("处理图片失败:", error);
return null;

View File

@ -111,7 +111,6 @@ export default defineComponent({
argv: "",
argvType: "string",
saveOutput: false,
useOutput: null,
outputVariable: null,
cmd: action.value || action.cmd,
value: action.value || action.cmd,
@ -122,8 +121,6 @@ export default defineComponent({
this.commandFlow.forEach((cmd) => {
let line = "";
// TODO: string
console.log("Generating code for command:", cmd);
if (cmd.outputVariable) {
line += `let ${cmd.outputVariable} = `;
@ -131,14 +128,8 @@ export default defineComponent({
if (cmd.value === "ubrowser") {
line += cmd.argv;
} else if (cmd.useOutput !== null) {
const outputVar = this.commandFlow[cmd.useOutput].outputVariable;
line += `${cmd.value}(${outputVar})`;
} else {
const needQuotes =
cmd.argvType === "string" && cmd.argvType !== "variable";
const argv = needQuotes ? `"${cmd.argv}"` : cmd.argv;
line += `${cmd.value}(${argv})`;
line += `${cmd.value}(${cmd.argv})`;
}
code.push(line);

View File

@ -89,9 +89,9 @@
<VariableInput
v-model="argvLocal"
:label="placeholder"
:command="command"
class="col"
ref="variableInput"
@update:type="handleArgvTypeUpdate"
/>
</template>
</div>
@ -106,6 +106,7 @@ import { defineComponent, inject } from "vue";
import KeyEditor from "./KeyEditor.vue";
import UBrowserEditor from "./ubrowser/UBrowserEditor.vue";
import VariableInput from "./VariableInput.vue";
import { validateVariableName } from "js/common/variableValidator";
export default defineComponent({
name: "ComposerCard",
@ -137,13 +138,7 @@ export default defineComponent({
showKeyRecorder: false,
};
},
emits: [
"remove",
"toggle-output",
"update:argv",
"update:use-output",
"update:command",
],
emits: ["remove", "toggle-output", "update:argv", "update:command"],
computed: {
saveOutputLocal: {
get() {
@ -158,31 +153,26 @@ export default defineComponent({
return this.command.argv;
},
set(value) {
this.$emit("update:argv", value);
},
},
useOutputLocal: {
get() {
return this.command.useOutput;
},
set(value) {
this.$emit("update:use-output", value);
const updatedCommand = {
...this.command,
argv: value,
};
this.$emit("update:command", updatedCommand);
},
},
},
setup() {
const addVariable = inject("addVariable");
const removeVariable = inject("removeVariable");
const variables = inject("composerVariables", []);
return {
addVariable,
removeVariable,
variables,
};
},
methods: {
handleClearOutput() {
this.$emit("update:use-output", null);
},
handleKeyRecord(keys) {
this.showKeyRecorder = false;
// keyTap("a","control")
@ -200,6 +190,19 @@ export default defineComponent({
}
},
handleOutputVariableUpdate(value) {
//
const validation = validateVariableName(value);
if (!validation.isValid) {
quickcommand.showMessageBox(validation.error, "warning");
return;
}
//
if (this.variables.some((v) => v.name === value)) {
quickcommand.showMessageBox(`变量名 "${value}" 已经存在`, "warning");
return;
}
//
const updatedCommand = {
...this.command,
@ -210,14 +213,6 @@ export default defineComponent({
//
this.handleOutputVariableChange(value);
},
handleArgvTypeUpdate(type) {
console.log("Type updated in card:", type);
const updatedCommand = {
...this.command,
argvType: type,
};
this.$emit("update:command", updatedCommand);
},
handleToggleOutput() {
//
const updatedCommand = {
@ -394,7 +389,7 @@ export default defineComponent({
background: rgba(255, 255, 255, 0.05);
}
/* 输入框内部样式化 */
/* 输入框内部样式化 */
.output-section :deep(.q-field__control) {
height: 28px;
min-height: 28px;

View File

@ -30,12 +30,10 @@
>
<ComposerCard
:command="element"
:available-outputs="getAvailableOutputs(index)"
:placeholder="getPlaceholder(element, index)"
@remove="removeCommand(index)"
@toggle-output="toggleSaveOutput(index)"
@update:argv="(val) => handleArgvChange(index, val)"
@update:use-output="(val) => handleUseOutputChange(index, val)"
@update:command="(val) => updateCommand(index, val)"
/>
</div>
@ -54,7 +52,7 @@
</template>
<script>
import { defineComponent } from "vue";
import { defineComponent, inject } from "vue";
import draggable from "vuedraggable";
import ComposerCard from "./ComposerCard.vue";
@ -81,6 +79,13 @@ export default defineComponent({
},
},
},
setup() {
const removeVariable = inject("removeVariable");
return {
removeVariable,
};
},
data() {
return {
dragIndex: -1,
@ -135,45 +140,59 @@ export default defineComponent({
},
onDrop(event) {
const actionData = JSON.parse(event.dataTransfer.getData("action"));
const newCommand = {
...actionData,
id: Date.now(), // 使ID
argv: "",
saveOutput: false,
useOutput: null,
cmd: actionData.value || actionData.cmd,
value: actionData.value || actionData.cmd,
};
try {
//
const actionData = event.dataTransfer.getData("action");
const newCommands = [...this.commands];
if (this.dragIndex >= 0) {
newCommands.splice(this.dragIndex, 0, newCommand);
} else {
newCommands.push(newCommand);
// action
if (!actionData) {
return;
}
//
const parsedAction = JSON.parse(actionData);
const newCommand = {
...parsedAction,
id: Date.now(),
argv: "",
saveOutput: false,
useOutput: null,
outputVariable: null,
cmd: parsedAction.value || parsedAction.cmd,
value: parsedAction.value || parsedAction.cmd,
};
const newCommands = [...this.commands];
if (this.dragIndex >= 0) {
newCommands.splice(this.dragIndex, 0, newCommand);
} else {
newCommands.push(newCommand);
}
this.$emit("update:modelValue", newCommands);
this.dragIndex = -1;
document.querySelectorAll(".dragging").forEach((el) => {
el.classList.remove("dragging");
});
} catch (error) {
//
console.debug("Internal drag & drop reorder", error);
}
this.$emit("update:modelValue", newCommands);
this.dragIndex = -1;
document.querySelectorAll(".dragging").forEach((el) => {
el.classList.remove("dragging");
});
},
removeCommand(index) {
const command = this.commands[index];
//
if (command.outputVariable) {
this.removeVariable(command.outputVariable);
}
const newCommands = [...this.commands];
newCommands.splice(index, 1);
this.$emit("update:modelValue", newCommands);
},
getAvailableOutputs(currentIndex) {
return this.commands
.slice(0, currentIndex)
.map((cmd, index) => ({
label: `${cmd.label} 的输出`,
value: index,
disable: !cmd.saveOutput,
}))
.filter((item) => !item.disable);
getPlaceholder(element, index) {
return element.desc;
},
toggleSaveOutput(index) {
const newCommands = [...this.commands];
@ -195,22 +214,7 @@ export default defineComponent({
};
this.$emit("update:modelValue", newCommands);
},
handleUseOutputChange(index, value) {
const newCommands = [...this.commands];
newCommands[index].useOutput = value;
if (value !== null) {
newCommands[index].argv = "";
}
this.$emit("update:modelValue", newCommands);
},
getPlaceholder(element, index) {
if (element.useOutput !== null) {
return `使用 ${this.commands[element.useOutput].label} 的输出`;
}
return element.desc;
},
updateCommand(index, updatedCommand) {
console.log("Command updated in flow:", updatedCommand);
const newCommands = [...this.commands];
newCommands[index] = {
...newCommands[index],
@ -289,7 +293,7 @@ export default defineComponent({
border-color: #676666;
}
/* 滑动淡出<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>画 */
/* 滑动淡出画 */
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.3s ease;

View File

@ -65,7 +65,7 @@ export default defineComponent({
"action",
JSON.stringify({
...command,
cmd: command.value || command.cmd,
value: command.value,
})
);
event.target.classList.add("dragging");

View File

@ -66,14 +66,10 @@
<template #item="{ element, index }">
<ComposerCard
:command="element"
:available-outputs="getAvailableOutputs(index, 'if')"
:placeholder="getPlaceholder(element, index, 'if')"
@remove="removeCommand('if', index)"
@toggle-output="toggleSaveOutput('if', index)"
@update:argv="(val) => handleArgvChange('if', index, val)"
@update:use-output="
(val) => handleUseOutputChange('if', index, val)
"
/>
</template>
</draggable>
@ -108,14 +104,10 @@
<template #item="{ element, index }">
<ComposerCard
:command="element"
:available-outputs="getAvailableOutputs(index, 'else')"
:placeholder="getPlaceholder(element, index, 'else')"
@remove="removeCommand('else', index)"
@toggle-output="toggleSaveOutput('else', index)"
@update:argv="(val) => handleArgvChange('else', index, val)"
@update:use-output="
(val) => handleUseOutputChange('else', index, val)
"
/>
</template>
</draggable>
@ -294,20 +286,6 @@ export default defineComponent({
});
},
getAvailableOutputs(currentIndex, branch) {
//
const commands = branch === "if" ? this.ifCommands : this.elseCommands;
return commands
.slice(0, currentIndex)
.map((cmd, index) => ({
label: `${cmd.label} 的输出`,
value: index,
disable: !cmd.saveOutput,
}))
.filter((item) => !item.disable);
},
toggleSaveOutput(branch, index) {
const commands =
branch === "if" ? [...this.ifCommands] : [...this.elseCommands];
@ -338,19 +316,6 @@ export default defineComponent({
});
},
handleUseOutputChange(branch, index, value) {
const commands =
branch === "if" ? [...this.ifCommands] : [...this.elseCommands];
commands[index].useOutput = value;
if (value !== null) {
commands[index].argv = "";
}
this.$emit("update:modelValue", {
...this.modelValue,
[branch]: commands,
});
},
getPlaceholder(element, index, branch) {
if (element.useOutput !== null) {
const commands = branch === "if" ? this.ifCommands : this.elseCommands;

View File

@ -1,33 +1,12 @@
<template>
<q-input
v-model="inputValue"
v-if="!isNumber"
v-model="localValue"
dense
outlined
:label="label"
class="variable-input"
>
<template v-slot:prepend>
<q-btn
flat
dense
round
:icon="isString ? 'format_quote' : 'format_quote'"
size="sm"
:class="{
'text-primary': isString,
'text-grey-6': !isString,
}"
class="string-toggle"
@click="toggleStringType"
>
<q-tooltip>{{
isString
? "当前类型是:字符串,点击切换"
: "当前类型是:变量、数字、表达式等,点击切换"
}}</q-tooltip>
</q-btn>
</template>
<template v-slot:append>
<q-btn
v-if="hasSelectedVariable"
@ -41,11 +20,25 @@
>
<q-tooltip>清除选中的变量</q-tooltip>
</q-btn>
<q-btn
flat
dense
round
:icon="isString ? 'format_quote' : 'data_object'"
size="sm"
class="string-toggle"
@click="toggleStringType"
>
<q-tooltip>{{
isString
? "当前类型是:字符串,点击切换"
: "当前类型是:变量、数字、表达式等,点击切换"
}}</q-tooltip>
</q-btn>
<q-btn-dropdown
flat
dense
:icon="hasSelectedVariable ? 'data_object' : 'functions'"
:class="{
'text-primary': hasSelectedVariable,
'text-grey-6': !hasSelectedVariable,
@ -92,84 +85,174 @@
</q-list>
</q-btn-dropdown>
</template>
<template v-slot:prepend>
<q-icon :name="command.icon || commandIcons[command.value] || 'code'" />
</template>
</q-input>
<!-- 强制为数字类型时不支持切换类型 -->
<q-input
v-else
type="number"
v-model.number="localValue"
dense
outlined
:label="label"
class="number-input"
>
<template v-slot:prepend>
<q-icon :name="command.icon || commandIcons[command.value] || 'code'" />
</template>
<template v-slot:append>
<!-- <q-icon name="pin" size="xs" /> -->
<div class="column items-center number-controls">
<q-btn
flat
dense
round
icon="keyboard_arrow_up"
size="xs"
class="number-btn"
@click="updateNumber(100)"
/>
<q-btn
flat
dense
round
icon="keyboard_arrow_down"
size="xs"
class="number-btn"
@click="updateNumber(-100)"
/>
</div>
</template>
</q-input>
</template>
<script>
import { defineComponent, inject, computed } from "vue";
import { defineComponent, inject } from "vue";
import { commandIcons } from "js/composer/composerConfig";
export default defineComponent({
name: "VariableInput",
props: {
modelValue: String,
modelValue: [String, Number],
label: String,
command: {
type: Object,
required: true,
},
},
emits: ["update:modelValue", "update:type"],
emits: ["update:modelValue"],
setup() {
const variables = inject("composerVariables", []);
return { variables };
return { variables, commandIcons };
},
data() {
return {
isString: true,
selectedVariable: null,
//
isString: this.command.inputType !== "number",
};
},
computed: {
inputValue: {
localValue: {
get() {
return this.modelValue;
//
if (this.isNumber) return this.modelValue;
// isString
const val = this.modelValue || "";
return this.isString ? val.replace(/^"|"$/g, "") : val;
},
set(value) {
this.$emit("update:modelValue", value);
this.$emit("update:modelValue", this.formatValue(value));
},
},
// UI
hasSelectedVariable() {
return this.selectedVariable !== null;
},
isNumber() {
return this.command.inputType === "number";
},
},
methods: {
//
formatValue(value) {
//
if (!value || this.hasSelectedVariable || this.isNumber) return value;
// isString
return this.isString
? `"${value.replace(/^"|"$/g, "")}"`
: value.replace(/^"|"$/g, "");
},
// /
toggleStringType() {
if (!this.hasSelectedVariable) {
this.isString = !this.isString;
this.$emit("update:type", this.isString ? "string" : "number");
//
if (this.modelValue) {
this.$emit("update:modelValue", this.formatValue(this.modelValue));
}
}
},
//
setIsString(value) {
if (!this.hasSelectedVariable && this.isString !== value) {
this.isString = value;
//
if (this.modelValue) {
this.$emit("update:modelValue", this.formatValue(this.modelValue));
}
}
},
//
insertVariable(variable) {
this.selectedVariable = variable;
this.isString = false;
this.$emit("update:type", "variable");
this.isString = false; //
this.$emit("update:modelValue", variable.name);
},
//
clearVariable() {
this.selectedVariable = null;
this.isString = true;
this.$emit("update:type", "string");
this.isString = true; //
this.$emit("update:modelValue", "");
},
//
updateNumber(delta) {
const current = Number(this.localValue) || 0;
this.$emit("update:modelValue", current + delta);
},
},
watch: {
modelValue(newVal) {
if (this.selectedVariable && newVal !== this.selectedVariable.name) {
this.selectedVariable = null;
this.isString = true;
this.$emit("update:type", "string");
}
//
modelValue: {
immediate: true,
handler(newVal) {
//
if (newVal && !this.hasSelectedVariable && !this.isNumber) {
const formattedValue = this.formatValue(newVal);
// <EFBFBD><EFBFBD><EFBFBD>
if (formattedValue !== newVal) {
this.$emit("update:modelValue", formattedValue);
}
}
},
},
},
mounted() {
this.$emit("update:type", "string");
},
});
</script>
@ -223,7 +306,7 @@ export default defineComponent({
}
.variable-item:hover {
background: rgba(var(--q-primary-rgb), 0.1);
background: var(--q-primary-opacity-10);
}
.variable-name {
@ -252,4 +335,45 @@ export default defineComponent({
transform: scale(1.1);
color: var(--q-negative);
}
/* 数字输入框样式 */
.number-input {
width: 100%;
}
/* 隐藏默认的数字输入框箭头 - Chrome, Safari, Edge, Opera */
.number-input :deep(input[type="number"]::-webkit-outer-spin-button),
.number-input :deep(input[type="number"]::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
.number-input :deep(.q-field__control) {
padding-left: 8px;
padding-right: 4px;
}
.number-controls {
border-left: 1px solid rgba(0, 0, 0, 0.12);
margin-left: 4px;
padding: 2px 4px;
}
.body--dark .number-controls {
border-left-color: rgba(255, 255, 255, 0.12);
}
.number-btn {
opacity: 0.7;
font-size: 18px;
padding: 2px;
margin: 0;
min-height: 20px;
min-width: 24px;
}
.number-btn:hover {
opacity: 1;
color: var(--q-primary);
}
</style>

View File

@ -2,49 +2,34 @@
<div class="row q-col-gutter-sm">
<!-- 基础配置 -->
<div class="col-12">
<q-input
<VariableInput
v-model="localConfigs.goto.url"
label="网址"
dense
outlined
:command="{ icon: 'link' }"
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="link" />
</template>
</q-input>
/>
</div>
<!-- Headers配置 -->
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-12">
<q-input
<VariableInput
v-model="localConfigs.goto.headers.Referer"
label="Referer"
dense
outlined
:command="{ icon: 'link' }"
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="link" />
</template>
</q-input>
/>
</div>
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col">
<q-input
<VariableInput
v-model="localConfigs.goto.headers.userAgent"
label="User-Agent"
dense
outlined
:command="{ icon: 'devices' }"
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="devices" />
</template>
</q-input>
/>
</div>
<div class="col-auto">
<q-select
@ -70,27 +55,25 @@
<!-- 超时配置 -->
<div class="col-12">
<q-input
v-model.number="localConfigs.goto.timeout"
type="number"
<VariableInput
v-model="localConfigs.goto.timeout"
:command="{ icon: 'timer', inputType: 'number' }"
label="超时时间(ms)"
dense
outlined
@update:model-value="updateConfigs"
>
<template v-slot:prepend>
<q-icon name="timer" />
</template>
</q-input>
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/editor/composer/VariableInput.vue";
export default defineComponent({
name: "UBrowserBasic",
components: {
VariableInput,
},
props: {
configs: {
type: Object,

View File

@ -95,12 +95,11 @@ export default defineComponent({
this.configs = newConfigs;
},
removeAction(action) {
const index = this.selectedActions.findIndex(
(a) => a.value === action.value
);
if (index > -1) {
this.selectedActions.splice(index, 1);
}
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: {

View File

@ -4,10 +4,8 @@
<!-- 操作选择网格 -->
<div class="row q-col-gutter-xs">
<div
v-for="[actionName, { label }] in Object.entries(
ubrowserOperationConfigs
)"
:key="actionName"
v-for="action in ubrowserOperationConfigs"
:key="action.value"
class="col-2"
>
<q-card
@ -16,13 +14,13 @@
class="action-card cursor-pointer"
:class="{
'action-selected': selectedActions.some(
(a) => a.value === actionName
(a) => a.value === action.value
),
}"
@click="toggleAction({ value: actionName, label: label })"
@click="toggleAction(action)"
>
<div class="q-pa-xs text-caption text-wrap text-center">
{{ label }}
{{ action.label }}
</div>
</q-card>
</div>
@ -45,9 +43,7 @@
<q-avatar color="primary">
<q-icon
color="white"
:name="
ubrowserOperationConfigs[action.value].icon || 'touch_app'
"
:name="getActionProps(action, 'icon') || 'touch_app'"
size="14px"
/>
</q-avatar>
@ -76,11 +72,11 @@
/>
</div>
</div>
<div v-if="ubrowserOperationConfigs[action.value].config">
<div v-if="getActionProps(action, 'config')">
<UBrowserOperation
:configs="configs"
:action="action.value"
:fields="ubrowserOperationConfigs[action.value].config"
:fields="getActionProps(action, 'config')"
@update:configs="$emit('update:configs', $event)"
/>
</div>
@ -147,7 +143,7 @@ export default defineComponent({
]);
//
const { config } = this.ubrowserOperationConfigs[action.value];
const { config } = action;
if (config) {
const newConfigs = { ...this.configs };
if (!newConfigs[action.value]) {
@ -168,6 +164,11 @@ export default defineComponent({
this.$emit("update:selectedActions", newActions);
}
},
getActionProps(action, key) {
return this.ubrowserOperationConfigs.find(
(a) => a.value === action.value
)[key];
},
},
});
</script>

View File

@ -8,22 +8,20 @@
>
<div class="row items-center q-gutter-x-sm">
<div class="col">
<q-input
<VariableInput
:model-value="cookie.name"
label="名称"
dense
outlined
:command="{ icon: 'label' }"
@update:model-value="
(value) => handleUpdate(index, 'name', value)
"
/>
</div>
<div class="col">
<q-input
<VariableInput
:model-value="cookie.value"
label="值"
dense
outlined
:command="{ icon: 'edit' }"
@update:model-value="
(value) => handleUpdate(index, 'value', value)
"
@ -56,9 +54,13 @@
<script>
import { defineComponent } from "vue";
import VariableInput from "components/editor/composer/VariableInput.vue";
export default defineComponent({
name: "UBrowserCookieList",
components: {
VariableInput,
},
props: {
modelValue: {
type: Array,

View File

@ -1,17 +1,12 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col">
<q-input
<VariableInput
:command="{ icon: icon }"
:model-value="modelValue"
:label="label"
dense
outlined
@update:model-value="$emit('update:modelValue', $event)"
>
<template v-slot:prepend>
<q-icon :name="icon" />
</template>
</q-input>
/>
</div>
<div class="col-auto">
<q-select
@ -36,9 +31,13 @@
<script>
import { defineComponent } from "vue";
import VariableInput from "components/editor/composer/VariableInput.vue";
export default defineComponent({
name: "UBrowserDeviceName",
components: {
VariableInput,
},
props: {
modelValue: {
type: String,

View File

@ -1,41 +1,33 @@
<template>
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-input
<VariableInput
v-model.number="size.width"
type="number"
label="宽度"
dense
outlined
:command="{ icon: 'width', inputType: 'number' }"
@update:model-value="handleUpdate"
>
<template v-slot:prepend>
<q-icon name="width" />
</template>
</q-input>
/>
</div>
<div class="col-6">
<q-input
<VariableInput
v-model.number="size.height"
type="number"
label="高度"
dense
outlined
:command="{ icon: 'height', inputType: 'number' }"
@update:model-value="handleUpdate"
>
<template v-slot:prepend>
<q-icon name="height" />
</template>
</q-input>
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "components/editor/composer/VariableInput.vue";
export default defineComponent({
name: "UBrowserDeviceSize",
components: {
VariableInput,
},
props: {
modelValue: {
type: Object,

View File

@ -8,11 +8,10 @@
>
<div class="row q-col-gutter-sm">
<div class="col">
<q-input
<VariableInput
:model-value="modelValue[index]"
label="文件路径"
dense
outlined
:command="{ icon: 'folder' }"
@update:model-value="(value) => handleUpdate(index, value)"
/>
</div>
@ -43,9 +42,13 @@
<script>
import { defineComponent } from "vue";
import VariableInput from "components/editor/composer/VariableInput.vue";
export default defineComponent({
name: "UBrowserFileList",
components: {
VariableInput,
},
props: {
modelValue: {
type: Array,

View File

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

View File

@ -7,20 +7,18 @@
class="row q-col-gutter-sm q-mb-sm"
>
<div class="col-5">
<q-input
<VariableInput
:model-value="param.name"
label="参数名"
dense
outlined
:command="{ icon: 'label' }"
@update:model-value="(value) => handleUpdate(index, 'name', value)"
/>
</div>
<div class="col-5">
<q-input
<VariableInput
:model-value="param.value"
label="传递给参数的值"
dense
outlined
:command="{ icon: 'edit' }"
@update:model-value="(value) => handleUpdate(index, 'value', value)"
/>
</div>
@ -48,18 +46,19 @@
<script>
import { defineComponent } from "vue";
import VariableInput from "components/editor/composer/VariableInput.vue";
export default defineComponent({
name: "UBrowserNamedParamList",
components: {
VariableInput,
},
props: {
modelValue: {
type: Array,
default: () => [],
},
label: {
type: String,
required: true,
default: () => [{ name: "", value: "" }],
},
label: String,
},
emits: ["update:modelValue"],
methods: {

View File

@ -36,11 +36,13 @@
</template>
<!-- 普通输入框 -->
<template v-else>
<UBrowserInput
<VariableInput
v-model="fieldValue[field.key]"
:label="field.label"
:icon="field.icon"
:input-type="field.inputType"
:command="{
icon: field.icon,
inputType: field.inputType,
}"
@update:model-value="updateValue(field.key, $event)"
/>
</template>
@ -139,7 +141,7 @@ import UBrowserNamedParamList from "./UBrowserNamedParamList.vue";
import UBrowserSelect from "./UBrowserSelect.vue";
import UBrowserDeviceName from "./UBrowserDeviceName.vue";
import UBrowserTextarea from "./UBrowserTextarea.vue";
import UBrowserInput from "./UBrowserInput.vue";
import VariableInput from "components/editor/composer/VariableInput.vue";
import UBrowserCheckboxGroup from "./UBrowserCheckboxGroup.vue";
export default defineComponent({
@ -155,7 +157,7 @@ export default defineComponent({
UBrowserSelect,
UBrowserDeviceName,
UBrowserTextarea,
UBrowserInput,
VariableInput,
UBrowserCheckboxGroup,
},
props: {

View File

@ -145,6 +145,7 @@
</template>
<script>
export default {
name: "PersonalizeMenu",
props: {

View File

@ -0,0 +1,150 @@
// JavaScript 关键字和保留字列表
const reservedWords = [
// ES6+ JavaScript 语言关键字
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"export",
"extends",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"new",
"return",
"super",
"switch",
"this",
"throw",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
"let",
"static",
"await",
"enum",
// 严格模式下的额外保留字
"implements",
"interface",
"package",
"private",
"protected",
"public",
// 历史遗留的保留字(可能在未来版本中使用)
"abstract",
"boolean",
"byte",
"char",
"double",
"final",
"float",
"goto",
"int",
"long",
"native",
"short",
"synchronized",
"throws",
"transient",
"volatile",
// JavaScript 内置的特殊值
"null",
"true",
"false",
"undefined",
"NaN",
"Infinity",
// 常用的全局对象和构造函数
"Array",
"Boolean",
"Date",
"Error",
"Function",
"JSON",
"Math",
"Number",
"Object",
"RegExp",
"String",
"Promise",
"Proxy",
"Map",
"Set",
"Symbol",
"BigInt",
// 浏览器和 Node.js 环境的全局对象
"window",
"document",
"console",
"global",
"process",
"globalThis",
// 特殊的内置标识符
"arguments",
"eval",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf",
];
/**
* 检查变量名是否合法
* @param {string} name - 要检查的变量名
* @returns {object} - 包含验证结果和错误信息的对象
*/
export function validateVariableName(name) {
// 检查是否为空
if (!name) {
return {
isValid: false,
error: "变量名不能为空",
};
}
// 检查是否是保留字
if (reservedWords.includes(name)) {
return {
isValid: false,
error: `"${name}" 是 JavaScript 保留字,不能用作变量名`,
};
}
// 检查变量名格式是否合法
const validNameRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
if (!validNameRegex.test(name)) {
return {
isValid: false,
error:
"变量名必须以字母、下划线或 $ 开头,只能包含字母、数字、下划线和 $",
};
}
return {
isValid: true,
error: null,
};
}

View File

@ -109,6 +109,7 @@ export const commandCategories = [
value: "quickcommand.sleep",
label: "添加延时",
desc: "延迟的毫秒数",
inputType: "number",
},
],
},

View File

@ -10,29 +10,33 @@ export function generateUBrowserCode(configs, selectedActions) {
let code = "utools.ubrowser";
// 基础参数
if (configs.useragent.value) {
code += `\n .useragent('${configs.useragent.value}')`;
}
// if (configs.useragent.value) {
// code += `\n .useragent('${configs.useragent.value}')`;
// }
if (configs.goto.url) {
const gotoOptions = {};
if (configs.goto.headers.Referer) {
gotoOptions.headers = gotoOptions.headers || {};
gotoOptions.headers.Referer = configs.goto.headers.Referer;
}
if (configs.goto.headers.userAgent) {
gotoOptions.headers = gotoOptions.headers || {};
gotoOptions.headers["User-Agent"] = configs.goto.headers.userAgent;
}
if (configs.goto.timeout !== 60000) {
gotoOptions.timeout = configs.goto.timeout;
let gotoOptionsStr = `\n .goto(\n`;
gotoOptionsStr += `${configs.goto.url}`;
if (configs.goto.headers.Referer || configs.goto.headers.userAgent) {
gotoOptionsStr += ",\n{";
if (configs.goto.headers.Referer) {
gotoOptionsStr += `\nReferer: ${configs.goto.headers.Referer}`;
}
if (configs.goto.headers.userAgent) {
gotoOptionsStr += `${
configs.goto.headers.Referer ? "," : ""
}\nuserAgent: ${configs.goto.headers.userAgent}`;
}
gotoOptionsStr += "\n}";
}
code += `\n .goto('${configs.goto.url}'${
Object.keys(gotoOptions).length
? `,\n${JSON.stringify(gotoOptions, null, 2).replace(/\n/g, "\n ")}`
: ""
})`;
if (configs.goto.timeout !== 60000) {
gotoOptionsStr += `,\n${configs.goto.timeout}`;
}
gotoOptionsStr += "\n)";
code += gotoOptionsStr;
}
// 浏览器操作
@ -43,7 +47,7 @@ export function generateUBrowserCode(configs, selectedActions) {
if (config.type === "time" && config.time) {
code += `\n .wait(${config.time})`;
} else if (config.type === "selector" && config.selector) {
code += `\n .wait('${config.selector}'${
code += `\n .wait(${config.selector}${
config.timeout !== 60000 ? `, ${config.timeout}` : ""
})`;
} else if (config.type === "function" && config.function) {
@ -66,13 +70,13 @@ export function generateUBrowserCode(configs, selectedActions) {
case "click":
if (config.selector) {
code += `\n .click('${config.selector}')`;
code += `\n .click(${config.selector})`;
}
break;
case "css":
if (config.value) {
code += `\n .css('${config.value}')`;
code += `\n .css(${config.value})`;
}
break;
@ -81,32 +85,31 @@ export function generateUBrowserCode(configs, selectedActions) {
const modifiers = config.modifiers.length
? `, ${JSON.stringify(config.modifiers)}`
: "";
code += `\n .press('${config.key}'${modifiers})`;
code += `\n .press(${config.key}${modifiers})`;
}
break;
case "paste":
if (config.text) {
code += `\n .paste('${config.text}')`;
code += `\n .paste(${config.text})`;
}
break;
case "screenshot":
if (config.selector || config.savePath) {
const options = {};
if (config.selector) options.selector = config.selector;
if (config.rect.width && config.rect.height) {
options.rect = config.rect;
}
code += `\n .screenshot('${config.savePath}'${
Object.keys(options).length ? `, ${JSON.stringify(options)}` : ""
if (config.selector) {
code += `\n .screenshot(${config.selector}${
config.savePath ? `, '${config.savePath}'` : ""
})`;
} else if (config.rect) {
code += `\n .screenshot(${JSON.stringify(config.rect)}${
config.savePath ? `, ${config.savePath}` : ""
})`;
}
break;
case "pdf":
if (config.savePath) {
code += `\n .pdf('${config.savePath}'${
code += `\n .pdf(${config.savePath}${
config.options ? `, ${JSON.stringify(config.options)}` : ""
})`;
}
@ -114,39 +117,56 @@ export function generateUBrowserCode(configs, selectedActions) {
case "device":
if (config.type === "preset" && config.deviceName) {
code += `\n .device('${config.deviceName}')`;
code += `\n .device(${config.deviceName})`;
} else if (config.type === "custom") {
const options = {
size: config.size,
};
if (config.useragent) options.useragent = config.useragent;
code += `\n .device(${JSON.stringify(options, null, 2).replace(
/\n/g,
"\n "
)})`;
let deviceOptionsStr = `\n .device(\n{`;
if (config.size) {
deviceOptionsStr += `\nsize: ${JSON.stringify(config.size)}`;
}
if (config.useragent) {
deviceOptionsStr += `${config.size ? "," : ""}\nuserAgent: ${
config.useragent
}`;
}
deviceOptionsStr += "\n}";
code += deviceOptionsStr + "\n)";
}
break;
case "cookies":
if (config.name) {
code += `\n .cookies('${config.name}')`;
code += `\n .cookies(${config.name})`;
} else {
code += `\n .cookies()`;
}
break;
case "setCookies":
if (config.items?.length) {
code += `\n .setCookies(${JSON.stringify(config.items)})`;
let cookiesStr = `\n .setCookies([\n`;
config.items.forEach((item, index) => {
cookiesStr += " {";
if (item.name) cookiesStr += `\n name: ${item.name}`;
if (item.value)
cookiesStr += `${item.name ? "," : ""}\n value: ${
item.value
}}`;
if (index < config.items.length - 1) cookiesStr += ",";
cookiesStr += "\n";
});
cookiesStr += " ])";
code += cookiesStr;
}
break;
case "removeCookies":
if (config.name) {
code += `\n .removeCookies('${config.name}')`;
code += `\n .removeCookies(${config.name})`;
}
break;
case "clearCookies":
code += `\n .clearCookies(${config.url ? `'${config.url}'` : ""})`;
code += `\n .clearCookies(${config.url || ""})`;
break;
case "evaluate":
@ -168,34 +188,38 @@ export function generateUBrowserCode(configs, selectedActions) {
case "when":
if (config.condition) {
code += `\n .when('${config.condition}')`;
code += `\n .when(${config.condition})`;
}
break;
case "mousedown":
case "mouseup":
if (config.selector) {
code += `\n .${action.value}('${config.selector}')`;
code += `\n .${action.value}(${config.selector})`;
}
break;
case "file":
if (config.selector && config.files?.length) {
code += `\n .file('${config.selector}', ${JSON.stringify(
config.files
)})`;
let filesStr = `\n .file(${config.selector}, [\n`;
config.files.forEach((file, index) => {
filesStr += ` ${file}`;
if (index < config.files.length - 1) filesStr += ",\n";
});
filesStr += "\n ])";
code += filesStr;
}
break;
case "value":
if (config.selector) {
code += `\n .value('${config.selector}', '${config.value}')`;
code += `\n .value(${config.selector}, ${config.value})`;
}
break;
case "check":
if (config.selector) {
code += `\n .check('${config.selector}'${
code += `\n .check(${config.selector}${
config.checked !== undefined ? `, ${config.checked}` : ""
})`;
}
@ -203,13 +227,13 @@ export function generateUBrowserCode(configs, selectedActions) {
case "focus":
if (config.selector) {
code += `\n .focus('${config.selector}')`;
code += `\n .focus(${config.selector})`;
}
break;
case "scroll":
if (config.type === "element" && config.selector) {
code += `\n .scroll('${config.selector}')`;
code += `\n .scroll(${config.selector})`;
} else if (config.type === "position") {
if (config.x !== undefined && config.y !== undefined) {
code += `\n .scroll(${config.x}, ${config.y})`;
@ -221,8 +245,8 @@ export function generateUBrowserCode(configs, selectedActions) {
case "download":
if (config.url) {
code += `\n .download('${config.url}'${
config.savePath ? `, '${config.savePath}'` : ""
code += `\n .download(${config.url}${
config.savePath ? `, ${config.savePath}` : ""
})`;
}
break;
@ -234,7 +258,7 @@ export function generateUBrowserCode(configs, selectedActions) {
case "devTools":
if (config.mode) {
code += `\n .devTools('${config.mode}')`;
code += `\n .devTools(${config.mode})`;
} else {
code += `\n .devTools()`;
}

View File

@ -1,6 +1,7 @@
// ubrowser 浏览器操作配置
export const ubrowserOperationConfigs = {
wait: {
export const ubrowserOperationConfigs = [
{
value: "wait",
label: "等待",
config: [
{
@ -56,7 +57,8 @@ export const ubrowserOperationConfigs = {
],
icon: "timer",
},
click: {
{
value: "click",
label: "点击",
config: [
{
@ -68,7 +70,8 @@ export const ubrowserOperationConfigs = {
],
icon: "mouse",
},
css: {
{
value: "css",
label: "注入CSS",
config: [
{
@ -80,7 +83,8 @@ export const ubrowserOperationConfigs = {
],
icon: "style",
},
press: {
{
value: "press",
label: "按键",
config: [
{
@ -106,7 +110,8 @@ export const ubrowserOperationConfigs = {
],
icon: "keyboard",
},
paste: {
{
value: "paste",
label: "粘贴",
config: [
{
@ -118,7 +123,8 @@ export const ubrowserOperationConfigs = {
],
icon: "content_paste",
},
viewport: {
{
value: "viewport",
label: "视窗",
config: [
{
@ -140,7 +146,8 @@ export const ubrowserOperationConfigs = {
],
icon: "crop",
},
screenshot: {
{
value: "screenshot",
label: "截图",
config: [
{ key: "selector", label: "元素选择器", icon: "crop", type: "input" },
@ -180,7 +187,8 @@ export const ubrowserOperationConfigs = {
],
icon: "picture_as_pdf",
},
pdf: {
{
value: "pdf",
label: "导出PDF",
config: [
{
@ -205,7 +213,8 @@ export const ubrowserOperationConfigs = {
],
icon: "devices",
},
device: {
{
value: "device",
label: "模拟设备",
config: [
{
@ -245,26 +254,44 @@ export const ubrowserOperationConfigs = {
showValue: "custom",
},
],
icon: "phone_iphone",
},
{
value: "cookies",
label: "获取Cookie",
config: [
{
key: "name",
label: "Cookie名称",
icon: "cookie",
type: "input",
width: 12,
},
],
icon: "cookie",
},
setCookies: {
{
value: "setCookies",
label: "设置Cookie",
config: [{ key: "items", label: "Cookie列表", type: "cookie-list" }],
icon: "cookie",
},
removeCookies: {
{
value: "removeCookies",
label: "删除Cookie",
config: [
{ key: "name", label: "Cookie名称", icon: "cookie", type: "input" },
],
icon: "cookie",
},
clearCookies: {
{
value: "clearCookies",
label: "清空Cookie",
config: [{ key: "url", label: "URL(可选)", icon: "link", type: "input" }],
icon: "cookie",
},
evaluate: {
{
value: "evaluate",
label: "执行代码",
config: [
{
@ -277,7 +304,8 @@ export const ubrowserOperationConfigs = {
],
icon: "code",
},
when: {
{
value: "when",
label: "条件判断",
config: [
{
@ -322,12 +350,14 @@ export const ubrowserOperationConfigs = {
],
icon: "rule",
},
end: {
{
value: "end",
label: "结束条件",
config: [],
icon: "stop",
},
mousedown: {
{
value: "mousedown",
label: "按下鼠标",
config: [
{
@ -339,7 +369,8 @@ export const ubrowserOperationConfigs = {
],
icon: "mouse",
},
mouseup: {
{
value: "mouseup",
label: "释放鼠标",
config: [
{
@ -351,7 +382,8 @@ export const ubrowserOperationConfigs = {
],
icon: "mouse",
},
file: {
{
value: "file",
label: "上传文件",
config: [
{
@ -364,7 +396,8 @@ export const ubrowserOperationConfigs = {
],
icon: "upload_file",
},
value: {
{
value: "value",
label: "设置值",
config: [
{
@ -384,7 +417,8 @@ export const ubrowserOperationConfigs = {
],
icon: "check_box",
},
check: {
{
value: "check",
label: "设置选中",
config: [
{
@ -404,7 +438,8 @@ export const ubrowserOperationConfigs = {
],
icon: "center_focus_strong",
},
focus: {
{
value: "focus",
label: "聚焦元素",
config: [
{
@ -416,7 +451,8 @@ export const ubrowserOperationConfigs = {
],
icon: "swap_vert",
},
scroll: {
{
value: "scroll",
label: "滚动",
config: [
{
@ -461,7 +497,8 @@ export const ubrowserOperationConfigs = {
],
icon: "download",
},
download: {
{
value: "download",
label: "下载",
config: [
{
@ -481,7 +518,8 @@ export const ubrowserOperationConfigs = {
],
icon: "download",
},
devTools: {
{
value: "devTools",
label: "开发工具",
config: [
{
@ -499,7 +537,19 @@ export const ubrowserOperationConfigs = {
],
icon: "developer_board",
},
};
{
value: "hide",
label: "隐藏",
config: [],
icon: "visibility_off",
},
{
value: "show",
label: "显示",
config: [],
icon: "visibility",
},
];
// 添加默认运行配置
const defaultUBrowserRunConfigs = {