优化ArrayDict/DictEditor参数传递,修复formatString处理Array时的BUG

This commit is contained in:
fofolee 2025-01-10 18:54:11 +08:00
parent 41b3501945
commit 5e70c4c9a0
10 changed files with 372 additions and 371 deletions

View File

@ -6,7 +6,6 @@ const quickcomposer = {
network: require("./quickcomposer/network"),
coding: require("./quickcomposer/coding"),
math: require("./quickcomposer/math"),
ui: require("./quickcomposer/ui"),
audio: require("./quickcomposer/audio"),
image: require("./quickcomposer/image"),
};

View File

@ -1,50 +0,0 @@
const showSaveDialog = (
title,
defaultPath,
buttonLabel,
message,
extensions,
properties
) => {
return window.utools.showSaveDialog({
title,
defaultPath,
buttonLabel,
message,
properties,
filters: [
{
name: "文件",
extensions,
},
],
});
};
const showOpenDialog = (
title,
defaultPath,
buttonLabel,
message,
extensions,
properties
) => {
return window.utools.showOpenDialog({
title,
defaultPath,
buttonLabel,
message,
properties,
filters: [
{
name: "文件",
extensions,
},
],
});
};
module.exports = {
showSaveDialog,
showOpenDialog,
};

View File

@ -1,6 +0,0 @@
const { showSaveDialog, showOpenDialog } = require("./dialog");
module.exports = {
showSaveDialog,
showOpenDialog,
};

View File

@ -96,7 +96,7 @@ export default defineComponent({
},
saveFlow() {
const flow = window.lodashM.cloneDeep(this.commandFlow);
const uselessProps = ["config", "argvs", "label", "type"];
const uselessProps = ["config", "argvs", "label", "component", "subCommands", "options", "defaultValue"];
//
flow.forEach((cmd) => {
for (const props of uselessProps) {

View File

@ -6,25 +6,21 @@
:model-value="isCollapse"
>
<div class="array-editor">
<div v-for="(item, index) in items" :key="index" class="row items-center">
<template v-if="optionsKeys.length">
<div v-for="(row, index) in rows" :key="index" class="row items-center">
<template v-if="columns">
<div
v-for="key in optionsKeys"
:key="key.value"
v-for="column in processedColumns"
:key="column.key"
:class="[
key.width ? `col-${key.width}` : 'col',
optionsKeys.length > 1 ? 'q-pr-sm' : '',
column.width ? `col-${column.width}` : 'col',
Object.keys(columns).length > 1 ? 'q-pr-sm' : '',
]"
>
<VariableInput
:model-value="item[key.value] || key.defaultValue"
:label="key.label"
:no-icon="true"
:placeholder="key.placeholder"
:options="key.options"
:disable-toggle-type="key.disableToggleType"
:model-value="row[column.key]"
v-bind="column"
@update:model-value="
(val) => updateItemKeyValue(index, key.value, val)
(val) => updateColumn(index, column.key, val)
"
/>
</div>
@ -32,38 +28,32 @@
<template v-else>
<div class="col">
<VariableInput
:model-value="item"
:label="`${label || '项目'} ${index + 1}`"
:icon="icon || 'code'"
:placeholder="placeholder"
:options="{
items: options.items,
}"
:disable-toggle-type="disableToggleType"
@update:model-value="(val) => updateItemValue(index, val)"
:model-value="row"
v-bind="$attrs"
@update:model-value="(val) => updateValue(index, val)"
/>
</div>
</template>
<div class="col-auto">
<div class="btn-container">
<template v-if="items.length === 1">
<template v-if="rows.length === 1">
<q-btn
flat
dense
size="sm"
icon="add"
class="center-btn"
@click="addItem"
@click="addRow"
/>
</template>
<template v-else-if="index === items.length - 1">
<template v-else-if="index === rows.length - 1">
<q-btn
flat
dense
size="sm"
icon="remove"
class="top-btn"
@click="removeItem(index)"
@click="removeRow(index)"
/>
<q-btn
flat
@ -71,7 +61,7 @@
size="sm"
icon="add"
class="bottom-btn"
@click="addItem"
@click="addRow"
/>
</template>
<template v-else>
@ -81,7 +71,7 @@
size="sm"
icon="remove"
class="center-btn"
@click="removeItem(index)"
@click="removeRow(index)"
/>
</template>
</div>
@ -94,63 +84,62 @@
<script>
/**
* 数组编辑器组件
* @description 支持数组和多键对象数组的编辑
* @description 支持单数组和对象数组的编辑
*
* @property {Array} modelValue - 绑定的数组值
* @property {String} label - 输入框标签
* @property {String} icon - 输入框图标
* @property {Object} options - 配置选项
* @property {Object} columns - 列配置可选
*
* @property {String[]} [options.keys] - 多键对象模式的键名列表
* @property {String[]} [options.keys.value] - 元素为对象时对象的键名
* @property {String[]} [options.keys.label] - 对应varInput的label
* @property {String[]} [options.keys.placeholder] - 对应varInput的placeholder
* @property {String[]} [options.keys.defaultValue] - 对应varInput的defaultValue
* @property {Object} [options.keys.options] - 对应varInput的options
*
* @property {String[]} [options.items] - 下拉选择模式的选项列表
* @property {Object} [options.defaultValue] - 初始化时默认的值决定显示几个元素对应元素内容
* @example
* //
* // 1. columns
* //
* [
* newVarInputVal("str", "张三")
* newVarInputVal('str', '选项1'),
* newVarInputVal('str', '选项2')
* ]
*
* //
* options.keys = ['name', 'age', 'email']
* 属性透传
* ArrayEditor属性全部透传给VariableInput
*
* options.keys= [
* {
* label: "姓名",
* value: "name",
* placeholder: "姓名",
* 初始值
* 使用defaultValue属性设置初始值
* 使用defaultRowValue属性设置新增行时的初始值
*
* // 2. columns
* columns = {
* name: {
* label: '姓名',
* placeholder: '请输入姓名',
* width: 6,
* options: {
* items: ["张三", "李四", "王五"],
* multiSelect: true,
* items: ['张三', '李四', '王五']
* },
* defaultValue: newVarInputVal('str', '张三')
* },
* age: {
* label: '年龄',
* placeholder: '请输入年龄',
* width: 4,
* defaultValue: newVarInputVal('str', '18')
* }
* ]
*
* }
* //
* [
* {
* name: newVarInputVal("str", "张三"),
* age: newVarInputVal("str", "18"),
* email: newVarInputVal("str", "zhangsan@example.com")
* name: newVarInputVal('str', '张三'),
* age: newVarInputVal('str', '18')
* }
* ]
*
* //
* options.items = ['选项1', '选项2', '选项3']
* [
* newVarInputVal("str", "选项1"),
* newVarInputVal("str", "选项2"),
* newVarInputVal("str", "选项3")
* ]
* 属性透传
* columns的每一个对象的值的属性全部透传给VariableInput
*
* 初始值
* 使用defaultValue属性设置初始的每一行
* columns的每一个对象的defaultValue属性设置新增行时的初始值
*/
import { defineComponent } from "vue";
import VariableInput from "components/composer/common/VariableInput.vue";
import VariableInput from "./VariableInput.vue";
import { newVarInputVal } from "js/composer/varInputValManager";
import BorderLabel from "components/composer/common/BorderLabel.vue";
import BorderLabel from "./BorderLabel.vue";
export default defineComponent({
name: "ArrayEditor",
@ -164,9 +153,17 @@ export default defineComponent({
required: true,
default: () => [],
},
label: {
type: String,
default: "",
columns: {
type: Object,
default: null,
},
defaultValue: {
type: [Object, Array],
default: null,
},
defaultRowValue: {
type: [Object, String],
default: null,
},
topLabel: {
type: String,
@ -180,103 +177,89 @@ export default defineComponent({
type: Boolean,
default: true,
},
options: {
type: Object,
default: () => ({}),
},
placeholder: {
type: String,
default: "",
},
disableToggleType: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue"],
computed: {
items() {
return this.modelValue.length ? this.modelValue : this.initializeItems();
rows() {
return this.modelValue.length ? this.modelValue : this.initializeRow();
},
optionsKeys() {
return (
this.options?.keys?.map((key) => {
return {
...key,
value: key.value || key,
label: key.label || key,
};
}) || []
);
processedColumns() {
if (!this.columns) return null;
return Object.entries(this.columns).map(([key, config]) => ({
key,
...config,
width: config.width || null,
defaultValue: config.defaultValue || newVarInputVal("str"),
}));
},
},
methods: {
initializeItems() {
if (this.optionsKeys?.length) {
const item = {};
this.optionsKeys.forEach((key) => {
item[key] = newVarInputVal("str");
});
return [item];
initializeRow() {
if (!this.columns) {
// 使 defaultValue VarInputVal
return this.defaultValue || [newVarInputVal("str")];
}
return [newVarInputVal("str")];
},
/**
* 添加新的数组项
* 根据配置创建相应的数据结构
*/
addItem() {
let newItems = [];
if (this.options.keys) {
const newItem = {};
this.options.keys.forEach((key) => {
newItem[key] = newVarInputVal("str");
});
newItems = [...this.items, newItem];
} else {
newItems = [...this.items, newVarInputVal("str")];
//
if (this.defaultValue) {
return [this.defaultValue];
}
this.$emit("update:modelValue", newItems);
const row = {};
Object.entries(this.columns).forEach(([key, config]) => {
row[key] = config.defaultValue || newVarInputVal("str");
});
return [row];
},
/**
* 移除指定索引的数组项
* 如果移除后数组为空则创建一个新的空项
*/
removeItem(index) {
const newItems = [...this.items];
newItems.splice(index, 1);
if (newItems.length === 0) {
if (this.options.keys) {
const newItem = {};
this.options.keys.forEach((key) => {
newItem[key] = newVarInputVal("str");
});
newItems.push(newItem);
} else {
newItems.push(newVarInputVal("str"));
getNewRowValue() {
if (!this.columns) {
// 使 defaultRowValue VarInputVal
return this.defaultRowValue || newVarInputVal("str");
}
//
if (this.defaultRowValue) {
return this.defaultRowValue;
}
const row = {};
Object.entries(this.columns).forEach(([key, config]) => {
row[key] = config.defaultValue || newVarInputVal("str");
});
return row;
},
addRow() {
const newRows = [...this.rows, this.getNewRowValue()];
this.$emit("update:modelValue", newRows);
},
removeRow(index) {
const newRows = [...this.rows];
newRows.splice(index, 1);
if (newRows.length === 0) {
newRows.push(this.initializeRow()[0]);
}
this.$emit("update:modelValue", newRows);
},
updateValue(index, value) {
const newRows = [...this.rows];
newRows[index] = value;
this.$emit("update:modelValue", newRows);
},
updateColumn(index, columnKey, value) {
//
const newRows = this.rows.map((row, i) => {
//
if (i === index) {
return {
...row,
[columnKey]: value,
};
}
}
this.$emit("update:modelValue", newItems);
},
/**
* 更新单值模式下的值
*/
updateItemValue(index, value) {
const newItems = [...this.items];
newItems[index] = value;
this.$emit("update:modelValue", newItems);
},
/**
* 更新多键模式下指定键的值
*/
updateItemKeyValue(index, key, value) {
const newItems = [...this.items];
newItems[index] = {
...newItems[index],
[key]: value,
};
this.$emit("update:modelValue", newItems);
return row;
});
this.$emit("update:modelValue", newRows);
},
},
});

View File

@ -157,7 +157,7 @@ import BorderLabel from "components/composer/common/BorderLabel.vue";
* }
*
* //
* options.items = ['User-Agent', 'Content-Type', 'Accept']
* options.optionKeys = ['User-Agent', 'Content-Type', 'Accept']
* {
* "User-Agent": newVarInputVal("str", "Mozilla/5.0"),
* "Content-Type": newVarInputVal("str", "text/html"),

View File

@ -130,23 +130,8 @@
<template v-if="argvs.optionType === 'json'">
<ArrayEditor
:model-value="argvs.selects"
:options="{
keys: [
{
value: 'id',
label: 'id',
width: 3,
},
{
value: 'title',
label: '标题',
},
{
value: 'description',
label: '描述',
},
],
}"
:columns="arrayEditorColumns"
:default-row-value="arrayEditorDefaultRowValue"
@update:model-value="updateArgvs('selects', $event)"
>
<template #header>
@ -178,8 +163,7 @@ import VariableInput from "../common/VariableInput.vue";
import ArrayEditor from "../common/ArrayEditor.vue";
import OperationCard from "../common/OperationCard.vue";
import { parseFunction, stringifyArgv } from "js/composer/formatString";
import { newVarInputVal, isVarInputVal} from "js/composer/varInputValManager";
import { newVarInputVal, isVarInputVal } from "js/composer/varInputValManager";
const jsonDefaultSelects = new Array(3).fill().map((_, index) => ({
id: newVarInputVal("var", index),
@ -226,6 +210,28 @@ export default defineComponent({
showCancelButton: false,
closeOnSelect: true,
},
arrayEditorDefaultRowValue: {
id: newVarInputVal("var"),
title: newVarInputVal("str"),
description: newVarInputVal("str"),
},
arrayEditorColumns: {
id: {
label: "id",
width: 3,
noIcon: true,
},
title: {
label: "标题",
width: 4,
noIcon: true,
},
description: {
label: "描述",
width: 4,
noIcon: true,
},
},
};
},
computed: {

View File

@ -335,6 +335,7 @@ export const dataCommands = {
icon: "memory",
width: 12,
defaultValue: [newVarInputVal("var")],
defaultRowValue: newVarInputVal("var"),
disableToggleType: true,
},
{

View File

@ -1,5 +1,109 @@
import { newVarInputVal } from "js/composer/varInputValManager";
const SAVE_DIALOG_PROPERTIES = {
component: "CheckGroup",
icon: "settings",
label: "选项",
width: 12,
options: [
{ label: "显示隐藏文件", value: "showHiddenFiles" },
{ label: "允许创建文件夹Mac", value: "createDirectory" },
{
label: "将.App作为目录Mac",
value: "treatPackageAsDirectory",
},
{
label: "显示覆盖确认Linux",
value: "showOverwriteConfirmation",
},
{ label: "不添加到最近Win", value: "dontAddToRecent" },
],
};
const OPEN_DIALOG_PROPERTIES = {
...SAVE_DIALOG_PROPERTIES,
options: [
{ label: "选择文件", value: "openFile" },
{ label: "选择文件夹", value: "openDirectory" },
{ label: "允许多选", value: "multiSelections" },
{ label: "显示隐藏文件", value: "showHiddenFiles" },
{ label: "提示新建路径Win", value: "promptToCreate" },
{ label: "不添加到最近Win", value: "dontAddToRecent" },
{ label: "允许创建文件夹Mac", value: "createDirectory" },
{ label: "不解析符号链接Mac", value: "noResolveAliases" },
{
label: "将.App作为目录Mac",
value: "treatPackageAsDirectory",
},
],
};
const DIALOG_CONFIG = {
options: {
title: {
label: "标题",
component: "VariableInput",
icon: "title",
width: 6,
},
defaultPath: {
label: "默认路径",
component: "VariableInput",
icon: "folder",
width: 6,
},
buttonLabel: {
label: "按钮文本",
component: "VariableInput",
icon: "text_fields",
width: 6,
},
message: {
label: "提示信息",
component: "VariableInput",
icon: "info",
width: 6,
},
filters: {
topLabel: "过滤器",
component: "ArrayEditor",
icon: "filter_list",
width: 12,
defaultRowValue: [newVarInputVal("str"), newVarInputVal("var", "")],
columns: {
name: {
label: "文件类型",
noIcon: true,
width: 4,
},
extensions: {
label: "扩展名",
noIcon: true,
width: 7,
disableToggleType: true,
options: {
items: ["*", "jpg", "png", "gif", "txt", "json", "exe"],
multiSelect: true,
},
},
},
},
},
defaultValue: {
title: newVarInputVal("str", "请选择"),
defaultPath: newVarInputVal("str"),
buttonLabel: newVarInputVal("str", "选择"),
message: newVarInputVal("str", "请选择"),
filters: [
{
name: newVarInputVal("str", "file"),
extensions: newVarInputVal("var", '["*"]'),
},
],
properties: ["openFile", "showHiddenFiles"],
},
};
export const uiCommands = {
label: "用户交互",
icon: "web",
@ -19,6 +123,7 @@ export const uiCommands = {
newVarInputVal("str", "是"),
newVarInputVal("str", "否"),
],
defaultRowValue: newVarInputVal("str"),
},
],
},
@ -33,17 +138,13 @@ export const uiCommands = {
label: "输入框",
component: "ArrayEditor",
width: 12,
options: {
keys: [
{
label: "标签",
value: "label",
},
{
label: "默认值",
value: "value",
},
],
columns: {
label: {
label: "标签",
},
value: {
label: "默认值",
},
},
defaultValue: [
{
@ -163,106 +264,43 @@ export const uiCommands = {
],
},
{
value: "quickcomposer.ui.showOpenDialog",
value: "utools.showOpenDialog",
label: "文件选择框",
desc: "显示一个文件选择框,返回选择的文件路径",
outputVariable: "filePaths",
saveOutput: true,
config: [
{
label: "标题",
component: "VariableInput",
defaultValue: newVarInputVal("str", "请选择文件"),
width: 6,
},
{
label: "默认路径",
component: "VariableInput",
defaultValue: newVarInputVal("str"),
width: 6,
placeholder: "默认打开的路径",
},
{
label: "按钮文本",
component: "VariableInput",
defaultValue: newVarInputVal("str", "选择"),
width: 3,
},
{
label: "提示信息",
component: "VariableInput",
defaultValue: newVarInputVal("str"),
width: 3,
placeholder: "对话框底部的提示信息",
defaultValue: newVarInputVal("str", "请选择"),
},
{
label: "扩展名",
component: "VariableInput",
width: 6,
options: {
items: ["*", "jpg", "png", "gif", "txt", "json", "exe"],
multiSelect: true,
},
defaultValue: newVarInputVal("var", '["*"]'),
disableToggleType: true,
},
],
subCommands: [
{
value: "quickcomposer.ui.showOpenDialog",
value: "utools.showOpenDialog",
label: "打开文件对话框",
desc: "打开文件对话框",
icon: "folder_open",
config: [
{
label: "选择选项",
component: "CheckGroup",
icon: "settings",
width: 12,
options: [
{ label: "选择文件", value: "openFile" },
{ label: "选择文件夹", value: "openDirectory" },
{ label: "允许多选", value: "multiSelections" },
{ label: "显示隐藏文件", value: "showHiddenFiles" },
{ label: "提示新建路径Win", value: "promptToCreate" },
{ label: "不添加到最近Win", value: "dontAddToRecent" },
{ label: "允许创建文件夹Mac", value: "createDirectory" },
{ label: "不解析符号链接Mac", value: "noResolveAliases" },
{
label: "将.App作为目录Mac",
value: "treatPackageAsDirectory",
},
],
defaultValue: ["openFile", "showHiddenFiles"],
label: "选项",
component: "OptionEditor",
defaultValue: DIALOG_CONFIG.defaultValue,
options: {
...DIALOG_CONFIG.options,
properties: OPEN_DIALOG_PROPERTIES,
},
},
],
},
{
value: "quickcomposer.ui.showSaveDialog",
value: "utools.showSaveDialog",
label: "保存文件对话框",
desc: "保存文件对话框",
icon: "save",
config: [
{
label: "选择选项",
component: "CheckGroup",
icon: "settings",
width: 12,
options: [
{ label: "显示隐藏文件", value: "showHiddenFiles" },
{ label: "允许创建文件夹Mac", value: "createDirectory" },
{
label: "将.App作为目录Mac",
value: "treatPackageAsDirectory",
},
{
label: "显示覆盖确认Linux",
value: "showOverwriteConfirmation",
},
{ label: "不添加到最近Win", value: "dontAddToRecent" },
],
defaultValue: ["showHiddenFiles"],
label: "选项",
component: "OptionEditor",
defaultValue: DIALOG_CONFIG.defaultValue,
options: {
...DIALOG_CONFIG.options,
properties: SAVE_DIALOG_PROPERTIES,
},
},
],
},

View File

@ -6,69 +6,101 @@ import {
} from "./varInputValManager";
/**
* 递归移除对象中的空值
* @param {Object} obj 要处理的对象
* @returns {Object} 处理后的对象
* 处理单个值返回格式化后的字符串
*/
const removeEmptyValues = (obj) => {
return window.lodashM.omitBy(obj, (value) => {
// 如果value是VariableInput的输出则取其value值
const realValue = isVarInputVal(value) ? value.value : value;
if (window.lodashM.isNil(realValue) || realValue === "") return true;
// 如果value是对象并且不是VariableInput的输出则递归移除空值
if (typeof value === "object" && !isVarInputVal(value))
return window.lodashM.isEmpty(removeEmptyValues(value));
return false;
});
const processValue = (value, parentPath = "") => {
if (!value) return value;
if (typeof value === "object") {
if (isVarInputVal(value)) {
return stringifyVarInputVal(value);
}
return processObject(value, parentPath);
}
return typeof value === "string" ? `"${value}"` : value;
};
/**
* 格式化带缩进的值
*/
const formatWithIndent = (value, indent, isLast = true) => {
return `${indent}${value}${isLast ? "" : ","}`;
};
/**
* 递归处理对象的值并格式化成字符串
* @param {Object} obj 要处理的对象
* @param {string} parentPath 父路径(用于缩进)
* @returns {string} 处理后的字符串
*/
const processObject = (obj, parentPath = "") => {
// 移除空值
obj = removeEmptyValues(obj);
// 如果是 VariableInput 的输出,直接用 stringifyVarInputVal 处理
if (isVarInputVal(obj)) {
return stringifyVarInputVal(obj);
}
let result = "{\n";
const indentLevel = parentPath.split(".").length;
const indent = " ".repeat(indentLevel + 1);
const closingIndent = " ".repeat(indentLevel);
// 处理数组
if (Array.isArray(obj)) {
if (obj.length === 0) return "[]";
const items = obj.map((item, index) =>
formatWithIndent(
processValue(item, parentPath + " "),
indent,
index === obj.length - 1
)
);
return `[\n${items.join("\n")}\n${closingIndent}]`;
}
// 处理对象
const entries = Object.entries(obj);
if (entries.length === 0) return "{}";
entries.forEach(([key, value], index) => {
let valueStr = "";
const items = entries.map(([key, value], index) =>
formatWithIndent(
`"${key}": ${processValue(value, parentPath + " ")}`,
indent,
index === entries.length - 1
)
);
// 处理对象类型
if (value && typeof value === "object") {
// 如果是 VariableInput 的输出,直接用 stringifyVarInputVal 处理
if (isVarInputVal(value)) {
valueStr = stringifyVarInputVal(value);
} else {
valueStr = processObject(value, parentPath + " ");
}
}
// 处理其他类型
else if (value && typeof value === "string") {
valueStr = `"${value}"`;
} else {
valueStr = value;
return `{\n${items.join("\n")}\n${closingIndent}}`;
};
/**
* 递归移除对象中的空值
*/
const removeEmptyValues = (obj) => {
const isEmptyValue = (value) => {
const realValue = isVarInputVal(value) ? value.value : value;
return window.lodashM.isNil(realValue) || realValue === "";
};
const processObjectValue = (value) => {
if (typeof value === "object" && !isVarInputVal(value)) {
return removeEmptyValues(value);
}
return value;
};
// 添加缩进
const indent = " ".repeat(parentPath.split(".").length + 1);
result += `${indent}"${key}": ${valueStr}`;
if (index < entries.length - 1) result += ",";
result += "\n";
});
if (Array.isArray(obj)) {
return obj.filter((value) => !isEmptyValue(value)).map(processObjectValue);
}
// 闭合括号的缩进
const closingIndent = " ".repeat(parentPath.split(".").length);
result += `${closingIndent}}`;
return result;
return window.lodashM.omitBy(
obj,
(value) =>
isEmptyValue(value) ||
(typeof value === "object" &&
!isVarInputVal(value) &&
window.lodashM.isEmpty(removeEmptyValues(value)))
);
};
/**
@ -264,9 +296,7 @@ export const parseFunction = (functionStr, options = {}) => {
: node.value;
// null
case "NullLiteral":
return shouldUseVariableFormat
? newVarInputVal("var", "null")
: null;
return shouldUseVariableFormat ? newVarInputVal("var", "null") : null;
case "Identifier":
// 标识符(变量)总是不带引号的
return shouldUseVariableFormat