import { obx, computed, makeObservable, action, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; import { Designer } from '../designer'; import { DocumentModel, isDocumentModel } from '../document'; import { IPublicTypeProjectSchema, IPublicTypeRootSchema, IPublicTypeComponentsMap, IPublicApiProject, IPublicModelDocumentModel, IPublicEnumTransformStage, } from '@alilc/lowcode-types'; import { isLowCodeComponentType, isProCodeComponentType } from '@alilc/lowcode-utils'; import { ISimulatorHost } from '../simulator'; export interface IProject extends IPublicApiProject { } export class Project implements IProject { private emitter: IEventBus = createModuleEventBus('Project'); @obx.shallow readonly documents: IPublicModelDocumentModel[] = []; private data: IPublicTypeProjectSchema = { version: '1.0.0', componentsMap: [], componentsTree: [], i18n: {}, }; private _simulator?: ISimulatorHost; /** * 模拟器 */ get simulator(): ISimulatorHost | null { return this._simulator || null; } key = Math.random(); // TODO: 考虑项目级别 History constructor(readonly designer: Designer, schema?: IPublicTypeProjectSchema, readonly viewName = 'global') { makeObservable(this); this.load(schema); } @computed get currentDocument() { return this.documents.find((doc) => doc.active); } @obx private _config: any = {}; @computed get config(): any { // TODO: parse layout Component return this._config; } set config(value: any) { this._config = value; } @obx.ref private _i18n: any = {}; @computed get i18n(): any { return this._i18n; } set i18n(value: any) { this._i18n = value || {}; } private getComponentsMap(): IPublicTypeComponentsMap { return this.documents.reduce(( compomentsMap: IPublicTypeComponentsMap, curDoc: DocumentModel, ) => { const curComponentsMap = curDoc.getComponentsMap(); if (Array.isArray(curComponentsMap)) { curComponentsMap.forEach((item) => { const found = compomentsMap.find((eItem) => { if ( isProCodeComponentType(eItem) && isProCodeComponentType(item) && eItem.package === item.package && eItem.componentName === item.componentName ) { return true; } else if ( isLowCodeComponentType(eItem) && eItem.componentName === item.componentName ) { return true; } return false; }); if (found) return; compomentsMap.push(item); }); } return compomentsMap; }, [] as IPublicTypeComponentsMap); } /** * 获取项目整体 schema */ getSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): IPublicTypeProjectSchema { return { ...this.data, componentsMap: this.getComponentsMap(), componentsTree: this.documents .filter((doc) => !doc.isBlank()) .map((doc) => doc.export(stage)), i18n: this.i18n, }; } /** * 替换当前 document 的 schema,并触发渲染器的 render * @param schema */ setSchema(schema?: IPublicTypeProjectSchema) { // FIXME: 这里的行为和 getSchema 并不对等,感觉不太对 const doc = this.documents.find((doc) => doc.active); doc && doc.import(schema?.componentsTree[0]); this.simulator?.rerender(); } /** * 整体设置项目 schema * * @param autoOpen true 自动打开文档 string 指定打开的文件 */ @action load(schema?: IPublicTypeProjectSchema, autoOpen?: boolean | string) { this.unload(); // load new document this.data = { version: '1.0.0', componentsMap: [], componentsTree: [], i18n: {}, ...schema, }; this.config = schema?.config || this.config; this.i18n = schema?.i18n || this.i18n; if (autoOpen) { if (autoOpen === true) { // auto open first document or open a blank page // this.open(this.data.componentsTree[0]); const documentInstances = this.data.componentsTree.map((data) => this.createDocument(data)); // TODO: 暂时先读 config tabBar 里的值,后面看整个 layout 结构是否能作为引擎规范 if (this.config?.layout?.props?.tabBar?.items?.length > 0) { // slice(1) 这个贼不雅,默认任务 fileName 是类'/fileName'的形式 documentInstances .find((i) => i.fileName === this.config.layout.props.tabBar.items[0].path?.slice(1)) ?.open(); } else { documentInstances[0].open(); } } else { // auto open should be string of fileName this.open(autoOpen); } } } /** * 卸载当前项目数据 */ unload() { if (this.documents.length < 1) { return; } for (let i = this.documents.length - 1; i >= 0; i--) { this.documents[i].remove(); } } removeDocument(doc: IPublicModelDocumentModel) { const index = this.documents.indexOf(doc); if (index < 0) { return; } this.documents.splice(index, 1); this.documentsMap.delete(doc.id); } /** * 分字段设置储存数据,不记录操作记录 */ set( // eslint-disable-next-line @typescript-eslint/no-unused-vars key: | 'version' | 'componentsTree' | 'componentsMap' | 'utils' | 'constants' | 'i18n' | 'css' | 'dataSource' | string, // eslint-disable-next-line @typescript-eslint/no-unused-vars value: any, ): void { if (key === 'config') { this.config = value; } if (key === 'i18n') { this.i18n = value; } Object.assign(this.data, { [key]: value }); } /** * 分字段设置储存数据 */ get( // eslint-disable-next-line @typescript-eslint/no-unused-vars key: | 'version' | 'componentsTree' | 'componentsMap' | 'utils' | 'constants' | 'i18n' | 'css' | 'dataSource' | 'config' | string, ): any { if (key === 'config') { return this.config; } if (key === 'i18n') { return this.i18n; } return Reflect.get(this.data, key); } private documentsMap = new Map(); getDocument(id: string): DocumentModel | null { // 此处不能使用 this.documentsMap.get(id),因为在乐高 rollback 场景,document.id 会被改成其他值 return this.documents.find((doc) => doc.id === id) || null; } getDocumentByFileName(fileName: string): DocumentModel | null { return this.documents.find((doc) => doc.fileName === fileName) || null; } @action createDocument(data?: IPublicTypeRootSchema): DocumentModel { const doc = new DocumentModel(this, data || this?.data?.componentsTree?.[0]); this.documents.push(doc); this.documentsMap.set(doc.id, doc); return doc; } open(doc?: string | DocumentModel | IPublicTypeRootSchema): DocumentModel | null { if (!doc) { const got = this.documents.find((item) => item.isBlank()); if (got) { return got.open(); } doc = this.createDocument(); return doc.open(); } if (typeof doc === 'string') { const got = this.documents.find((item) => item.fileName === doc || item.id === doc); if (got) { return got.open(); } const data = this.data.componentsTree.find((data) => data.fileName === doc); if (data) { doc = this.createDocument(data); return doc.open(); } return null; } else if (isDocumentModel(doc)) { return doc.open(); } // else if (isPageSchema(doc)) { // 暂时注释掉,影响了 diff 功能 // const foundDoc = this.documents.find(curDoc => curDoc?.rootNode?.id && curDoc?.rootNode?.id === doc?.id); // if (foundDoc) { // foundDoc.remove(); // } // } doc = this.createDocument(doc); return doc.open(); } checkExclusive(activeDoc: DocumentModel) { this.documents.forEach((doc) => { if (doc !== activeDoc) { doc.suspense(); } }); this.emitter.emit('current-document.change', activeDoc); } closeOthers(opened: DocumentModel) { this.documents.forEach((doc) => { if (doc !== opened) { doc.close(); } }); } mountSimulator(simulator: ISimulatorHost) { // TODO: 多设备 simulator 支持 this._simulator = simulator; this.emitter.emit('lowcode_engine_simulator_ready', simulator); } setRendererReady(renderer: any) { this.emitter.emit('lowcode_engine_renderer_ready', renderer); } onSimulatorReady(fn: (args: any) => void): () => void { this.emitter.on('lowcode_engine_simulator_ready', fn); return () => { this.emitter.removeListener('lowcode_engine_simulator_ready', fn); }; } onRendererReady(fn: (args: any) => void): () => void { this.emitter.on('lowcode_engine_renderer_ready', fn); return () => { this.emitter.removeListener('lowcode_engine_renderer_ready', fn); }; } onCurrentDocumentChange(fn: (doc: DocumentModel) => void): () => void { this.emitter.on('current-document.change', fn); return () => { this.emitter.removeListener('current-document.change', fn); }; } }