用户交互新增选择对话框,VarInput支持多选,新增CheckGroup组件

This commit is contained in:
fofolee 2025-01-09 16:37:41 +08:00
parent fe26f98809
commit 036b6fa934
14 changed files with 507 additions and 60 deletions

View File

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

View File

@ -0,0 +1,50 @@
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

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

View File

@ -197,7 +197,6 @@ export default defineComponent({
.composer-card .command-item {
transition: none !important;
transform: none !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
}
.command-item {

View File

@ -113,7 +113,6 @@ export default defineComponent({
* 变量模式stringify后null变成'null', ''保持''
*/
const stringifiedArgvs = argvs.map((argv) => stringifyArgv(argv));
/*
* 1. 去掉 undefined'', null
* 2. varInput在字符串模式下留空为'""'所以不会被处理

View File

@ -17,9 +17,11 @@
]"
>
<VariableInput
:model-value="item[key.value]"
:model-value="item[key.value] || key.defaultValue"
:label="key.label"
:no-icon="true"
:placeholder="key.placeholder"
:options="key.options"
@update:model-value="
(val) => updateItemKeyValue(index, key.value, val)
"
@ -32,6 +34,7 @@
:model-value="item"
:label="`${label || '项目'} ${index + 1}`"
:icon="icon || 'code'"
:placeholder="placeholder"
:options="{
items: options.items,
}"
@ -95,9 +98,16 @@
* @property {String} label - 输入框标签
* @property {String} icon - 输入框图标
* @property {Object} options - 配置选项
* @property {String[]} [options.keys] - 多键对象模式的键名列表
* @property {String[]} [options.items] - 下拉选择模式的选项列表
*
* @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
* //
* [
@ -106,6 +116,19 @@
*
* //
* options.keys = ['name', 'age', 'email']
*
* options.keys= [
* {
* label: "姓名",
* value: "name",
* placeholder: "姓名",
* options: {
* items: ["张三", "李四", "王五"],
* multiSelect: true,
* },
* }
* ]
*
* [
* {
* name: newVarInputVal("str", "张三"),
@ -159,6 +182,10 @@ export default defineComponent({
type: Object,
default: () => ({}),
},
placeholder: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
computed: {
@ -169,9 +196,9 @@ export default defineComponent({
return (
this.options?.keys?.map((key) => {
return {
...key,
value: key.value || key,
label: key.label || key,
width: key.width,
};
}) || []
);

View File

@ -1,20 +1,18 @@
<template>
<div class="border-label" :class="{ collapsed }" :data-label="label">
<div class="label-header row items-center" @click="toggleCollapse">
<q-icon
v-if="icon"
:name="icon"
size="16px"
class="collapse-icon"
/>
<div class="label-text">{{ label }}</div>
<q-icon
:name="collapsed ? 'expand_more' : 'expand_less'"
size="16px"
class="collapse-icon"
/>
<div class="label-text row items-center">
<q-icon
v-if="icon"
:name="icon"
size="16px"
class="collapse-icon q-pl-sm"
/>
<div class="label-text">{{ label }}</div>
</div>
</div>
<div class="content" :class="{ collapsed }">
<slot></slot>

View File

@ -0,0 +1,181 @@
<template>
<component
:is="!!label ? 'BorderLabel' : 'div'"
:label="label"
:icon="icon"
:model-value="isCollapse"
>
<div class="check-btn-group">
<q-btn
v-for="option in options"
:key="option.value"
:color="isSelected(option.value) ? 'primary' : 'grey-7'"
:flat="!isSelected(option.value)"
:outline="isSelected(option.value)"
dense
:class="[
'check-btn',
{ 'check-btn--selected': isSelected(option.value) },
]"
:style="{
flex: `1 0 ${100 / options.length}%`,
}"
@click="toggleOption(option.value)"
>
<template #default>
<div class="row items-center full-width">
<div class="check-btn-content">
<div class="check-btn-label">{{ option.label }}</div>
</div>
<q-icon
:name="
isSelected(option.value)
? 'check_circle'
: 'radio_button_unchecked'
"
size="14px"
class="q-ml-xs check-btn-icon"
/>
</div>
<q-tooltip v-if="option.tooltip">{{ option.tooltip }}</q-tooltip>
</template>
</q-btn>
</div>
</component>
</template>
<script>
import { defineComponent } from "vue";
import BorderLabel from "components/composer/common/BorderLabel.vue";
export default defineComponent({
name: "CheckGroup",
components: { BorderLabel },
props: {
modelValue: {
type: Array,
default: () => [],
},
options: {
type: Array,
required: true,
validator: (options) =>
options.every((opt) => opt.label && opt.value !== undefined),
},
label: {
type: String,
default: "",
},
icon: {
type: String,
default: "",
},
isCollapse: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
methods: {
isSelected(value) {
return this.modelValue.includes(value);
},
toggleOption(value) {
const newValue = [...this.modelValue];
const index = newValue.indexOf(value);
if (index === -1) {
newValue.push(value);
} else {
newValue.splice(index, 1);
}
this.$emit("update:model-value", newValue);
},
},
});
</script>
<style scoped>
.check-btn-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
width: 100%;
}
.check-btn {
min-width: fit-content !important;
max-width: 100% !important;
height: auto !important;
min-height: 32px;
font-size: 12px;
padding: 4px 12px;
border-radius: 4px !important;
transition: all 0.3s;
background-color: rgba(0, 0, 0, 0.03);
}
.check-btn :deep(.q-btn__content) {
min-width: 0;
height: auto;
white-space: normal;
}
.check-btn-content {
flex: 1;
min-width: 0;
margin-right: 4px;
}
.check-btn-label {
text-align: center;
line-height: 1.2;
word-break: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.check-btn-icon {
flex: none;
opacity: 0.8;
transition: all 0.3s;
margin-top: 2px;
}
.check-btn--selected .check-btn-icon {
opacity: 1;
transform: scale(1.1);
}
/* 移除按钮组默认的边框合并样式 */
.check-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
border-color: var(--q-primary);
}
/* 暗色模式适配 */
.body--dark .check-btn {
background-color: rgba(255, 255, 255, 0.03);
}
.check-btn--selected {
background-color: transparent !important;
border-color: var(--q-primary) !important;
}
.check-btn.q-btn--flat {
color: var(--q-primary);
opacity: 0.8;
}
.body--dark .check-btn.q-btn--flat {
color: rgba(255, 255, 255, 0.9);
}
/* 选中状态的按钮样式 */
.check-btn.q-btn--outline {
opacity: 1;
background-color: transparent;
}
</style>

View File

@ -152,6 +152,10 @@ export default defineComponent({
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
isCollapse: {
type: Boolean,
default: true,

View File

@ -10,51 +10,37 @@
v-if="config.type === 'controlInput'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:label="config.label"
:placeholder="config.placeholder"
:icon="config.icon"
v-bind="config"
/>
<VariableInput
v-else-if="config.type === 'varInput'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:label="config.label"
:placeholder="config.placeholder"
:icon="config.icon"
:options="config.options"
v-bind="config"
/>
<NumberInput
v-else-if="config.type === 'numInput'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:label="config.label"
:icon="config.icon"
:placeholder="config.placeholder"
v-bind="config"
/>
<ArrayEditor
v-else-if="config.type === 'arrayEditor'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:label="config.label"
:options="config.options"
:icon="config.icon"
:is-collapse="config.isCollapse"
v-bind="config"
/>
<DictEditor
v-else-if="config.type === 'dictEditor'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:label="config.label"
:options="config.options"
:icon="config.icon"
:is-collapse="config.isCollapse"
v-bind="config"
/>
<q-toggle
v-else-if="config.type === 'switch'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:label="config.label"
:icon="config.icon"
v-bind="config"
/>
<q-select
v-else-if="config.type === 'select'"
@ -63,7 +49,7 @@
map-options
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:options="config.options"
v-bind="config"
>
<template v-slot:prepend>
<q-icon :name="config.icon || 'code'" />
@ -74,9 +60,7 @@
filled
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:label="config.label"
:icon="config.icon"
:placeholder="config.placeholder"
v-bind="config"
>
<template v-slot:prepend>
<q-icon :name="config.icon || 'code'" />
@ -86,14 +70,19 @@
v-else-if="config.type === 'checkbox'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:label="config.label"
:icon="config.icon"
v-bind="config"
/>
<ButtonGroup
v-else-if="config.type === 'buttonGroup'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
:options="config.options"
v-bind="config"
/>
<CheckGroup
v-else-if="config.type === 'checkGroup'"
:model-value="values[index]"
@update:model-value="$emit('update', index, $event)"
v-bind="config"
/>
</div>
</div>
@ -107,6 +96,7 @@ import ArrayEditor from "./ArrayEditor.vue";
import DictEditor from "./DictEditor.vue";
import ButtonGroup from "./ButtonGroup.vue";
import ControlInput from "./ControlInput.vue";
import CheckGroup from "./CheckGroup.vue";
/**
* 参数输入组件
@ -128,6 +118,7 @@ export default defineComponent({
DictEditor,
ButtonGroup,
ControlInput,
CheckGroup,
},
props: {
configs: {

View File

@ -45,18 +45,47 @@
class="options-dropdown prepend-btn"
>
<q-list class="options-item-list">
<q-item
v-for="item in normalizedItems"
:key="getItemValue(item)"
clickable
v-close-popup
@click="selectItem(item)"
class="option-item"
>
<q-item-section>
{{ getItemLabel(item) }}
</q-item-section>
</q-item>
<template v-if="options.multiSelect">
<q-item
v-for="item in normalizedItems"
:key="getItemValue(item)"
clickable
class="option-item"
@click="toggleSelectItem(item)"
>
<q-checkbox
size="xs"
:model-value="isItemSelected(item)"
@update:model-value="toggleSelectItem(item)"
/>
<div class="option-item-label">{{ getItemLabel(item) }}</div>
</q-item>
<q-separator />
<q-item
clickable
class="option-item"
@click="confirmMultiSelect"
v-close-popup
>
<q-item-section class="text-primary">
确定 (已选择 {{ selectedItems.length }} )
</q-item-section>
</q-item>
</template>
<template v-else>
<q-item
v-for="item in normalizedItems"
:key="getItemValue(item)"
clickable
v-close-popup
@click="selectItem(item)"
class="option-item"
>
<q-item-section>
{{ getItemLabel(item) }}
</q-item-section>
</q-item>
</template>
</q-list>
</q-btn-dropdown>
<q-btn
@ -147,8 +176,11 @@ import { newVarInputVal } from "js/composer/varInputValManager";
* @property {Object} modelValue - 输入框的值对象
* @property {String} label - 输入框标签
* @property {String} icon - 输入框图标
*
* @property {Object} [options] - 可选的配置对象
* @property {Array} [options.items] - 选项列表
* @property {Array} [options.items] - 选项列表默认单选选中后插入值且设置为字符模式
* @property {Boolean} [options.multiSelect] - 选项列表支持多选选中后插入选择的数组且设置为变量模式
*
* @property {Boolean} [options.dialog] - 是否显示文件选择对话框
* @property {Object} [options.dialog] - 文件选择对话框配置
* @property {String} [options.dialog.type] - 对话框类型open save
@ -213,6 +245,7 @@ export default defineComponent({
return {
selectedVariable: null,
variables: [],
selectedItems: [],
};
},
@ -290,8 +323,12 @@ export default defineComponent({
},
selectItem(option) {
const value = this.getItemValue(option);
this.$emit("update:modelValue", newVarInputVal("str", value));
if (this.options.multiSelect) {
this.toggleSelectItem(option);
} else {
const value = this.getItemValue(option);
this.$emit("update:modelValue", newVarInputVal("str", value));
}
},
handleFileOpen(dialog) {
let { type, options } = window.lodashM.cloneDeep(dialog);
@ -310,6 +347,40 @@ export default defineComponent({
this.$emit("update:modelValue", newVarInputVal("str", file));
}
},
//
isItemSelected(item) {
return this.selectedItems.includes(this.getItemValue(item));
},
//
toggleSelectItem(item) {
const value = this.getItemValue(item);
const index = this.selectedItems.indexOf(value);
if (index === -1) {
this.selectedItems.push(value);
} else {
this.selectedItems.splice(index, 1);
}
},
//
confirmMultiSelect() {
if (this.selectedItems.length === 0) return;
this.$emit(
"update:modelValue",
newVarInputVal(
"var",
JSON.stringify(this.selectedItems).replace(/,/g, ", ")
)
);
this.selectedItems = []; //
},
},
//
beforeUnmount() {
this.selectedItems = [];
},
});
</script>
@ -333,7 +404,6 @@ export default defineComponent({
.prepend-btn:hover {
opacity: 1;
transform: scale(1.05);
}
.clear-btn:hover {
@ -398,6 +468,11 @@ export default defineComponent({
background-color: var(--q-primary-opacity-10);
}
.option-item-label {
text-align: center;
flex: 1;
}
/* 暗色模式适配 */
.body--dark .option-item:hover {
background-color: rgba(255, 255, 255, 0.1);
@ -413,4 +488,15 @@ export default defineComponent({
.empty-variables-tip:hover {
opacity: 1;
}
/* 多选确认按钮样式 */
.option-item.text-primary {
justify-content: center;
font-weight: 500;
}
/* 多选项样式 */
.option-item .q-checkbox {
margin-right: 4px;
}
</style>

View File

@ -120,7 +120,7 @@
:model-value="argvs.options.env"
@update:model-value="(val) => updateArgvs('options.env', val)"
label="环境变量"
icon="environment"
icon="attach_money"
/>
</div>
</div>

View File

@ -158,5 +158,110 @@ export const uiCommands = {
},
],
},
{
value: "quickcomposer.ui.showOpenDialog",
label: "文件选择框",
desc: "显示一个文件选择框,返回选择的文件路径",
outputVariable: "filePaths",
saveOutput: true,
config: [
{
label: "标题",
type: "varInput",
defaultValue: newVarInputVal("str", "请选择文件"),
width: 6,
},
{
label: "默认路径",
type: "varInput",
defaultValue: newVarInputVal("str"),
width: 6,
placeholder: "默认打开的路径",
},
{
label: "按钮文本",
type: "varInput",
defaultValue: newVarInputVal("str", "选择"),
width: 3,
},
{
label: "提示信息",
type: "varInput",
defaultValue: newVarInputVal("str"),
width: 3,
placeholder: "对话框底部的提示信息",
defaultValue: newVarInputVal("str", "请选择"),
},
{
label: "扩展名",
type: "varInput",
width: 6,
options: {
items: ["*", "jpg", "png", "gif", "txt", "json", "exe"],
multiSelect: true,
},
defaultValue: newVarInputVal("var", '["*"]'),
},
],
subCommands: [
{
value: "quickcomposer.ui.showOpenDialog",
label: "打开文件对话框",
desc: "打开文件对话框",
icon: "folder_open",
config: [
{
label: "选择选项",
type: "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"],
},
],
},
{
value: "quickcomposer.ui.showSaveDialog",
label: "保存文件对话框",
desc: "保存文件对话框",
icon: "save",
config: [
{
label: "选择选项",
type: "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"],
},
],
},
],
},
],
};

View File

@ -81,7 +81,7 @@ const stringifyObject = (jsonObj) => {
return stringifyVarInputVal(jsonObj);
}
if (jsonObj instanceof Array) {
return `[${jsonObj.map((item) => stringifyObject(item)).join(",")}]`;
return `[${jsonObj.map((item) => stringifyArgv(item)).join(",")}]`;
}
try {
return processObject(jsonObj);