各种调整

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);
// 只在值真正需要更新时才<E697B6><E6898D><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 = {