feat(dats-source,editor,form,schema,ui): 迭代器容器支持配置子项显示条件

This commit is contained in:
roymondchen 2024-06-28 17:44:04 +08:00
parent 0c5485b1d0
commit a164e2be62
18 changed files with 400 additions and 159 deletions

View File

@ -20,13 +20,13 @@ import EventEmitter from 'events';
import { cloneDeep } from 'lodash-es';
import type { AppCore, DataSourceSchema, Id, MNode } from '@tmagic/schema';
import type { AppCore, DataSourceSchema, DisplayCond, Id, MNode } from '@tmagic/schema';
import { compiledNode } from '@tmagic/utils';
import { SimpleObservedData } from './observed-data/SimpleObservedData';
import { DataSource, HttpDataSource } from './data-sources';
import type { ChangeEvent, DataSourceManagerData, DataSourceManagerOptions, ObservedDataClass } from './types';
import { compiledNodeField, compliedConditions, compliedIteratorItems } from './utils';
import { compiledNodeField, compliedConditions, compliedIteratorItemConditions, compliedIteratorItems } from './utils';
class DataSourceManager extends EventEmitter {
private static dataSourceClassMap = new Map<string, typeof DataSource>();
@ -214,6 +214,10 @@ class DataSourceManager extends EventEmitter {
return compliedConditions(node, this.data);
}
public compliedIteratorItemConds(itemData: any, displayConds: DisplayCond[] = []) {
return compliedIteratorItemConditions(displayConds, itemData);
}
public compliedIteratorItems(itemData: any, items: MNode[], dataSourceField: string[] = []) {
const [dsId, ...keys] = dataSourceField;
const ds = this.get(dsId);

View File

@ -1,7 +1,7 @@
import { cloneDeep, template } from 'lodash-es';
import { isDataSourceTemplate, isUseDataSourceField, Target, Watcher } from '@tmagic/dep';
import type { MApp, MNode, MPage, MPageFragment } from '@tmagic/schema';
import type { DisplayCond, DisplayCondItem, MApp, MNode, MPage, MPageFragment } from '@tmagic/schema';
import {
compiledCond,
compiledNode,
@ -15,29 +15,69 @@ import {
import type { AsyncDataSourceResolveResult, DataSourceManagerData } from './types';
/**
*
* @param cond
* @param data
* @returns boolean
*/
export const compiledCondition = (cond: DisplayCondItem[], data: DataSourceManagerData) => {
let result = true;
for (const { op, value, range, field } of cond) {
const [sourceId, ...fields] = field;
const dsData = data[sourceId];
if (!dsData || !fields.length) {
break;
}
const fieldValue = getValueByKeyPath(fields.join('.'), dsData);
if (!compiledCond(op, fieldValue, value, range)) {
result = false;
break;
}
}
return result;
};
/**
*
* @param node dsl节点
* @param data
* @returns boolean
*/
export const compliedConditions = (node: MNode, data: DataSourceManagerData) => {
export const compliedConditions = (node: { displayConds?: DisplayCond[] }, data: DataSourceManagerData) => {
if (!node.displayConds || !Array.isArray(node.displayConds) || !node.displayConds.length) return true;
for (const { cond } of node.displayConds) {
if (!cond) continue;
if (compiledCondition(cond, data)) {
return true;
}
}
return false;
};
/**
*
* @param displayConds
* @param data
* @returns boolean
*/
export const compliedIteratorItemConditions = (displayConds: DisplayCond[] = [], data: DataSourceManagerData) => {
if (!displayConds || !Array.isArray(displayConds) || !displayConds.length) return true;
for (const { cond } of displayConds) {
if (!cond) continue;
let result = true;
for (const { op, value, range, field } of cond) {
const [sourceId, ...fields] = field;
const dsData = data[sourceId];
if (!dsData || !fields.length) {
break;
}
const fieldValue = getValueByKeyPath(fields.join('.'), data[sourceId]);
const fieldValue = getValueByKeyPath(field.join('.'), data);
if (!compiledCond(op, fieldValue, value, range)) {
result = false;
@ -45,9 +85,7 @@ export const compliedConditions = (node: MNode, data: DataSourceManagerData) =>
}
}
if (result) {
return true;
}
return result;
}
return false;

View File

@ -0,0 +1,87 @@
<template>
<TMagicSelect
v-model="model[name]"
clearable
filterable
:size="size"
:disabled="disabled"
@change="fieldChangeHandler"
>
<component
v-for="option in options"
class="tmagic-design-option"
:key="option.value"
:is="optionComponent?.component || 'el-option'"
v-bind="
optionComponent?.props({
label: option.text,
value: option.value,
}) || {
label: option.text,
value: option.value,
}
"
>
</component>
</TMagicSelect>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue';
import { getConfig as getDesignConfig, TMagicSelect } from '@tmagic/design';
import type { FieldProps } from '@tmagic/form';
import type { CondOpSelectConfig, Services } from '@editor/type';
import { arrayOptions, eqOptions, numberOptions } from '@editor/utils';
defineOptions({
name: 'MFieldsCondOpSelect',
});
const emit = defineEmits(['change']);
const { dataSourceService } = inject<Services>('services') || {};
const props = defineProps<FieldProps<CondOpSelectConfig>>();
const optionComponent = getDesignConfig('components')?.option;
const options = computed(() => {
const [id, ...fieldNames] = [...(props.config.parentFields || []), ...props.model.field];
const ds = dataSourceService?.getDataSourceById(id);
let fields = ds?.fields || [];
let type = '';
(fieldNames || []).forEach((fieldName: string) => {
const field = fields.find((f) => f.name === fieldName);
fields = field?.fields || [];
type = field?.type || '';
});
if (type === 'array') {
return arrayOptions;
}
if (type === 'boolean') {
return [
{ text: '是', value: 'is' },
{ text: '不是', value: 'not' },
];
}
if (type === 'number') {
return [...eqOptions, ...numberOptions];
}
if (type === 'string') {
return [...arrayOptions, ...eqOptions];
}
return [...arrayOptions, ...eqOptions, ...numberOptions];
});
const fieldChangeHandler = (v: string[]) => {
emit('change', v);
};
</script>

View File

@ -137,7 +137,7 @@ const dsChangeHandler = (v: string) => {
emit('change', modelValue.value);
};
const fieldChangeHandler = (v: string[]) => {
const fieldChangeHandler = (v: string[] = []) => {
modelValue.value = [selectDataSourceId.value, ...v];
emit('change', modelValue.value);
};

View File

@ -129,10 +129,20 @@ const checkStrictly = computed(() => {
});
const onChangeHandler = (value: string[]) => {
if (!Array.isArray(value)) {
emit('change', value);
return;
}
const [dsId, ...keys] = value;
const dataSource = dataSources.value.find((ds) => ds.id === removeDataSourceFieldPrefix(dsId));
let fields = dataSource?.fields || [];
if (!dataSource) {
emit('change', value);
return;
}
let fields = dataSource.fields || [];
let field: DataSchema | undefined;
(keys || []).forEach((key) => {
field = fields.find((f) => f.name === key);
@ -145,7 +155,6 @@ const onChangeHandler = (value: string[]) => {
}
if (
!dsId ||
!keys.length ||
(field?.type &&
(field.type === 'any' || dataSourceFieldType.includes('any') || dataSourceFieldType.includes(field.type)))

View File

@ -172,6 +172,12 @@ const dataSourceFieldsConfig: FormConfig = [
{ text: 'null', value: 'null' },
{ text: 'any', value: 'any' },
],
onChange: (formState, v: string, { model }) => {
if (!['any', 'array', 'object'].includes(v)) {
model.fields = [];
}
return v;
},
},
{
name: 'name',

View File

@ -0,0 +1,145 @@
<template>
<MGroupList
style="width: 100%"
:config="config"
:name="name"
:disabled="disabled"
:model="model"
:last-values="lastValues"
:prop="prop"
:size="size"
></MGroupList>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue';
import {
type FieldProps,
type FilterFunction,
filterFunction,
type FormState,
type GroupListConfig,
MGroupList,
} from '@tmagic/form';
import type { Services } from '@editor/type';
import { getCascaderOptionsFromFields } from '@editor/utils';
defineOptions({
name: 'm-fields-display-conds',
});
const props = withDefaults(
defineProps<
FieldProps<{
titlePrefix?: string;
parentFields?: string[] | FilterFunction<string[]>;
}>
>(),
{
disabled: false,
},
);
const { dataSourceService } = inject<Services>('services') || {};
const mForm = inject<FormState | undefined>('mForm');
const parentFields = computed(() => filterFunction<string[]>(mForm, props.config.parentFields, props) || []);
const config = computed<GroupListConfig>(() => ({
type: 'groupList',
name: props.name,
titlePrefix: props.config.titlePrefix,
expandAll: true,
items: [
{
type: 'table',
name: 'cond',
operateColWidth: 50,
items: [
parentFields.value.length
? {
type: 'cascader',
options: () => {
const [dsId, ...keys] = parentFields.value;
const ds = dataSourceService?.getDataSourceById(dsId);
if (!ds) {
return [];
}
let fields = ds.fields || [];
keys.forEach((key) => {
const field = fields.find((f) => f.name === key);
fields = field?.fields || [];
});
return getCascaderOptionsFromFields(fields, ['string', 'number', 'boolean', 'any']);
},
name: 'field',
value: 'key',
label: '字段',
checkStrictly: false,
}
: {
type: 'data-source-field-select',
name: 'field',
value: 'key',
label: '字段',
checkStrictly: false,
dataSourceFieldType: ['string', 'number', 'boolean', 'any'],
},
{
type: 'cond-op-select',
parentFields: parentFields.value,
label: '条件',
width: 100,
name: 'op',
},
{
label: '值',
width: 100,
items: [
{
name: 'value',
type: (mForm, { model }) => {
const [id, ...fieldNames] = model.field;
const ds = dataSourceService?.getDataSourceById(id);
let fields = ds?.fields || [];
let type = '';
(fieldNames || []).forEach((fieldName: string) => {
const field = fields.find((f) => f.name === fieldName);
fields = field?.fields || [];
type = field?.type || '';
});
if (type === 'number') {
return 'number';
}
if (type === 'boolean') {
return 'select';
}
return 'text';
},
options: [
{ text: 'true', value: true },
{ text: 'false', value: false },
],
display: (vm, { model }) => !['between', 'not_between'].includes(model.op),
},
{
name: 'range',
type: 'number-range',
display: (vm, { model }) => ['between', 'not_between'].includes(model.op),
},
],
},
],
},
],
}));
</script>

View File

@ -21,6 +21,7 @@ import Code from './fields/Code.vue';
import CodeLink from './fields/CodeLink.vue';
import CodeSelect from './fields/CodeSelect.vue';
import CodeSelectCol from './fields/CodeSelectCol.vue';
import CondOpSelect from './fields/CondOpSelect.vue';
import DataSourceFields from './fields/DataSourceFields.vue';
import DataSourceFieldSelect from './fields/DataSourceFieldSelect/Index.vue';
import DataSourceInput from './fields/DataSourceInput.vue';
@ -28,6 +29,7 @@ import DataSourceMethods from './fields/DataSourceMethods.vue';
import DataSourceMethodSelect from './fields/DataSourceMethodSelect.vue';
import DataSourceMocks from './fields/DataSourceMocks.vue';
import DataSourceSelect from './fields/DataSourceSelect.vue';
import DisplayConds from './fields/DisplayConds.vue';
import EventSelect from './fields/EventSelect.vue';
import KeyValue from './fields/KeyValue.vue';
import PageFragmentSelect from './fields/PageFragmentSelect.vue';
@ -84,6 +86,8 @@ export { default as Resizer } from './components/Resizer.vue';
export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
export { default as FloatingBox } from './components/FloatingBox.vue';
export { default as PageFragmentSelect } from './fields/PageFragmentSelect.vue';
export { default as DisplayConds } from './fields/DisplayConds.vue';
export { default as CondOpSelect } from './fields/CondOpSelect.vue';
const defaultInstallOpt: InstallOptions = {
// eslint-disable-next-line no-eval
@ -114,5 +118,7 @@ export default {
app.component('m-fields-data-source-method-select', DataSourceMethodSelect);
app.component('m-fields-data-source-field-select', DataSourceFieldSelect);
app.component('m-fields-page-fragment-select', PageFragmentSelect);
app.component('m-fields-display-conds', DisplayConds);
app.component('m-fields-cond-op-select', CondOpSelect);
},
};

View File

@ -677,6 +677,11 @@ export interface DataSourceFieldSelectConfig extends FormItem {
notEditable?: boolean | FilterFunction;
}
export interface CondOpSelectConfig extends FormItem {
type: 'cond-op';
parentFields?: string[];
}
/** 可新增的数据源类型选项 */
export interface DatasourceTypeOption {
/** 数据源类型 */

View File

@ -215,7 +215,10 @@ export const getCascaderOptionsFromFields = (
dataSourceFieldType.push('any');
}
const children = getCascaderOptionsFromFields(field.fields, dataSourceFieldType);
let children: CascaderOption[] = [];
if (field.type && ['any', 'array', 'object'].includes(field.type)) {
children = getCascaderOptionsFromFields(field.fields, dataSourceFieldType);
}
const item = {
label: `${field.title || field.name}(${field.type})`,

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
@ -18,19 +19,17 @@
import type { FormConfig, FormState, TabPaneConfig } from '@tmagic/form';
import dataSourceService from '@editor/services/dataSource';
const arrayOptions = [
export const arrayOptions = [
{ text: '包含', value: 'include' },
{ text: '不包含', value: 'not_include' },
];
const eqOptions = [
export const eqOptions = [
{ text: '等于', value: '=' },
{ text: '不等于', value: '!=' },
];
const numberOptions = [
export const numberOptions = [
{ text: '大于', value: '>' },
{ text: '大于等于', value: '>=' },
{ text: '小于', value: '<' },
@ -359,106 +358,10 @@ export const displayTabConfig: TabPaneConfig = {
display: (vm: FormState, { model }: any) => model.type !== 'page',
items: [
{
type: 'groupList',
type: 'display-conds',
name: 'displayConds',
titlePrefix: '条件组',
expandAll: true,
items: [
{
type: 'table',
name: 'cond',
items: [
{
type: 'data-source-field-select',
name: 'field',
value: 'key',
label: '字段',
checkStrictly: false,
dataSourceFieldType: ['string', 'number', 'boolean', 'any'],
},
{
type: 'select',
options: (mForm, { model }) => {
const [id, ...fieldNames] = model.field;
const ds = dataSourceService.getDataSourceById(id);
let fields = ds?.fields || [];
let type = '';
(fieldNames || []).forEach((fieldName: string) => {
const field = fields.find((f) => f.name === fieldName);
fields = field?.fields || [];
type = field?.type || '';
});
if (type === 'array') {
return arrayOptions;
}
if (type === 'boolean') {
return [
{ text: '是', value: 'is' },
{ text: '不是', value: 'not' },
];
}
if (type === 'number') {
return [...eqOptions, ...numberOptions];
}
if (type === 'string') {
return [...arrayOptions, ...eqOptions];
}
return [...arrayOptions, ...eqOptions, ...numberOptions];
},
label: '条件',
name: 'op',
},
{
label: '值',
items: [
{
name: 'value',
type: (mForm, { model }) => {
const [id, ...fieldNames] = model.field;
const ds = dataSourceService.getDataSourceById(id);
let fields = ds?.fields || [];
let type = '';
(fieldNames || []).forEach((fieldName: string) => {
const field = fields.find((f) => f.name === fieldName);
fields = field?.fields || [];
type = field?.type || '';
});
if (type === 'number') {
return 'number';
}
if (type === 'boolean') {
return 'select';
}
return 'text';
},
options: [
{ text: 'true', value: true },
{ text: 'false', value: false },
],
display: (vm, { model }) => !['between', 'not_between'].includes(model.op),
},
{
name: 'range',
type: 'number-range',
display: (vm, { model }) => ['between', 'not_between'].includes(model.op),
},
],
},
],
},
],
defaultValue: [],
},
],
};

View File

@ -49,7 +49,7 @@ const options = ref<CascaderOption[]>([]);
const remoteData = ref<any>(null);
const checkStrictly = computed(() => filterFunction(mForm, props.config.checkStrictly, props));
const valueSeparator = computed(() => filterFunction(mForm, props.config.valueSeparator, props));
const valueSeparator = computed(() => filterFunction<string>(mForm, props.config.valueSeparator, props));
const value = computed({
get() {

View File

@ -11,7 +11,7 @@ import { computed, inject } from 'vue';
import { TMagicCheckbox, TMagicCheckboxGroup } from '@tmagic/design';
import type { CheckboxGroupConfig, FieldProps, FormState } from '../schema';
import type { CheckboxGroupConfig, CheckboxGroupOption, FieldProps, FormState } from '../schema';
import { filterFunction } from '../utils/form';
import { useAddField } from '../utils/useAddField';
@ -37,7 +37,8 @@ const changeHandler = (v: Array<string | number | boolean>) => {
const mForm = inject<FormState | undefined>('mForm');
const options = computed(() => {
if (Array.isArray(props.config.options)) return props.config.options;
if (typeof props.config.options === 'function') return filterFunction(mForm, props.config.options, props);
if (typeof props.config.options === 'function')
return filterFunction<CheckboxGroupOption[]>(mForm, props.config.options, props) || [];
return [];
});
</script>

View File

@ -170,6 +170,7 @@ export type FilterFunction<T = boolean> = (
formValue: Record<any, any>;
prop: string;
config: any;
index?: number;
},
) => T;
@ -433,18 +434,18 @@ export interface ColorPickConfig extends FormItem {
type: 'colorPicker';
}
export interface CheckboxGroupOption {
value: any;
text: string;
disabled?: boolean;
}
/**
*
*/
export interface CheckboxGroupConfig extends FormItem {
type: 'checkbox-group';
options:
| {
value: any;
text: string;
disabled?: boolean;
}[]
| Function;
options: CheckboxGroupOption[] | FilterFunction<CheckboxGroupOption[]>;
}
/**

View File

@ -24,6 +24,7 @@ import {
ChildConfig,
ContainerCommonConfig,
DaterangeConfig,
FilterFunction,
FormConfig,
FormState,
FormValue,
@ -181,20 +182,24 @@ const getDefaultValue = function (mForm: FormState | undefined, { defaultValue,
return '';
};
export const filterFunction = <T = any>(mForm: FormState | undefined, config: T, props: any) => {
if (typeof config !== 'function') {
return config;
export const filterFunction = <T = any>(
mForm: FormState | undefined,
config: T | FilterFunction<T> | undefined,
props: any,
) => {
if (typeof config === 'function') {
return (config as FilterFunction<T>)(mForm, {
values: mForm?.initValues || {},
model: props.model,
parent: mForm?.parentValues || {},
formValue: mForm?.values || props.model,
prop: props.prop,
config: props.config,
index: props.index,
});
}
return config(mForm, {
values: mForm?.initValues || {},
model: props.model,
parent: mForm?.parentValues || {},
formValue: mForm?.values || props.model,
prop: props.prop,
config: props.config,
index: props.index,
});
return config;
};
export const display = function (mForm: FormState | undefined, config: any, props: any) {

View File

@ -139,6 +139,7 @@ export interface MComponent {
style?: {
[key: string]: any;
};
displayConds?: DisplayCond[];
[key: string]: any;
}
@ -285,3 +286,14 @@ export type HookData = {
/** 参数 */
params?: object;
};
export interface DisplayCondItem {
field: string[];
op: string;
value?: any;
range?: [number, number];
}
export interface DisplayCond {
cond: DisplayCondItem[];
}

View File

@ -7,7 +7,7 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { type MContainer, NodeType } from '@tmagic/schema';
import { type DisplayCond, type MContainer, NodeType } from '@tmagic/schema';
import { useApp } from '@tmagic/vue-runtime-help';
import Container from '../../container';
@ -20,6 +20,7 @@ const props = withDefaults(
dsField: string[];
itemConfig: {
layout: string;
displayConds: DisplayCond[];
style: Record<string, string | number>;
};
};
@ -46,18 +47,26 @@ const configs = computed(() => {
iteratorData.push({});
}
return iteratorData.map((itemData) => ({
items:
app?.dataSourceManager?.compliedIteratorItems(itemData, props.config.items, props.config.dsField) ??
props.config.items,
id: '',
type: NodeType.CONTAINER,
style: {
...props.config.itemConfig.style,
position: 'relative',
left: 0,
top: 0,
},
}));
return iteratorData.map((itemData) => {
const condResult =
app?.platform !== 'editor'
? app?.dataSourceManager?.compliedIteratorItemConds(itemData, props.config.itemConfig.displayConds) ?? true
: true;
return {
items:
app?.dataSourceManager?.compliedIteratorItems(itemData, props.config.items, props.config.dsField) ??
props.config.items,
id: '',
type: NodeType.CONTAINER,
condResult,
style: {
...props.config.itemConfig.style,
position: 'relative',
left: 0,
top: 0,
},
};
});
});
</script>

View File

@ -43,6 +43,13 @@ export default [
title: '子项配置',
name: 'itemConfig',
items: [
{
type: 'display-conds',
name: 'displayConds',
titlePrefix: '条件组',
parentFields: (formState: any, { formValue }: any) => formValue.dsField,
defaultValue: [],
},
{
name: 'layout',
text: '容器布局',