+
@@ -39,10 +39,10 @@ import { computed, inject, ref } from 'vue';
import { Aim, Close, Coin, Edit, View } from '@element-plus/icons-vue';
import { tMagicMessageBox, TMagicTooltip, TMagicTree } from '@tmagic/design';
-import { Id } from '@tmagic/schema';
+import { Dep, Id } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
-import type { Services } from '@editor/type';
+import { DepTargetType, Services } from '@editor/type';
defineOptions({
name: 'MEditorDataSourceList',
@@ -57,18 +57,52 @@ const { depService, editorService, dataSourceService } = inject('servi
const editable = computed(() => dataSourceService?.get('editable') ?? true);
+const dataSources = computed(() => dataSourceService?.get('dataSources') || []);
+
+const dsDep = computed(() => depService?.getTargets(DepTargetType.DATA_SOURCE) || {});
+const dsMethodDep = computed(() => depService?.getTargets(DepTargetType.DATA_SOURCE_METHOD) || {});
+const dsCondDep = computed(() => depService?.getTargets(DepTargetType.DATA_SOURCE_COND) || {});
+
+const getKeyTreeConfig = (dep: Dep[string], type?: string) =>
+ dep.keys.map((key) => ({ name: key, id: key, type: 'key', isMethod: type === 'method', isCond: type === 'cond' }));
+
+const getNodeTreeConfig = (id: string, dep: Dep[string], type?: string) => ({
+ name: dep.name,
+ type: 'node',
+ id,
+ children: getKeyTreeConfig(dep, type),
+});
+
const list = computed(() =>
- Object.values(depService?.targets['data-source'] || {}).map((target) => ({
- id: target.id,
- name: target.name,
- type: 'code',
- children: Object.entries(target.deps).map(([id, dep]) => ({
- name: dep.name,
- type: 'node',
- id,
- children: dep.keys.map((key) => ({ name: key, id: key, type: 'key' })),
- })),
- })),
+ dataSources.value.map((ds) => {
+ const dsDeps = dsDep.value[ds.id].deps;
+ const dsMethodDeps = dsMethodDep.value[ds.id].deps;
+ const dsCondDeps = dsCondDep.value[ds.id].deps;
+
+ const children: any[] = [];
+
+ const mergeChildren = (deps: Dep, type?: string) => {
+ Object.entries(deps).forEach(([id, dep]) => {
+ const nodeItem = children.find((item) => item.id === id);
+ if (nodeItem) {
+ nodeItem.children = nodeItem.children.concat(getKeyTreeConfig(dep, type));
+ } else {
+ children.push(getNodeTreeConfig(id, dep, type));
+ }
+ });
+ };
+
+ mergeChildren(dsDeps);
+ mergeChildren(dsMethodDeps, 'method');
+ mergeChildren(dsCondDeps, 'cond');
+
+ return {
+ id: ds.id,
+ name: ds.title,
+ type: 'ds',
+ children,
+ };
+ }),
);
const editHandler = (id: string) => {
diff --git a/packages/editor/src/services/dep.ts b/packages/editor/src/services/dep.ts
index 54d29cb3..e0a5f936 100644
--- a/packages/editor/src/services/dep.ts
+++ b/packages/editor/src/services/dep.ts
@@ -19,27 +19,24 @@ import { EventEmitter } from 'events';
import { reactive } from 'vue';
-import { Id, MNode } from '@tmagic/schema';
+import type { Dep, Id, MNode } from '@tmagic/schema';
+import { isObject } from '@tmagic/utils';
+
+import { DepTargetType } from '@editor/type';
type IsTarget = (key: string | number, value: any) => boolean;
interface TargetOptions {
isTarget: IsTarget;
id: string | number;
- type?: string;
- name: string;
-}
-
-interface Dep {
- [key: string | number]: {
- name: string;
- keys: (string | number)[];
- };
+ /** 类型,数据源、代码块或其他 */
+ type?: DepTargetType | string;
+ name?: string;
}
interface TargetList {
- [key: string]: {
- [key: string | number]: Target;
+ [type: DepTargetType | string]: {
+ [targetId: string | number]: Target;
};
}
@@ -60,11 +57,11 @@ export class Target extends EventEmitter {
/**
* 目标名称,用于显示在依赖列表中
*/
- public name: string;
+ public name?: string;
/**
* 不同的目标可以进行分类,例如代码块,数据源可以为两个不同的type
*/
- public type = 'default';
+ public type: DepTargetType | string = DepTargetType.DEFAULT;
/**
* 依赖详情
* 实例:{ 'node_id': { name: 'node_name', keys: [ created, mounted ] } }
@@ -156,14 +153,14 @@ export class Target extends EventEmitter {
}
export class Watcher extends EventEmitter {
- public targets = reactive({});
+ private targets = reactive({});
/**
* 获取指定类型中的所有target
* @param type 分类
* @returns Target[]
*/
- public getTargets(type = 'default') {
+ public getTargets(type: DepTargetType | string = DepTargetType.DEFAULT) {
return this.targets[type] || {};
}
@@ -230,7 +227,7 @@ export class Watcher extends EventEmitter {
* @param type 分类
* @returns void
*/
- public removeTargets(type = 'default') {
+ public removeTargets(type: DepTargetType | string = DepTargetType.DEFAULT) {
const targets = this.targets[type];
if (!targets) return;
@@ -307,9 +304,11 @@ export class Watcher extends EventEmitter {
this.emit('update-dep', node, fullKey);
} else if (!keyIsItems && Array.isArray(value)) {
value.forEach((item, index) => {
- collectTarget(item, `${fullKey}.${index}`);
+ if (isObject(item)) {
+ collectTarget(item, `${fullKey}.${index}`);
+ }
});
- } else if (Object.prototype.toString.call(value) === '[object Object]') {
+ } else if (isObject(value)) {
collectTarget(value, fullKey);
}
@@ -321,6 +320,7 @@ export class Watcher extends EventEmitter {
};
Object.entries(config).forEach(([key, value]) => {
+ if (typeof value === 'undefined' || value === '') return;
doCollect(key, value);
});
};
diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts
index 1e6dbb1c..b0ce041e 100644
--- a/packages/editor/src/type.ts
+++ b/packages/editor/src/type.ts
@@ -500,3 +500,23 @@ export interface DataSourceMethodSelectConfig {
disabled?: boolean | FilterFunction;
display?: boolean | FilterFunction;
}
+
+export interface DataSourceFieldSelectConfig {
+ type: 'data-source-field-select';
+ name: string;
+ labelWidth?: number | string;
+ disabled?: boolean | FilterFunction;
+ display?: boolean | FilterFunction;
+}
+
+export enum DepTargetType {
+ DEFAULT = 'default',
+ /** 代码块 */
+ CODE_BLOCK = 'code-block',
+ /** 数据源 */
+ DATA_SOURCE = 'data-source',
+ /** 数据源方法 */
+ DATA_SOURCE_METHOD = 'data-source-method',
+ /** 数据源条件 */
+ DATA_SOURCE_COND = 'data-source-cond',
+}
diff --git a/packages/editor/src/utils/dep.ts b/packages/editor/src/utils/dep.ts
index a3581d57..63733012 100644
--- a/packages/editor/src/utils/dep.ts
+++ b/packages/editor/src/utils/dep.ts
@@ -1,13 +1,14 @@
import { isEmpty } from 'lodash-es';
-import { CodeBlockContent, DataSourceSchema, HookType, Id } from '@tmagic/schema';
+import { CodeBlockContent, HookType, Id } from '@tmagic/schema';
+import dataSourceService from '@editor/services/dataSource';
import { Target } from '@editor/services/dep';
-import type { HookData } from '@editor/type';
+import { DepTargetType, HookData } from '@editor/type';
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent) =>
new Target({
- type: 'code-block',
+ type: DepTargetType.CODE_BLOCK,
id,
name: codeBlock.name,
isTarget: (key: string | number, value: any) => {
@@ -24,12 +25,37 @@ export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent) =>
},
});
-export const createDataSourceTarget = (id: Id, ds: DataSourceSchema) =>
+export const createDataSourceTarget = (id: Id) =>
new Target({
- type: 'data-source',
+ type: DepTargetType.DATA_SOURCE,
id,
- name: ds.title || `${id}`,
isTarget: (key: string | number, value: any) =>
// 关联数据源对象或者在模板在使用数据源
- (value.isBindDataSource && value.dataSourceId) || (typeof value === 'string' && value.includes(`${id}`)),
+ (value?.isBindDataSource && value.dataSourceId) || (typeof value === 'string' && value.includes(`${id}`)),
+ });
+
+export const createDataSourceCondTarget = (id: string) =>
+ new Target({
+ type: DepTargetType.DATA_SOURCE_COND,
+ id,
+ isTarget: (key: string | number, value: any) => {
+ if (!Array.isArray(value) || value[0] !== id) return false;
+
+ const ds = dataSourceService.getDataSourceById(id);
+
+ return Boolean(ds?.fields?.find((field) => field.name === value[1]));
+ },
+ });
+
+export const createDataSourceMethodTarget = (id: string) =>
+ new Target({
+ type: DepTargetType.DATA_SOURCE_METHOD,
+ id,
+ isTarget: (key: string | number, value: any) => {
+ if (!Array.isArray(value) || value[0] !== id) return false;
+
+ const ds = dataSourceService.getDataSourceById(id);
+
+ return Boolean(ds?.methods?.find((method) => method.name === value[1]));
+ },
});
diff --git a/packages/editor/src/utils/props.ts b/packages/editor/src/utils/props.ts
index b88d47d4..bbb56224 100644
--- a/packages/editor/src/utils/props.ts
+++ b/packages/editor/src/utils/props.ts
@@ -16,7 +16,300 @@
* limitations under the License.
*/
-import { FormConfig, FormState } from '@tmagic/form';
+import type { FormConfig, FormState, TabPaneConfig } from '@tmagic/form';
+
+import dataSourceService from '@editor/services/dataSource';
+
+const arrayOptions = [
+ { text: '包含', value: 'include' },
+ { text: '不包含', value: 'not_include' },
+];
+
+const eqOptions = [
+ { text: '等于', value: '=' },
+ { text: '不等于', value: '!=' },
+];
+
+const numberOptions = [
+ { text: '大于', value: '>' },
+ { text: '大于等于', value: '>=' },
+ { text: '小于', value: '<' },
+ { text: '小于等于', value: '<=' },
+ { text: '在范围内', value: 'between' },
+ { text: '不在范围内', value: 'not_between' },
+];
+
+export const styleTabConfig: TabPaneConfig = {
+ title: '样式',
+ labelWidth: '80px',
+ items: [
+ {
+ name: 'style',
+ items: [
+ {
+ type: 'fieldset',
+ legend: '位置',
+ items: [
+ {
+ name: 'position',
+ type: 'checkbox',
+ activeValue: 'fixed',
+ inactiveValue: 'absolute',
+ defaultValue: 'absolute',
+ text: '固定定位',
+ },
+ {
+ name: 'left',
+ text: 'left',
+ },
+ {
+ name: 'top',
+ text: 'top',
+ disabled: (vm: FormState, { model }: any) =>
+ model.position === 'fixed' && model._magic_position === 'fixedBottom',
+ },
+ {
+ name: 'right',
+ text: 'right',
+ },
+ {
+ name: 'bottom',
+ text: 'bottom',
+ disabled: (vm: FormState, { model }: any) =>
+ model.position === 'fixed' && model._magic_position === 'fixedTop',
+ },
+ ],
+ },
+ {
+ type: 'fieldset',
+ legend: '盒子',
+ items: [
+ {
+ name: 'width',
+ text: '宽度',
+ },
+ {
+ name: 'height',
+ text: '高度',
+ },
+ ],
+ },
+ {
+ type: 'fieldset',
+ legend: '边框',
+ items: [
+ {
+ name: 'borderWidth',
+ text: '宽度',
+ defaultValue: '0',
+ },
+ {
+ name: 'borderColor',
+ text: '颜色',
+ type: 'colorPicker',
+ },
+ {
+ name: 'borderStyle',
+ text: '样式',
+ type: 'select',
+ defaultValue: 'none',
+ options: [
+ { text: 'none', value: 'none' },
+ { text: 'hidden', value: 'hidden' },
+ { text: 'dotted', value: 'dotted' },
+ { text: 'dashed', value: 'dashed' },
+ { text: 'solid', value: 'solid' },
+ { text: 'double', value: 'double' },
+ { text: 'groove', value: 'groove' },
+ { text: 'ridge', value: 'ridge' },
+ { text: 'inset', value: 'inset' },
+ { text: 'outset', value: 'outset' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'fieldset',
+ legend: '背景',
+ items: [
+ {
+ 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%',
+ },
+ ],
+ },
+ {
+ type: 'fieldset',
+ legend: '字体',
+ items: [
+ {
+ name: 'color',
+ text: '颜色',
+ type: 'colorPicker',
+ },
+ {
+ name: 'fontSize',
+ text: '大小',
+ },
+ {
+ name: 'fontWeight',
+ text: '粗细',
+ },
+ ],
+ },
+ {
+ type: 'fieldset',
+ legend: '变形',
+ name: 'transform',
+ items: [
+ {
+ name: 'rotate',
+ text: '旋转角度',
+ },
+ {
+ name: 'scale',
+ text: '缩放',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+export const eventTabConfig: TabPaneConfig = {
+ title: '事件',
+ items: [
+ {
+ name: 'events',
+ type: 'event-select',
+ },
+ ],
+};
+
+export const advancedTabConfig: TabPaneConfig = {
+ title: '高级',
+ lazy: true,
+ items: [
+ {
+ name: 'created',
+ text: 'created',
+ labelWidth: '100px',
+ type: 'code-select',
+ },
+ {
+ name: 'mounted',
+ text: 'mounted',
+ labelWidth: '100px',
+ type: 'code-select',
+ },
+ ],
+};
+
+export const displayTabConfig: TabPaneConfig = {
+ title: '显示条件',
+ display: (vm: FormState, { model }: any) => model.type !== 'page',
+ items: [
+ {
+ type: 'groupList',
+ name: 'showCond',
+ items: [
+ {
+ type: 'table',
+ name: 'cond',
+ items: [
+ {
+ type: 'data-source-field-select',
+ name: 'field',
+ label: '字段',
+ },
+ {
+ type: 'select',
+ options: (mForm: FormState | undefined, { model }: any) => {
+ const [id, field] = model.field;
+
+ const ds = dataSourceService.getDataSourceById(id);
+
+ const type = ds?.fields.find((f) => f.name === 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',
+ display: (vm: FormState, { model }: any) =>
+ !['between', 'not_between', 'is', 'not'].includes(model.op),
+ },
+ {
+ name: 'value',
+ type: 'select',
+ options: [
+ { text: 'true', value: true },
+ { text: 'false', value: false },
+ ],
+ display: (vm: FormState, { model }: any) => ['is', 'not'].includes(model.op),
+ },
+ {
+ name: 'range',
+ type: 'number-range',
+ display: (vm: FormState, { model }: any) =>
+ ['between', 'not_between'].includes(model.op) && !['is', 'not'].includes(model.op),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
/**
* 统一为组件属性表单加上事件、高级、样式配置
@@ -50,194 +343,10 @@ export const fillConfig = (config: FormConfig = []) => [
...config,
],
},
- {
- title: '样式',
- labelWidth: '80px',
- items: [
- {
- name: 'style',
- items: [
- {
- type: 'fieldset',
- legend: '位置',
- items: [
- {
- name: 'position',
- type: 'checkbox',
- activeValue: 'fixed',
- inactiveValue: 'absolute',
- defaultValue: 'absolute',
- text: '固定定位',
- },
- {
- name: 'left',
- text: 'left',
- },
- {
- name: 'top',
- text: 'top',
- disabled: (vm: FormState, { model }: any) =>
- model.position === 'fixed' && model._magic_position === 'fixedBottom',
- },
- {
- name: 'right',
- text: 'right',
- },
- {
- name: 'bottom',
- text: 'bottom',
- disabled: (vm: FormState, { model }: any) =>
- model.position === 'fixed' && model._magic_position === 'fixedTop',
- },
- ],
- },
- {
- type: 'fieldset',
- legend: '盒子',
- items: [
- {
- name: 'width',
- text: '宽度',
- },
- {
- name: 'height',
- text: '高度',
- },
- ],
- },
- {
- type: 'fieldset',
- legend: '边框',
- items: [
- {
- name: 'borderWidth',
- text: '宽度',
- defaultValue: '0',
- },
- {
- name: 'borderColor',
- text: '颜色',
- type: 'colorPicker',
- },
- {
- name: 'borderStyle',
- text: '样式',
- type: 'select',
- defaultValue: 'none',
- options: [
- { text: 'none', value: 'none' },
- { text: 'hidden', value: 'hidden' },
- { text: 'dotted', value: 'dotted' },
- { text: 'dashed', value: 'dashed' },
- { text: 'solid', value: 'solid' },
- { text: 'double', value: 'double' },
- { text: 'groove', value: 'groove' },
- { text: 'ridge', value: 'ridge' },
- { text: 'inset', value: 'inset' },
- { text: 'outset', value: 'outset' },
- ],
- },
- ],
- },
- {
- type: 'fieldset',
- legend: '背景',
- items: [
- {
- 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%',
- },
- ],
- },
- {
- type: 'fieldset',
- legend: '字体',
- items: [
- {
- name: 'color',
- text: '颜色',
- type: 'colorPicker',
- },
- {
- name: 'fontSize',
- text: '大小',
- },
- {
- name: 'fontWeight',
- text: '粗细',
- },
- ],
- },
- {
- type: 'fieldset',
- legend: '变形',
- name: 'transform',
- items: [
- {
- name: 'rotate',
- text: '旋转角度',
- },
- {
- name: 'scale',
- text: '缩放',
- },
- ],
- },
- ],
- },
- ],
- },
- {
- title: '事件',
- items: [
- {
- name: 'events',
- type: 'event-select',
- labelWidth: 0,
- },
- ],
- },
- {
- title: '高级',
- lazy: true,
- items: [
- {
- name: 'created',
- text: 'created',
- labelWidth: '100px',
- type: 'code-select',
- },
- {
- name: 'mounted',
- text: 'mounted',
- labelWidth: '100px',
- type: 'code-select',
- },
- ],
- },
+ { ...styleTabConfig },
+ { ...eventTabConfig },
+ { ...advancedTabConfig },
+ { ...displayTabConfig },
],
},
];
diff --git a/packages/editor/tests/unit/services/dep.spec.ts b/packages/editor/tests/unit/services/dep.spec.ts
index 0e8bf52c..d3da54eb 100644
--- a/packages/editor/tests/unit/services/dep.spec.ts
+++ b/packages/editor/tests/unit/services/dep.spec.ts
@@ -122,10 +122,10 @@ describe('depService', () => {
const target1 = depService.getTarget('collect_1');
const target2 = depService.getTarget('collect_2');
- expect((target1?.deps || {}).node_1.name).toBe('node');
- expect((target2?.deps || {}).node_1.name).toBe('node');
- expect((target1?.deps || {}).node_1.keys).toHaveLength(1);
- expect((target2?.deps || {}).node_1.keys).toHaveLength(3);
+ expect(target1?.deps?.node_1.name).toBe('node');
+ expect(target2?.deps?.node_1.name).toBe('node');
+ expect(target1?.deps?.node_1.keys).toHaveLength(1);
+ expect(target2?.deps?.node_1.keys).toHaveLength(3);
depService.collect([
{
@@ -146,8 +146,8 @@ describe('depService', () => {
},
]);
- expect((target1?.deps || {}).node_1).toBeUndefined();
- expect((target2?.deps || {}).node_1.keys).toHaveLength(1);
+ expect(target1?.deps?.node_1).toBeUndefined();
+ expect(target2?.deps?.node_1.keys).toHaveLength(1);
depService.collect([
{
@@ -158,8 +158,8 @@ describe('depService', () => {
},
]);
- expect((target1?.deps || {}).node_1).toBeUndefined();
- expect((target2?.deps || {}).node_1.keys[0]).toBe('text1');
+ expect(target1?.deps?.node_1).toBeUndefined();
+ expect(target2?.deps?.node_1.keys[0]).toBe('text1');
depService.clear([
{
@@ -168,8 +168,8 @@ describe('depService', () => {
},
]);
- expect((target1?.deps || {}).node_1).toBeUndefined();
- expect((target2?.deps || {}).node_1).toBeUndefined();
+ expect(target1?.deps?.node_1).toBeUndefined();
+ expect(target2?.deps?.node_1).toBeUndefined();
});
test('collect deep', () => {
@@ -204,8 +204,8 @@ describe('depService', () => {
const target1 = depService.getTarget('collect_1');
- expect((target1?.deps || {}).node_1.name).toBe('node');
- expect((target1?.deps || {}).node_2.name).toBe('node2');
+ expect(target1?.deps?.node_1.name).toBe('node');
+ expect(target1?.deps?.node_2.name).toBe('node2');
depService.clear([
{
@@ -221,7 +221,7 @@ describe('depService', () => {
},
]);
- expect((target1?.deps || {}).node_1).toBeUndefined();
- expect((target1?.deps || {}).node_2).toBeUndefined();
+ expect(target1?.deps?.node_1).toBeUndefined();
+ expect(target1?.deps?.node_2).toBeUndefined();
});
});
diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts
index bf0c537b..928069b0 100644
--- a/packages/schema/src/index.ts
+++ b/packages/schema/src/index.ts
@@ -125,6 +125,7 @@ export interface MApp extends MComponent {
dataSources?: DataSourceSchema[];
dataSourceDeps?: DataSourceDeps;
+ dataSourceCondDeps?: DataSourceDeps;
}
export interface CodeBlockDSL {
@@ -191,14 +192,15 @@ export interface DataSourceSchema {
/** 字段列表 */
fields: DataSchema[];
/** 方法列表 */
- methods?: CodeBlockContent[];
+ methods: CodeBlockContent[];
/** 扩展字段 */
[key: string]: any;
}
export interface Dep {
[nodeId: Id]: {
+ /** 组件名称 */
name: string;
- keys: Id[];
+ keys: (string | number)[];
};
}