mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
feat: 支持数据源事件 (#605)
* feat: 添加observedData * feat: 修改错误 * fix: 修复单测报错问题 * feat: 完善数据源事件 * fix: 修复数据源事件调用组件方法时报错的异常 * fix: 修复多个相同类型的数据源数据变化的事件混淆的问题 * chore: 删除无用代码 * feat: 默认使用SimpleObservedData * feat: 删除无用代码 --------- Co-authored-by: marchyang <marchyang@tencent.com>
This commit is contained in:
parent
831204663a
commit
88c04c6dac
@ -20,7 +20,7 @@ import { EventEmitter } from 'events';
|
|||||||
|
|
||||||
import { has, isEmpty } from 'lodash-es';
|
import { has, isEmpty } from 'lodash-es';
|
||||||
|
|
||||||
import { createDataSourceManager, DataSourceManager } from '@tmagic/data-source';
|
import { createDataSourceManager, DataSourceManager, ObservedDataClass } from '@tmagic/data-source';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
type AppCore,
|
type AppCore,
|
||||||
@ -35,6 +35,7 @@ import {
|
|||||||
type MApp,
|
type MApp,
|
||||||
type RequestFunction,
|
type RequestFunction,
|
||||||
} from '@tmagic/schema';
|
} from '@tmagic/schema';
|
||||||
|
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX } from '@tmagic/utils';
|
||||||
|
|
||||||
import Env from './Env';
|
import Env from './Env';
|
||||||
import { bindCommonEventListener, isCommonMethod, triggerCommonMethod } from './events';
|
import { bindCommonEventListener, isCommonMethod, triggerCommonMethod } from './events';
|
||||||
@ -52,6 +53,7 @@ interface AppOptionsConfig {
|
|||||||
useMock?: boolean;
|
useMock?: boolean;
|
||||||
transformStyle?: (style: Record<string, any>) => Record<string, any>;
|
transformStyle?: (style: Record<string, any>) => Record<string, any>;
|
||||||
request?: RequestFunction;
|
request?: RequestFunction;
|
||||||
|
DataSourceObservedData?: ObservedDataClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventCache {
|
interface EventCache {
|
||||||
@ -79,6 +81,7 @@ class App extends EventEmitter implements AppCore {
|
|||||||
public eventQueueMap: Record<string, EventCache[]> = {};
|
public eventQueueMap: Record<string, EventCache[]> = {};
|
||||||
|
|
||||||
private eventList = new Map<(fromCpt: Node, ...args: any[]) => void, string>();
|
private eventList = new Map<(fromCpt: Node, ...args: any[]) => void, string>();
|
||||||
|
private dataSourceEventList = new Map<string, Map<string, (...args: any[]) => void>>();
|
||||||
|
|
||||||
constructor(options: AppOptionsConfig) {
|
constructor(options: AppOptionsConfig) {
|
||||||
super();
|
super();
|
||||||
@ -272,6 +275,7 @@ class App extends EventEmitter implements AppCore {
|
|||||||
this.on(eventName, eventHandler);
|
this.on(eventName, eventHandler);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.bindDataSourceEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
public emit(name: string | symbol, ...args: any[]): boolean {
|
public emit(name: string | symbol, ...args: any[]): boolean {
|
||||||
@ -356,6 +360,43 @@ class App extends EventEmitter implements AppCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bindDataSourceEvents() {
|
||||||
|
if (this.platform === 'editor') return;
|
||||||
|
|
||||||
|
// 先清掉之前注册的事件,重新注册
|
||||||
|
Array.from(this.dataSourceEventList.keys()).forEach((dataSourceId) => {
|
||||||
|
const dataSourceEvent = this.dataSourceEventList.get(dataSourceId)!;
|
||||||
|
Array.from(dataSourceEvent.keys()).forEach((eventName) => {
|
||||||
|
const [prefix, ...path] = eventName.split('.');
|
||||||
|
if (prefix === DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX) {
|
||||||
|
this.dataSourceManager?.offDataChange(dataSourceId, path.join('.'), dataSourceEvent.get(eventName)!);
|
||||||
|
} else {
|
||||||
|
this.dataSourceManager?.get(dataSourceId)?.off(prefix, dataSourceEvent.get(eventName)!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
(this.dsl?.dataSources || []).forEach((dataSource) => {
|
||||||
|
const dataSourceEvent = this.dataSourceEventList.get(dataSource.id) ?? new Map<string, (args: any) => void>();
|
||||||
|
(dataSource.events || []).forEach((event) => {
|
||||||
|
const [prefix, ...path] = event.name?.split('.') || [];
|
||||||
|
if (!prefix) return;
|
||||||
|
const handler = (...args: any[]) => {
|
||||||
|
this.eventHandler(event, this.dataSourceManager?.get(dataSource.id), args);
|
||||||
|
};
|
||||||
|
dataSourceEvent.set(event.name, handler);
|
||||||
|
if (prefix === DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX) {
|
||||||
|
// 数据源数据变化
|
||||||
|
this.dataSourceManager?.onDataChange(dataSource.id, path.join('.'), handler);
|
||||||
|
} else {
|
||||||
|
// 数据源自定义事件
|
||||||
|
this.dataSourceManager?.get(dataSource.id)?.on(prefix, handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.dataSourceEventList.set(dataSource.id, dataSourceEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件联动处理函数
|
* 事件联动处理函数
|
||||||
* @param eventsConfigIndex 事件配置索引,可以通过此索引从node.event中获取最新事件配置
|
* @param eventsConfigIndex 事件配置索引,可以通过此索引从node.event中获取最新事件配置
|
||||||
|
@ -35,8 +35,9 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tmagic/dep": "workspace:*",
|
"@tmagic/dep": "workspace:*",
|
||||||
"@tmagic/utils": "workspace:*",
|
|
||||||
"@tmagic/schema": "workspace:*",
|
"@tmagic/schema": "workspace:*",
|
||||||
|
"@tmagic/utils": "workspace:*",
|
||||||
|
"deep-state-observer": "^5.5.13",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"lodash-es": "^4.17.21"
|
"lodash-es": "^4.17.21"
|
||||||
},
|
},
|
||||||
|
@ -23,12 +23,15 @@ import { cloneDeep } from 'lodash-es';
|
|||||||
import type { AppCore, DataSourceSchema, Id, MNode } from '@tmagic/schema';
|
import type { AppCore, DataSourceSchema, Id, MNode } from '@tmagic/schema';
|
||||||
import { compiledNode } from '@tmagic/utils';
|
import { compiledNode } from '@tmagic/utils';
|
||||||
|
|
||||||
|
import { SimpleObservedData } from './observed-data/SimpleObservedData';
|
||||||
import { DataSource, HttpDataSource } from './data-sources';
|
import { DataSource, HttpDataSource } from './data-sources';
|
||||||
import type { ChangeEvent, DataSourceManagerData, DataSourceManagerOptions } from './types';
|
import type { ChangeEvent, DataSourceManagerData, DataSourceManagerOptions, ObservedDataClass } from './types';
|
||||||
import { compiledNodeField, compliedConditions, compliedIteratorItems } from './utils';
|
import { compiledNodeField, compliedConditions, compliedIteratorItems } from './utils';
|
||||||
|
|
||||||
class DataSourceManager extends EventEmitter {
|
class DataSourceManager extends EventEmitter {
|
||||||
private static dataSourceClassMap = new Map<string, typeof DataSource>();
|
private static dataSourceClassMap = new Map<string, typeof DataSource>();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
private static ObservedDataClass: ObservedDataClass = SimpleObservedData;
|
||||||
|
|
||||||
public static register<T extends typeof DataSource = typeof DataSource>(type: string, dataSource: T) {
|
public static register<T extends typeof DataSource = typeof DataSource>(type: string, dataSource: T) {
|
||||||
DataSourceManager.dataSourceClassMap.set(type, dataSource);
|
DataSourceManager.dataSourceClassMap.set(type, dataSource);
|
||||||
@ -45,6 +48,10 @@ class DataSourceManager extends EventEmitter {
|
|||||||
return DataSourceManager.dataSourceClassMap.get(type);
|
return DataSourceManager.dataSourceClassMap.get(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static registerObservedData(ObservedDataClass: ObservedDataClass) {
|
||||||
|
DataSourceManager.ObservedDataClass = ObservedDataClass;
|
||||||
|
}
|
||||||
|
|
||||||
public app: AppCore;
|
public app: AppCore;
|
||||||
|
|
||||||
public dataSourceMap = new Map<string, DataSource>();
|
public dataSourceMap = new Map<string, DataSource>();
|
||||||
@ -133,6 +140,7 @@ class DataSourceManager extends EventEmitter {
|
|||||||
request: this.app.request,
|
request: this.app.request,
|
||||||
useMock: this.useMock,
|
useMock: this.useMock,
|
||||||
initialData: this.data[config.id],
|
initialData: this.data[config.id],
|
||||||
|
ObservedDataClass: DataSourceManager.ObservedDataClass,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataSourceMap.set(config.id, ds);
|
this.dataSourceMap.set(config.id, ds);
|
||||||
@ -210,6 +218,14 @@ class DataSourceManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
this.dataSourceMap.clear();
|
this.dataSourceMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDataChange(id: string, path: string, callback: (newVal: any) => void) {
|
||||||
|
return this.get(id)?.onDataChange(path, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public offDataChange(id: string, path: string, callback: (newVal: any) => void) {
|
||||||
|
return this.get(id)?.offDataChange(path, callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DataSourceManager.register('http', HttpDataSource as any);
|
DataSourceManager.register('http', HttpDataSource as any);
|
||||||
|
@ -18,8 +18,10 @@
|
|||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
import type { AppCore, CodeBlockContent, DataSchema, DataSourceSchema } from '@tmagic/schema';
|
import type { AppCore, CodeBlockContent, DataSchema, DataSourceSchema } from '@tmagic/schema';
|
||||||
import { getDefaultValueFromFields, setValueByKeyPath } from '@tmagic/utils';
|
import { getDefaultValueFromFields } from '@tmagic/utils';
|
||||||
|
|
||||||
|
import { ObservedData } from '@data-source/observed-data/ObservedData';
|
||||||
|
import { SimpleObservedData } from '@data-source/observed-data/SimpleObservedData';
|
||||||
import type { ChangeEvent, DataSourceOptions } from '@data-source/types';
|
import type { ChangeEvent, DataSourceOptions } from '@data-source/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,8 +30,6 @@ import type { ChangeEvent, DataSourceOptions } from '@data-source/types';
|
|||||||
export default class DataSource<T extends DataSourceSchema = DataSourceSchema> extends EventEmitter {
|
export default class DataSource<T extends DataSourceSchema = DataSourceSchema> extends EventEmitter {
|
||||||
public isInit = false;
|
public isInit = false;
|
||||||
|
|
||||||
public data: Record<string, any> = {};
|
|
||||||
|
|
||||||
/** @tmagic/core 实例 */
|
/** @tmagic/core 实例 */
|
||||||
public app: AppCore;
|
public app: AppCore;
|
||||||
|
|
||||||
@ -38,6 +38,7 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
|
|||||||
#type = 'base';
|
#type = 'base';
|
||||||
#id: string;
|
#id: string;
|
||||||
#schema: T;
|
#schema: T;
|
||||||
|
#observedData: ObservedData;
|
||||||
|
|
||||||
/** 数据源自定义字段配置 */
|
/** 数据源自定义字段配置 */
|
||||||
#fields: DataSchema[] = [];
|
#fields: DataSchema[] = [];
|
||||||
@ -55,22 +56,27 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
|
|||||||
this.setFields(options.schema.fields);
|
this.setFields(options.schema.fields);
|
||||||
this.setMethods(options.schema.methods || []);
|
this.setMethods(options.schema.methods || []);
|
||||||
|
|
||||||
|
let data = options.initialData;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const ObservedDataClass = options.ObservedDataClass || SimpleObservedData;
|
||||||
if (this.app.platform === 'editor') {
|
if (this.app.platform === 'editor') {
|
||||||
// 编辑器中有mock使用mock,没有使用默认值
|
// 编辑器中有mock使用mock,没有使用默认值
|
||||||
this.mockData = options.schema.mocks?.find((mock) => mock.useInEditor)?.data || this.getDefaultData();
|
this.mockData = options.schema.mocks?.find((mock) => mock.useInEditor)?.data || this.getDefaultData();
|
||||||
this.setData(this.mockData);
|
data = this.mockData;
|
||||||
} else if (typeof options.useMock === 'boolean' && options.useMock) {
|
} else if (typeof options.useMock === 'boolean' && options.useMock) {
|
||||||
// 设置了使用mock就使用mock数据
|
// 设置了使用mock就使用mock数据
|
||||||
this.mockData = options.schema.mocks?.find((mock) => mock.enable)?.data || this.getDefaultData();
|
this.mockData = options.schema.mocks?.find((mock) => mock.enable)?.data || this.getDefaultData();
|
||||||
this.setData(this.mockData);
|
data = this.mockData;
|
||||||
} else if (!options.initialData) {
|
} else if (!options.initialData) {
|
||||||
this.setData(this.getDefaultData());
|
data = this.getDefaultData();
|
||||||
} else {
|
} else {
|
||||||
// 在ssr模式下,会将server端获取的数据设置到initialData
|
// 在ssr模式下,会将server端获取的数据设置到initialData
|
||||||
this.setData(options.initialData);
|
this.#observedData = new ObservedDataClass(options.initialData ?? {});
|
||||||
// 设置isInit,防止manager中执行init方法
|
// 设置isInit,防止manager中执行init方法
|
||||||
this.isInit = true;
|
this.isInit = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.#observedData = new ObservedDataClass(data ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public get id() {
|
public get id() {
|
||||||
@ -101,13 +107,12 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
|
|||||||
this.#methods = methods;
|
this.#methods = methods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get data() {
|
||||||
|
return this.#observedData.getData('');
|
||||||
|
}
|
||||||
|
|
||||||
public setData(data: any, path?: string) {
|
public setData(data: any, path?: string) {
|
||||||
if (path) {
|
this.#observedData.update(data, path);
|
||||||
setValueByKeyPath(path, data, this.data);
|
|
||||||
} else {
|
|
||||||
// todo: 校验数据,看是否符合 schema
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeEvent: ChangeEvent = {
|
const changeEvent: ChangeEvent = {
|
||||||
updateData: data,
|
updateData: data,
|
||||||
@ -117,6 +122,14 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
|
|||||||
this.emit('change', changeEvent);
|
this.emit('change', changeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDataChange(path: string, callback: (newVal: any) => void) {
|
||||||
|
this.#observedData.on(path, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public offDataChange(path: string, callback: (newVal: any) => void) {
|
||||||
|
this.#observedData.off(path, callback);
|
||||||
|
}
|
||||||
|
|
||||||
public getDefaultData() {
|
public getDefaultData() {
|
||||||
return getDefaultValueFromFields(this.#fields);
|
return getDefaultValueFromFields(this.#fields);
|
||||||
}
|
}
|
||||||
@ -126,8 +139,8 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
|
|||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
this.data = {};
|
|
||||||
this.#fields = [];
|
this.#fields = [];
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
|
this.#observedData.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,6 @@
|
|||||||
export { default as DataSourceManager } from './DataSourceManager';
|
export { default as DataSourceManager } from './DataSourceManager';
|
||||||
export * from './data-sources';
|
export * from './data-sources';
|
||||||
export * from './createDataSourceManager';
|
export * from './createDataSourceManager';
|
||||||
|
export * from './observed-data';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
49
packages/data-source/src/observed-data/DeepObservedData.ts
Normal file
49
packages/data-source/src/observed-data/DeepObservedData.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import State from 'deep-state-observer';
|
||||||
|
|
||||||
|
import { ObservedData } from './ObservedData';
|
||||||
|
|
||||||
|
const ignoreFirstCall = <F extends (...args: any[]) => any>(fn: F) => {
|
||||||
|
let calledTimes = 0;
|
||||||
|
return (...args: Parameters<F>) => {
|
||||||
|
if (calledTimes === 0) {
|
||||||
|
calledTimes += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return fn(...args);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DeepObservedData extends ObservedData {
|
||||||
|
state?: State;
|
||||||
|
subscribers = new Map<string, Map<Function, () => void>>();
|
||||||
|
constructor(initialData: Record<string, any>) {
|
||||||
|
super();
|
||||||
|
this.state = new State(initialData);
|
||||||
|
}
|
||||||
|
update = (data: any, path?: string) => {
|
||||||
|
this.state?.update(path ?? '', data);
|
||||||
|
};
|
||||||
|
on = (path: string, callback: (newVal: any) => void) => {
|
||||||
|
// subscribe 会立即执行一次,ignoreFirstCall 会忽略第一次执行
|
||||||
|
const unsubscribe = this.state!.subscribe(path, ignoreFirstCall(callback));
|
||||||
|
|
||||||
|
// 把取消监听的函数保存下来,供 off 时调用
|
||||||
|
const pathSubscribers = this.subscribers.get(path) ?? new Map<Function, () => void>();
|
||||||
|
pathSubscribers.set(callback, unsubscribe);
|
||||||
|
this.subscribers.set(path, pathSubscribers);
|
||||||
|
};
|
||||||
|
off = (path: string, callback: (newVal: any) => void) => {
|
||||||
|
const pathSubscribers = this.subscribers.get(path);
|
||||||
|
if (!pathSubscribers) return;
|
||||||
|
|
||||||
|
pathSubscribers.get(callback)?.();
|
||||||
|
pathSubscribers.delete(callback);
|
||||||
|
};
|
||||||
|
getData = (path: string) => (!this.state ? {} : this.state?.get(path));
|
||||||
|
destroy = () => {
|
||||||
|
// 销毁所有未被取消的监听
|
||||||
|
this.subscribers.forEach((pathSubscribers) => {
|
||||||
|
pathSubscribers.forEach((unsubscribe) => unsubscribe());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
11
packages/data-source/src/observed-data/ObservedData.ts
Normal file
11
packages/data-source/src/observed-data/ObservedData.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export abstract class ObservedData {
|
||||||
|
abstract update(data: any, path?: string): void;
|
||||||
|
|
||||||
|
abstract on(path: string, callback: (newVal: any) => void): void;
|
||||||
|
|
||||||
|
abstract off(path: string, callback: (newVal: any) => void): void;
|
||||||
|
|
||||||
|
abstract getData(path: string): any;
|
||||||
|
|
||||||
|
abstract destroy(): void;
|
||||||
|
}
|
38
packages/data-source/src/observed-data/SimpleObservedData.ts
Normal file
38
packages/data-source/src/observed-data/SimpleObservedData.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
import { getValueByKeyPath, setValueByKeyPath } from '@tmagic/utils';
|
||||||
|
|
||||||
|
import { ObservedData } from './ObservedData';
|
||||||
|
|
||||||
|
export class SimpleObservedData extends ObservedData {
|
||||||
|
data: Record<string, any> = {};
|
||||||
|
private event = new EventEmitter();
|
||||||
|
|
||||||
|
constructor(initialData: Record<string, any>) {
|
||||||
|
super();
|
||||||
|
this.data = initialData;
|
||||||
|
}
|
||||||
|
update(data: any, path?: string): void {
|
||||||
|
if (path) {
|
||||||
|
setValueByKeyPath(path, data, this.data);
|
||||||
|
} else {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeEvent = {
|
||||||
|
updateData: data,
|
||||||
|
path: path ?? '',
|
||||||
|
};
|
||||||
|
this.event.emit(path ?? '', changeEvent);
|
||||||
|
}
|
||||||
|
on(path: string, callback: (newVal: any) => void): void {
|
||||||
|
this.event.on(path, callback);
|
||||||
|
}
|
||||||
|
off(path: string, callback: (newVal: any) => void): void {
|
||||||
|
this.event.off(path, callback);
|
||||||
|
}
|
||||||
|
getData(path: string) {
|
||||||
|
return path ? getValueByKeyPath(path, this.data) : this.data;
|
||||||
|
}
|
||||||
|
destroy(): void {}
|
||||||
|
}
|
3
packages/data-source/src/observed-data/index.ts
Normal file
3
packages/data-source/src/observed-data/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { ObservedData } from './ObservedData';
|
||||||
|
export { DeepObservedData } from './DeepObservedData';
|
||||||
|
export { SimpleObservedData } from './SimpleObservedData';
|
@ -2,6 +2,9 @@ import type { AppCore, DataSourceSchema, HttpOptions, RequestFunction } from '@t
|
|||||||
|
|
||||||
import type DataSource from './data-sources/Base';
|
import type DataSource from './data-sources/Base';
|
||||||
import type HttpDataSource from './data-sources/Http';
|
import type HttpDataSource from './data-sources/Http';
|
||||||
|
import { ObservedData } from './observed-data/ObservedData';
|
||||||
|
|
||||||
|
export type ObservedDataClass = new (...args: any[]) => ObservedData;
|
||||||
|
|
||||||
export interface DataSourceOptions<T extends DataSourceSchema = DataSourceSchema> {
|
export interface DataSourceOptions<T extends DataSourceSchema = DataSourceSchema> {
|
||||||
schema: T;
|
schema: T;
|
||||||
@ -9,6 +12,7 @@ export interface DataSourceOptions<T extends DataSourceSchema = DataSourceSchema
|
|||||||
initialData?: Record<string, any>;
|
initialData?: Record<string, any>;
|
||||||
useMock?: boolean;
|
useMock?: boolean;
|
||||||
request?: RequestFunction;
|
request?: RequestFunction;
|
||||||
|
ObservedDataClass?: ObservedDataClass;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,14 +38,14 @@ import { computed, inject, ref, resolveComponent, watch } from 'vue';
|
|||||||
import { Coin, Edit, View } from '@element-plus/icons-vue';
|
import { Coin, Edit, View } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { TMagicButton } from '@tmagic/design';
|
import { TMagicButton } from '@tmagic/design';
|
||||||
import type { CascaderConfig, CascaderOption, FieldProps, FormState } from '@tmagic/form';
|
import type { CascaderConfig, FieldProps, FormState } from '@tmagic/form';
|
||||||
import { filterFunction, MCascader } from '@tmagic/form';
|
import { filterFunction, MCascader } from '@tmagic/form';
|
||||||
import type { DataSchema, DataSourceFieldType } from '@tmagic/schema';
|
|
||||||
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils';
|
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils';
|
||||||
|
|
||||||
import MIcon from '@editor/components/Icon.vue';
|
import MIcon from '@editor/components/Icon.vue';
|
||||||
import type { DataSourceFieldSelectConfig, EventBus, Services } from '@editor/type';
|
import type { DataSourceFieldSelectConfig, EventBus, Services } from '@editor/type';
|
||||||
import { SideItemKey } from '@editor/type';
|
import { SideItemKey } from '@editor/type';
|
||||||
|
import { getCascaderOptionsFromFields } from '@editor/utils';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MFieldsDataSourceFieldSelect',
|
name: 'MFieldsDataSourceFieldSelect',
|
||||||
@ -76,43 +76,6 @@ const selectedDataSourceId = computed(() => {
|
|||||||
|
|
||||||
const dataSources = computed(() => services?.dataSourceService.get('dataSources'));
|
const dataSources = computed(() => services?.dataSourceService.get('dataSources'));
|
||||||
|
|
||||||
const getOptionChildren = (
|
|
||||||
fields: DataSchema[] = [],
|
|
||||||
dataSourceFieldType: DataSourceFieldType[] = ['any'],
|
|
||||||
): CascaderOption[] => {
|
|
||||||
const child: CascaderOption[] = [];
|
|
||||||
fields.forEach((field) => {
|
|
||||||
if (!dataSourceFieldType.length) {
|
|
||||||
dataSourceFieldType.push('any');
|
|
||||||
}
|
|
||||||
|
|
||||||
const children = getOptionChildren(field.fields, dataSourceFieldType);
|
|
||||||
|
|
||||||
const item = {
|
|
||||||
label: field.title || field.name,
|
|
||||||
value: field.name,
|
|
||||||
children,
|
|
||||||
};
|
|
||||||
|
|
||||||
const fieldType = field.type || 'any';
|
|
||||||
if (dataSourceFieldType.includes('any') || dataSourceFieldType.includes(fieldType)) {
|
|
||||||
child.push(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dataSourceFieldType.includes(fieldType) && !['array', 'object', 'any'].includes(fieldType)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!children.length && ['object', 'array', 'any'].includes(field.type || '')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
child.push(item);
|
|
||||||
});
|
|
||||||
return child;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cascaderConfig = computed<CascaderConfig>(() => {
|
const cascaderConfig = computed<CascaderConfig>(() => {
|
||||||
const valueIsKey = props.config.value === 'key';
|
const valueIsKey = props.config.value === 'key';
|
||||||
|
|
||||||
@ -125,7 +88,7 @@ const cascaderConfig = computed<CascaderConfig>(() => {
|
|||||||
dataSources.value?.map((ds) => ({
|
dataSources.value?.map((ds) => ({
|
||||||
label: ds.title || ds.id,
|
label: ds.title || ds.id,
|
||||||
value: valueIsKey ? ds.id : `${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}${ds.id}`,
|
value: valueIsKey ? ds.id : `${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}${ds.id}`,
|
||||||
children: getOptionChildren(ds.fields, props.config.dataSourceFieldType),
|
children: getCascaderOptionsFromFields(ds.fields, props.config.dataSourceFieldType),
|
||||||
})) || [];
|
})) || [];
|
||||||
return options.filter((option) => option.children.length);
|
return options.filter((option) => option.children.length);
|
||||||
},
|
},
|
||||||
|
@ -55,11 +55,13 @@ import { has } from 'lodash-es';
|
|||||||
|
|
||||||
import type { EventOption } from '@tmagic/core';
|
import type { EventOption } from '@tmagic/core';
|
||||||
import { TMagicButton } from '@tmagic/design';
|
import { TMagicButton } from '@tmagic/design';
|
||||||
import type { FieldProps, FormState, PanelConfig } from '@tmagic/form';
|
import type { CascaderOption, FieldProps, FormState, PanelConfig } from '@tmagic/form';
|
||||||
import { MContainer, MPanel } from '@tmagic/form';
|
import { MContainer, MPanel } from '@tmagic/form';
|
||||||
import { ActionType } from '@tmagic/schema';
|
import { ActionType } from '@tmagic/schema';
|
||||||
|
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX } from '@tmagic/utils';
|
||||||
|
|
||||||
import type { CodeSelectColConfig, DataSourceMethodSelectConfig, EventSelectConfig, Services } from '@editor/type';
|
import type { CodeSelectColConfig, DataSourceMethodSelectConfig, EventSelectConfig, Services } from '@editor/type';
|
||||||
|
import { getCascaderOptionsFromFields } from '@editor/utils';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MFieldsEventSelect',
|
name: 'MFieldsEventSelect',
|
||||||
@ -78,26 +80,45 @@ const codeBlockService = services?.codeBlockService;
|
|||||||
|
|
||||||
// 事件名称下拉框表单配置
|
// 事件名称下拉框表单配置
|
||||||
const eventNameConfig = computed(() => {
|
const eventNameConfig = computed(() => {
|
||||||
|
const fieldType = props.config.src === 'component' ? 'select' : 'cascader';
|
||||||
const defaultEventNameConfig = {
|
const defaultEventNameConfig = {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
text: '事件',
|
text: '事件',
|
||||||
type: 'select',
|
type: fieldType,
|
||||||
labelWidth: '40px',
|
labelWidth: '40px',
|
||||||
|
checkStrictly: true,
|
||||||
|
valueSeparator: '.',
|
||||||
options: (mForm: FormState, { formValue }: any) => {
|
options: (mForm: FormState, { formValue }: any) => {
|
||||||
let events: EventOption[] = [];
|
let events: EventOption[] | CascaderOption[] = [];
|
||||||
|
|
||||||
if (!eventsService || !dataSourceService) return events;
|
if (!eventsService || !dataSourceService) return events;
|
||||||
|
|
||||||
if (props.config.src === 'component') {
|
if (props.config.src === 'component') {
|
||||||
events = eventsService.getEvent(formValue.type);
|
events = eventsService.getEvent(formValue.type);
|
||||||
} else if (props.config.src === 'datasource') {
|
return events.map((option) => ({
|
||||||
events = dataSourceService.getFormEvent(formValue.type);
|
text: option.label,
|
||||||
|
value: option.value,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
if (props.config.src === 'datasource') {
|
||||||
|
// 从数据源类型中获取到相关事件
|
||||||
|
events = dataSourceService.getFormEvent(formValue.type);
|
||||||
|
// 从数据源类型和实例中分别获取数据以追加数据变化的事件
|
||||||
|
const dataSource = dataSourceService.getDataSourceById(formValue.id);
|
||||||
|
const fields = dataSource?.fields || [];
|
||||||
|
if (fields.length > 0) {
|
||||||
|
return [
|
||||||
|
...events,
|
||||||
|
{
|
||||||
|
label: '数据变化',
|
||||||
|
value: DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX,
|
||||||
|
children: getCascaderOptionsFromFields(fields),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return events.map((option) => ({
|
return events;
|
||||||
text: option.label,
|
}
|
||||||
value: option.value,
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { ...defaultEventNameConfig, ...props.config.eventNameConfig };
|
return { ...defaultEventNameConfig, ...props.config.eventNameConfig };
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FormConfig, FormState } from '@tmagic/form';
|
import { CascaderOption, FormConfig, FormState } from '@tmagic/form';
|
||||||
import { DataSchema, DataSourceSchema } from '@tmagic/schema';
|
import { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/schema';
|
||||||
|
|
||||||
import BaseFormConfig from './formConfigs/base';
|
import BaseFormConfig from './formConfigs/base';
|
||||||
import HttpFormConfig from './formConfigs/http';
|
import HttpFormConfig from './formConfigs/http';
|
||||||
@ -32,7 +32,6 @@ const fillConfig = (config: FormConfig): FormConfig => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '事件配置',
|
title: '事件配置',
|
||||||
display: false,
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: 'events',
|
name: 'events',
|
||||||
@ -198,3 +197,40 @@ export const getDisplayField = (dataSources: DataSourceSchema[], key: string) =>
|
|||||||
|
|
||||||
return displayState;
|
return displayState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCascaderOptionsFromFields = (
|
||||||
|
fields: DataSchema[] = [],
|
||||||
|
dataSourceFieldType: DataSourceFieldType[] = ['any'],
|
||||||
|
): CascaderOption[] => {
|
||||||
|
const child: CascaderOption[] = [];
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if (!dataSourceFieldType.length) {
|
||||||
|
dataSourceFieldType.push('any');
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = getCascaderOptionsFromFields(field.fields, dataSourceFieldType);
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
label: field.title || field.name,
|
||||||
|
value: field.name,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldType = field.type || 'any';
|
||||||
|
if (dataSourceFieldType.includes('any') || dataSourceFieldType.includes(fieldType)) {
|
||||||
|
child.push(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dataSourceFieldType.includes(fieldType) && !['array', 'object', 'any'].includes(fieldType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!children.length && ['object', 'array', 'any'].includes(field.type || '')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.push(item);
|
||||||
|
});
|
||||||
|
return child;
|
||||||
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<TMagicCascader
|
<TMagicCascader
|
||||||
v-model="model[name]"
|
v-model="value"
|
||||||
ref="tMagicCascader"
|
ref="tMagicCascader"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
clearable
|
clearable
|
||||||
@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, ref, watchEffect } from 'vue';
|
import { computed, inject, ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
import { TMagicCascader } from '@tmagic/design';
|
import { TMagicCascader } from '@tmagic/design';
|
||||||
|
|
||||||
@ -47,6 +47,22 @@ const tMagicCascader = ref<InstanceType<typeof TMagicCascader>>();
|
|||||||
const options = ref<CascaderOption[]>([]);
|
const options = ref<CascaderOption[]>([]);
|
||||||
const remoteData = ref<any>(null);
|
const remoteData = ref<any>(null);
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
if (typeof props.model[props.name] === 'string' && props.config.valueSeparator) {
|
||||||
|
return props.model[props.name].split(props.config.valueSeparator);
|
||||||
|
}
|
||||||
|
return props.model[props.name];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
let result = value;
|
||||||
|
if (props.config.valueSeparator) {
|
||||||
|
result = value.join(props.config.valueSeparator);
|
||||||
|
}
|
||||||
|
props.model[props.name] = result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const setRemoteOptions = async function () {
|
const setRemoteOptions = async function () {
|
||||||
const { config } = props;
|
const { config } = props;
|
||||||
const { option } = config;
|
const { option } = config;
|
||||||
@ -82,9 +98,18 @@ const setRemoteOptions = async function () {
|
|||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
if (typeof props.config.options === 'function' && props.model && mForm) {
|
if (typeof props.config.options === 'function' && props.model && mForm) {
|
||||||
watchEffect(
|
watchEffect(() => {
|
||||||
() => (options.value = (props.config.options as Function)(mForm, { model: props.model, formValues: mForm.values })),
|
typeof props.config.options === 'function' &&
|
||||||
);
|
Promise.resolve(
|
||||||
|
props.config.options(mForm, {
|
||||||
|
model: props.model,
|
||||||
|
prop: props.prop,
|
||||||
|
formValue: mForm?.values,
|
||||||
|
}),
|
||||||
|
).then((data) => {
|
||||||
|
options.value = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
} else if (!props.config.options?.length || props.config.remote) {
|
} else if (!props.config.options?.length || props.config.remote) {
|
||||||
Promise.resolve(setRemoteOptions());
|
Promise.resolve(setRemoteOptions());
|
||||||
} else if (Array.isArray(props.config.options)) {
|
} else if (Array.isArray(props.config.options)) {
|
||||||
@ -93,10 +118,10 @@ if (typeof props.config.options === 'function' && props.model && mForm) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeHandler = (value: any) => {
|
const changeHandler = () => {
|
||||||
if (!tMagicCascader.value) return;
|
if (!tMagicCascader.value) return;
|
||||||
tMagicCascader.value.setQuery('');
|
tMagicCascader.value.setQuery('');
|
||||||
tMagicCascader.value.setPreviousQuery(null);
|
tMagicCascader.value.setPreviousQuery(null);
|
||||||
emit('change', value);
|
emit('change', props.model[props.name]);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -539,12 +539,15 @@ export interface CascaderConfig extends FormItem, Input {
|
|||||||
checkStrictly?: boolean;
|
checkStrictly?: boolean;
|
||||||
/** 弹出内容的自定义类名 */
|
/** 弹出内容的自定义类名 */
|
||||||
popperClass?: string;
|
popperClass?: string;
|
||||||
|
/** 合并成字符串时的分隔符 */
|
||||||
|
valueSeparator?: string;
|
||||||
options?:
|
options?:
|
||||||
| ((
|
| ((
|
||||||
mForm: FormState | undefined,
|
mForm: FormState | undefined,
|
||||||
data: {
|
data: {
|
||||||
model: Record<any, any>;
|
model: Record<any, any>;
|
||||||
formValues: Record<any, any>;
|
prop: string;
|
||||||
|
formValue: Record<any, any>;
|
||||||
},
|
},
|
||||||
) => CascaderOption[])
|
) => CascaderOption[])
|
||||||
| CascaderOption[];
|
| CascaderOption[];
|
||||||
|
@ -258,6 +258,8 @@ export interface DataSourceSchema {
|
|||||||
methods: CodeBlockContent[];
|
methods: CodeBlockContent[];
|
||||||
/** mock数据 */
|
/** mock数据 */
|
||||||
mocks?: MockSchema[];
|
mocks?: MockSchema[];
|
||||||
|
/** 事件 */
|
||||||
|
events: EventConfig[];
|
||||||
/** 不执行init的环境 */
|
/** 不执行init的环境 */
|
||||||
disabledInitInJsEngine?: (JsEngine | string)[];
|
disabledInitInJsEngine?: (JsEngine | string)[];
|
||||||
/** 扩展字段 */
|
/** 扩展字段 */
|
||||||
|
@ -390,6 +390,8 @@ export const getDefaultValueFromFields = (fields: DataSchema[]) => {
|
|||||||
|
|
||||||
export const DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX = 'ds-field::';
|
export const DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX = 'ds-field::';
|
||||||
|
|
||||||
|
export const DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX = 'ds-field-changed';
|
||||||
|
|
||||||
export const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
|
export const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
|
||||||
|
|
||||||
export const calculatePercentage = (value: number, percentageStr: string) => {
|
export const calculatePercentage = (value: number, percentageStr: string) => {
|
||||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -205,6 +205,9 @@ importers:
|
|||||||
'@tmagic/utils':
|
'@tmagic/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../utils
|
version: link:../utils
|
||||||
|
deep-state-observer:
|
||||||
|
specifier: ^5.5.13
|
||||||
|
version: 5.5.13
|
||||||
events:
|
events:
|
||||||
specifier: ^3.3.0
|
specifier: ^3.3.0
|
||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
@ -3884,6 +3887,9 @@ packages:
|
|||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
|
deep-state-observer@5.5.13:
|
||||||
|
resolution: {integrity: sha512-Ai55DB6P/k/EBgC4jNlYqIgp8e6Mzl7E/4vzIDMfrJ+TnCFmeA7TySaa3BapioDz4Cr6dYamVI4Mx2FMtpfM4w==}
|
||||||
|
|
||||||
defaults@1.0.4:
|
defaults@1.0.4:
|
||||||
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
|
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
|
||||||
|
|
||||||
@ -9620,6 +9626,8 @@ snapshots:
|
|||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
|
deep-state-observer@5.5.13: {}
|
||||||
|
|
||||||
defaults@1.0.4:
|
defaults@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
clone: 1.0.4
|
clone: 1.0.4
|
||||||
|
Loading…
x
Reference in New Issue
Block a user