diff --git a/packages/data-source/src/DataSourceManager.ts b/packages/data-source/src/DataSourceManager.ts index 199f769d..48c2363e 100644 --- a/packages/data-source/src/DataSourceManager.ts +++ b/packages/data-source/src/DataSourceManager.ts @@ -21,10 +21,16 @@ import EventEmitter from 'events'; import { cloneDeep, template } from 'lodash-es'; import type { AppCore, DataSourceSchema, Id, MNode } from '@tmagic/schema'; -import { compiledCond, compiledNode, DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, isObject } from '@tmagic/utils'; +import { + compiledNode, + DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, + DSL_NODE_KEY_COPY_PREFIX, + getValueByKeyPath, +} from '@tmagic/utils'; import { DataSource, HttpDataSource } from './data-sources'; import type { ChangeEvent, DataSourceManagerData, DataSourceManagerOptions } from './types'; +import { compliedConditions, createIteratorContentData } from './utils'; class DataSourceManager extends EventEmitter { private static dataSourceClassMap = new Map(); @@ -189,15 +195,7 @@ class DataSourceManager extends EventEmitter { if (!data) return value; - return fields.reduce((accumulator, currentValue: any) => { - if (Array.isArray(accumulator)) return accumulator; - - if (isObject(accumulator)) { - return accumulator[currentValue]; - } - - return ''; - }, data); + return getValueByKeyPath(fields.join('.'), data); } } @@ -210,34 +208,39 @@ class DataSourceManager extends EventEmitter { } public compliedConds(node: MNode) { - if (!node.displayConds || !Array.isArray(node.displayConds) || !node.displayConds.length) return true; + return compliedConditions(node, this.data); + } - for (const { cond } of node.displayConds) { - if (!cond) continue; + public compliedIteratorItems(itemData: any, items: MNode[], dataSourceField: string[] = []) { + return items.map((item) => { + const keys: string[] = []; + const [dsId, ...fields] = dataSourceField; - let result = true; - for (const { op, value, range, field } of cond) { - const [sourceId, fieldKey] = field; - - const dsData = this.data[sourceId]; - - if (!dsData || !fieldKey) { - break; + Object.entries(item).forEach(([key, value]) => { + if ( + typeof value === 'string' && + !key.startsWith(DSL_NODE_KEY_COPY_PREFIX) && + value.includes(`${dsId}`) && + /\$\{([\s\S]+?)\}/.test(value) + ) { + keys.push(key); } + }); - const fieldValue = dsData[fieldKey]; - if (!compiledCond(op, fieldValue, value, range)) { - result = false; - break; - } - } - - if (result) { - return true; - } - } - - return false; + return compiledNode( + (value: string) => template(value)(createIteratorContentData(itemData, dsId, fields)), + cloneDeep(item), + { + [dsId]: { + [item.id]: { + name: '', + keys, + }, + }, + }, + dsId, + ); + }); } public destroy() { diff --git a/packages/data-source/src/createDataSourceManager.ts b/packages/data-source/src/createDataSourceManager.ts index a36ab521..c4d763de 100644 --- a/packages/data-source/src/createDataSourceManager.ts +++ b/packages/data-source/src/createDataSourceManager.ts @@ -17,11 +17,12 @@ */ import { cloneDeep, union } from 'lodash-es'; -import type { AppCore, MApp, MNode, MPage, MPageFragment } from '@tmagic/schema'; -import { getDepNodeIds, getNodes, isPage, isPageFragment, replaceChildNode } from '@tmagic/utils'; +import type { AppCore } from '@tmagic/schema'; +import { getDepNodeIds, getNodes } from '@tmagic/utils'; import DataSourceManager from './DataSourceManager'; import type { ChangeEvent, DataSourceManagerData } from './types'; +import { updateNode } from './utils'; /** * 创建数据源管理器 @@ -73,12 +74,3 @@ export const createDataSourceManager = (app: AppCore, useMock?: boolean, initial return dataSourceManager; }; - -const updateNode = (node: MNode, dsl: MApp) => { - if (isPage(node) || isPageFragment(node)) { - const index = dsl.items?.findIndex((child: MNode) => child.id === node.id); - dsl.items.splice(index, 1, node as MPage | MPageFragment); - } else { - replaceChildNode(node, dsl!.items); - } -}; diff --git a/packages/data-source/src/index.ts b/packages/data-source/src/index.ts index 60537a34..64852301 100644 --- a/packages/data-source/src/index.ts +++ b/packages/data-source/src/index.ts @@ -19,4 +19,5 @@ export { default as DataSourceManager } from './DataSourceManager'; export * from './data-sources'; export * from './createDataSourceManager'; +export * from './utils'; export * from './types'; diff --git a/packages/data-source/src/utils.ts b/packages/data-source/src/utils.ts new file mode 100644 index 00000000..f29afce0 --- /dev/null +++ b/packages/data-source/src/utils.ts @@ -0,0 +1,59 @@ +import type { MApp, MNode, MPage, MPageFragment } from '@tmagic/schema'; +import { compiledCond, getValueByKeyPath, isPage, isPageFragment, replaceChildNode } from '@tmagic/utils'; + +import type { DataSourceManagerData } from './types'; + +export const compliedConditions = (node: MNode, data: DataSourceManagerData) => { + if (!node.displayConds || !Array.isArray(node.displayConds) || !node.displayConds.length) return true; + + for (const { cond } of node.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]); + + if (!compiledCond(op, fieldValue, value, range)) { + result = false; + break; + } + } + + if (result) { + return true; + } + } + + return false; +}; + +export const updateNode = (node: MNode, dsl: MApp) => { + if (isPage(node) || isPageFragment(node)) { + const index = dsl.items?.findIndex((child: MNode) => child.id === node.id); + dsl.items.splice(index, 1, node as MPage | MPageFragment); + } else { + replaceChildNode(node, dsl!.items); + } +}; + +export const createIteratorContentData = (itemData: any, dsId: string, fields: string[] = []) => { + const data = { + [dsId]: {}, + }; + + fields.reduce((obj: any, field, index) => { + obj[field] = index === fields.length - 1 ? itemData : {}; + + return obj[field]; + }, data[dsId]); + + return data; +}; diff --git a/packages/dep/src/utils.ts b/packages/dep/src/utils.ts index 37f22359..c7c7e83a 100644 --- a/packages/dep/src/utils.ts +++ b/packages/dep/src/utils.ts @@ -1,5 +1,6 @@ import { type CodeBlockContent, + type DataSchema, type DataSourceSchema, type DepData, type HookData, @@ -46,14 +47,55 @@ export const createDataSourceTarget = (ds: DataSourceSchema, initialDeps: DepDat isTarget: (key: string | number, value: any) => { // 关联数据源对象,如:{ isBindDataSource: true, dataSourceId: 'xxx'} // 使用data-source-select value: 'value' 可以配置出来 - // 或者在模板在使用数据源,如:`xxx${id.field}xxx` - if ( - (value?.isBindDataSource && value.dataSourceId && value.dataSourceId === ds.id) || - (typeof value === 'string' && value.includes(`${ds.id}`) && /\$\{([\s\S]+?)\}/.test(value)) - ) { + + if (value?.isBindDataSource && value.dataSourceId && value.dataSourceId === ds.id) { return true; } + // 或者在模板在使用数据源,如:`xxx${dsId.field}xxx${dsId.field}` + if (typeof value === 'string' && value.includes(`${ds.id}`) && /\$\{([\s\S]+?)\}/.test(value)) { + // 模板中可能会存在多个表达式,将表达式从模板中提取出来 + const templates = value.match(/\$\{([\s\S]+?)\}/g) || []; + + for (const tpl of templates) { + const keys = tpl + // 将${dsId.xxxx} 转成 dsId.xxxx + .substring(2, tpl.length - 1) + // 将 array[0] 转成 array.0 + .replaceAll(/\[(\d+)\]/g, '.$1') + .split('.'); + const dsId = keys.shift(); + + if (!dsId || dsId !== ds.id) { + continue; + } + + // ${dsId.array} ${dsId.array[0]} ${dsId.array[0].a} 这种是依赖 + // ${dsId.array.a} 这种不是依赖,这种需要再迭代器容器中的组件才能使用,依赖由迭代器处理 + let includeArray = false; + keys.reduce((accumulator: DataSchema[], currentValue: string, currentIndex: number) => { + const field = accumulator.find(({ name }) => name === currentValue); + if ( + field && + field.type === 'array' && + typeof keys[currentIndex + 1] !== 'number' && + currentIndex < keys.length - 1 + ) { + includeArray = true; + } + return field?.fields || []; + }, ds.fields); + + if (includeArray) { + continue; + } + + return true; + } + + return false; + } + // 指定数据源的字符串模板,如:{ isBindDataSourceField: true, dataSourceId: 'id', template: `xxx${field}xxx`} if ( value?.isBindDataSourceField && diff --git a/packages/editor/src/fields/DataSourceFieldSelect.vue b/packages/editor/src/fields/DataSourceFieldSelect.vue index 8d6de8b8..d3213c28 100644 --- a/packages/editor/src/fields/DataSourceFieldSelect.vue +++ b/packages/editor/src/fields/DataSourceFieldSelect.vue @@ -1,23 +1,32 @@ diff --git a/packages/ui-vue2/src/iterator-container/src/formConfig.ts b/packages/ui-vue2/src/iterator-container/src/formConfig.ts new file mode 100644 index 00000000..afbb3c1e --- /dev/null +++ b/packages/ui-vue2/src/iterator-container/src/formConfig.ts @@ -0,0 +1,110 @@ +/* + * Tencent is pleased to support the open source community by making TMagicEditor available. + * + * Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils'; + +export default [ + { + name: 'iteratorData', + text: '数据源数据', + value: 'value', + fieldType: ['array'], + checkStrictly: false, + type: 'data-source-field-select', + onChange: (vm: any, v: string[] = [], { model }: any) => { + const [dsId, ...keys] = v; + model.dsField = [dsId.replace(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, ''), ...keys]; + return v; + }, + }, + { + name: 'dsField', + type: 'hidden', + }, + { + type: 'panel', + title: '子项配置', + name: 'itemConfig', + items: [ + { + name: 'layout', + text: '容器布局', + type: 'select', + defaultValue: 'absolute', + options: [ + { value: 'absolute', text: '绝对定位' }, + { value: 'relative', text: '流式布局', disabled: true }, + ], + }, + { + type: 'fieldset', + legend: '样式', + name: 'style', + items: [ + { + name: 'width', + text: '宽度', + }, + { + name: 'height', + text: '高度', + }, + { + text: 'overflow', + name: 'overflow', + type: 'select', + options: [ + { text: 'visible', value: 'visible' }, + { text: 'hidden', value: 'hidden' }, + { text: 'clip', value: 'clip' }, + { text: 'scroll', value: 'scroll' }, + { text: 'auto', value: 'auto' }, + { text: 'overlay', value: 'overlay' }, + ], + }, + { + name: 'backgroundImage', + text: '背景图', + }, + { + name: 'backgroundColor', + text: '背景颜色', + type: 'colorPicker', + }, + { + name: 'backgroundRepeat', + text: '背景图重复', + type: 'select', + defaultValue: 'no-repeat', + options: [ + { text: 'repeat', value: 'repeat' }, + { text: 'repeat-x', value: 'repeat-x' }, + { text: 'repeat-y', value: 'repeat-y' }, + { text: 'no-repeat', value: 'no-repeat' }, + { text: 'inherit', value: 'inherit' }, + ], + }, + { + name: 'backgroundSize', + text: '背景图大小', + defaultValue: '100% 100%', + }, + ], + }, + ], + }, +]; diff --git a/packages/ui-vue2/src/iterator-container/src/initValue.ts b/packages/ui-vue2/src/iterator-container/src/initValue.ts new file mode 100644 index 00000000..2223ec4c --- /dev/null +++ b/packages/ui-vue2/src/iterator-container/src/initValue.ts @@ -0,0 +1,31 @@ +/* + * Tencent is pleased to support the open source community by making TMagicEditor available. + * + * Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default { + style: { + width: '375', + height: '100', + }, + itemConfig: { + style: { + width: '100%', + height: '100%', + }, + }, + items: [], +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index d83dcc39..28c846bc 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -19,11 +19,12 @@ import Button from './button'; import Container from './container'; import Img from './img'; +import IteratorContainer from './iterator-container'; import Overlay from './overlay'; import Page from './page'; import PageFragment from './page-fragment'; import PageFragmentContainer from './page-fragment-container'; -import Qrcode from './qrcode'; +import QRcode from './qrcode'; import Text from './text'; const ui: Record = { @@ -32,10 +33,11 @@ const ui: Record = { button: Button, text: Text, img: Img, - qrcode: Qrcode, + qrcode: QRcode, overlay: Overlay, 'page-fragment-container': PageFragmentContainer, 'page-fragment': PageFragment, + 'iterator-container': IteratorContainer, }; export default ui; diff --git a/packages/ui/src/iterator-container/index.ts b/packages/ui/src/iterator-container/index.ts new file mode 100644 index 00000000..f05bdae6 --- /dev/null +++ b/packages/ui/src/iterator-container/index.ts @@ -0,0 +1,24 @@ +/* + * Tencent is pleased to support the open source community by making TMagicEditor available. + * + * Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import IteratorContainer from './src/IteratorContainer.vue'; + +export { default as config } from './src/formConfig'; +export { default as value } from './src/initValue'; + +export default IteratorContainer; diff --git a/packages/ui/src/iterator-container/src/IteratorContainer.vue b/packages/ui/src/iterator-container/src/IteratorContainer.vue new file mode 100644 index 00000000..eb3491d0 --- /dev/null +++ b/packages/ui/src/iterator-container/src/IteratorContainer.vue @@ -0,0 +1,63 @@ + + + diff --git a/packages/ui/src/iterator-container/src/formConfig.ts b/packages/ui/src/iterator-container/src/formConfig.ts new file mode 100644 index 00000000..afbb3c1e --- /dev/null +++ b/packages/ui/src/iterator-container/src/formConfig.ts @@ -0,0 +1,110 @@ +/* + * Tencent is pleased to support the open source community by making TMagicEditor available. + * + * Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils'; + +export default [ + { + name: 'iteratorData', + text: '数据源数据', + value: 'value', + fieldType: ['array'], + checkStrictly: false, + type: 'data-source-field-select', + onChange: (vm: any, v: string[] = [], { model }: any) => { + const [dsId, ...keys] = v; + model.dsField = [dsId.replace(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, ''), ...keys]; + return v; + }, + }, + { + name: 'dsField', + type: 'hidden', + }, + { + type: 'panel', + title: '子项配置', + name: 'itemConfig', + items: [ + { + name: 'layout', + text: '容器布局', + type: 'select', + defaultValue: 'absolute', + options: [ + { value: 'absolute', text: '绝对定位' }, + { value: 'relative', text: '流式布局', disabled: true }, + ], + }, + { + type: 'fieldset', + legend: '样式', + name: 'style', + items: [ + { + name: 'width', + text: '宽度', + }, + { + name: 'height', + text: '高度', + }, + { + text: 'overflow', + name: 'overflow', + type: 'select', + options: [ + { text: 'visible', value: 'visible' }, + { text: 'hidden', value: 'hidden' }, + { text: 'clip', value: 'clip' }, + { text: 'scroll', value: 'scroll' }, + { text: 'auto', value: 'auto' }, + { text: 'overlay', value: 'overlay' }, + ], + }, + { + name: 'backgroundImage', + text: '背景图', + }, + { + name: 'backgroundColor', + text: '背景颜色', + type: 'colorPicker', + }, + { + name: 'backgroundRepeat', + text: '背景图重复', + type: 'select', + defaultValue: 'no-repeat', + options: [ + { text: 'repeat', value: 'repeat' }, + { text: 'repeat-x', value: 'repeat-x' }, + { text: 'repeat-y', value: 'repeat-y' }, + { text: 'no-repeat', value: 'no-repeat' }, + { text: 'inherit', value: 'inherit' }, + ], + }, + { + name: 'backgroundSize', + text: '背景图大小', + defaultValue: '100% 100%', + }, + ], + }, + ], + }, +]; diff --git a/packages/ui/src/iterator-container/src/initValue.ts b/packages/ui/src/iterator-container/src/initValue.ts new file mode 100644 index 00000000..2223ec4c --- /dev/null +++ b/packages/ui/src/iterator-container/src/initValue.ts @@ -0,0 +1,31 @@ +/* + * Tencent is pleased to support the open source community by making TMagicEditor available. + * + * Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default { + style: { + width: '375', + height: '100', + }, + itemConfig: { + style: { + width: '100%', + height: '100%', + }, + }, + items: [], +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index cbbf6125..f4c326b8 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -18,7 +18,7 @@ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; -import { cloneDeep, get as objectGet, set as objectSet } from 'lodash-es'; +import { cloneDeep, set as objectSet } from 'lodash-es'; import type { DataSchema, DataSourceDeps, Id, MComponent, MNode } from '@tmagic/schema'; import { NodeType } from '@tmagic/schema'; @@ -165,7 +165,18 @@ export const guid = (digit = 8): string => return v.toString(16); }); -export const getValueByKeyPath: any = (keys: string, data: Record = {}) => objectGet(data, keys); +export const getValueByKeyPath: any = (keys = '', data: Record = {}) => + // 将 array[0] 转成 array.0 + keys + .replaceAll(/\[(\d+)\]/g, '.$1') + .split('.') + .reduce((accumulator, currentValue: any) => { + if (isObject(accumulator) || Array.isArray(accumulator)) { + return accumulator[currentValue]; + } + + return void 0; + }, data); export const setValueByKeyPath: any = (keys: string, value: any, data: Record = {}) => objectSet(data, keys, value); @@ -238,6 +249,8 @@ export const replaceChildNode = (newNode: MNode, data?: MNode[], parentId?: Id) parent.items.splice(index, 1, newNode); }; +export const DSL_NODE_KEY_COPY_PREFIX = '__magic__'; + export const compiledNode = ( compile: (value: any) => any, node: MNode, @@ -252,10 +265,8 @@ export const compiledNode = ( keys = dep?.[node.id].keys || []; } - const keyPrefix = '__magic__'; - keys.forEach((key) => { - const cacheKey = `${keyPrefix}${key}`; + const cacheKey = `${DSL_NODE_KEY_COPY_PREFIX}${key}`; const value = getValueByKeyPath(key, node); let templateValue = getValueByKeyPath(cacheKey, node); @@ -276,10 +287,6 @@ export const compiledNode = ( setValueByKeyPath(key, newValue, node); }); - if (Array.isArray(node.items)) { - node.items.forEach((item) => compiledNode(compile, item, dataSourceDeps)); - } - return node; }; diff --git a/playground/src/components/NavMenu.vue b/playground/src/components/NavMenu.vue index 4102b5f7..28a1873a 100644 --- a/playground/src/components/NavMenu.vue +++ b/playground/src/components/NavMenu.vue @@ -31,7 +31,7 @@ export default defineComponent({ }); -