feat(editor): 组件代码块的绑定关系记录到dsl中,修复删除组件解除关系的问题,代码块dsl支持扩展字段

This commit is contained in:
parisma 2022-09-22 15:14:52 +08:00 committed by jia000
parent 0b3585c150
commit 92f3696e44
7 changed files with 201 additions and 154 deletions

View File

@ -65,7 +65,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, onUnmounted, PropType, provide, reactive, toRaw, watch } from 'vue'; import { defineComponent, onUnmounted, PropType, provide, reactive, toRaw, watch } from 'vue';
import { isEmpty,union } from 'lodash-es';
import { EventOption } from '@tmagic/core'; import { EventOption } from '@tmagic/core';
import type { FormConfig } from '@tmagic/form'; import type { FormConfig } from '@tmagic/form';
@ -210,12 +209,6 @@ export default defineComponent({
updateDragEl: { updateDragEl: {
type: Function as PropType<(el: HTMLDivElement, target: HTMLElement) => void>, type: Function as PropType<(el: HTMLDivElement, target: HTMLElement) => void>,
}, },
/** 可挂载代码块的生命周期 */
codeHooks: {
type: Array<string>,
default: () => ['created', 'mounted'],
},
}, },
emits: ['props-panel-mounted', 'update:modelValue'], emits: ['props-panel-mounted', 'update:modelValue'],
@ -233,38 +226,11 @@ export default defineComponent({
emit('update:modelValue', toRaw(editorService.get('root'))); emit('update:modelValue', toRaw(editorService.get('root')));
}); });
const initCodeRelation = (rootValue: MNode) => {
if (isEmpty(rootValue.items)) return;
rootValue.items.forEach((nodeValue: MNode) => {
let curNodeCombineIds:string[] = []
// Id
props.codeHooks.forEach((hook) => {
// continue
if (isEmpty(nodeValue[hook])) return true
//
if(typeof nodeValue[hook] === 'string' && nodeValue[hook]) {
curNodeCombineIds = union(curNodeCombineIds,[nodeValue[hook]])
}else if(Array.isArray(nodeValue[hook])) {
curNodeCombineIds = union(curNodeCombineIds,nodeValue[hook])
}
});
//
if(!isEmpty(curNodeCombineIds)) {
codeBlockService.setCompRelation(nodeValue.id, curNodeCombineIds);
}
if (!isEmpty(nodeValue.items)) {
initCodeRelation(nodeValue);
}
});
};
// //
watch( watch(
() => props.modelValue, () => props.modelValue,
(modelValue) => { (modelValue) => {
editorService.set('root', modelValue); editorService.set('root', modelValue);
//
initCodeRelation(modelValue);
}, },
{ {
immediate: true, immediate: true,
@ -370,7 +336,6 @@ export default defineComponent({
containerHighlightType: props.containerHighlightType, containerHighlightType: props.containerHighlightType,
}), }),
); );
provide('codeHooks',props.codeHooks)
return services; return services;
}, },

View File

@ -35,14 +35,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineEmits, defineProps, inject, ref, watchEffect } from 'vue'; import { computed, defineEmits, defineProps, inject, ref, watchEffect } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { map, union } from 'lodash-es'; import { cloneDeep, map, xor } from 'lodash-es';
import { SelectConfig } from '@tmagic/form'; import { SelectConfig } from '@tmagic/form';
import type { Services } from '../type'; import type { Services } from '../type';
import { CodeEditorMode } from '../type'; import { CodeEditorMode, CodeSelectOp } from '../type';
const services = inject<Services>('services'); const services = inject<Services>('services');
const codeHooks = inject<string[]>('codeHooks');
const emit = defineEmits(['change']); const emit = defineEmits(['change']);
@ -77,7 +76,9 @@ const selectConfig = computed(() => {
}; };
}); });
const fieldKey = ref(''); const fieldKey = ref('');
const multiple = ref(true);
const combineIds = ref<string[]>([]); const combineIds = ref<string[]>([]);
let lastTagSnapshot = cloneDeep(props.model[props.name]) || [];
watchEffect(async () => { watchEffect(async () => {
const combineNames = await Promise.all( const combineNames = await Promise.all(
@ -90,29 +91,39 @@ watchEffect(async () => {
}); });
const changeHandler = async (value: any) => { const changeHandler = async (value: any) => {
await setCombineRelation(); let codeIds = value;
if (typeof value === 'string') {
multiple.value = false;
lastTagSnapshot = [lastTagSnapshot];
codeIds = value ? [value] : [];
}
await setCombineRelation(codeIds);
emit('change', value); emit('change', value);
}; };
// //
const setCombineRelation = async () => { const setCombineRelation = async (codeIds: string[]) => {
//
combineIds.value = [];
// id // id
const { id = '' } = services?.editorService.get('node') || {}; const { id = '' } = services?.editorService.get('node') || {};
codeHooks?.forEach((hook) => {
// continue //
if (!props.model[hook]) return true; let opFlag = CodeSelectOp.CHANGE;
if (typeof props.model[hook] === 'string' && props.model[hook]) { let diffValues = codeIds;
combineIds.value = union(combineIds.value, [props.model[hook]]); if (multiple.value) {
} else if (Array.isArray(props.model[hook])) { opFlag = codeIds.length < lastTagSnapshot.length ? CodeSelectOp.DELETE : CodeSelectOp.ADD;
combineIds.value = union(combineIds.value, props.model[hook]); diffValues = xor(codeIds, lastTagSnapshot) as string[];
} }
});
// //
await services?.codeBlockService.setCompRelation(id, combineIds.value); await services?.codeBlockService.setCombineRelation(id, diffValues, opFlag, props.prop);
// lastTagSnapshot = codeIds;
await services?.codeBlockService.setCombineIds(combineIds.value); await setCombineIds(codeIds);
};
//
const setCombineIds = async (codeIds: string[]) => {
combineIds.value = codeIds;
await services?.codeBlockService.setCombineIds(codeIds);
}; };
const viewHandler = async () => { const viewHandler = async () => {
@ -120,7 +131,8 @@ const viewHandler = async () => {
ElMessage.error('请先绑定代码块'); ElMessage.error('请先绑定代码块');
return; return;
} }
await setCombineRelation(); //
await setCombineIds(props.model[props.name]);
await services?.codeBlockService.setMode(CodeEditorMode.LIST); await services?.codeBlockService.setMode(CodeEditorMode.LIST);
services?.codeBlockService.setCodeEditorContent(true, combineIds.value[0]); services?.codeBlockService.setCodeEditorContent(true, combineIds.value[0]);
}; };

View File

@ -97,14 +97,14 @@
import { computed, inject, reactive, ref, watch } from 'vue'; import { computed, inject, reactive, ref, watch } from 'vue';
import { Close, Edit, Link, View } from '@element-plus/icons-vue'; import { Close, Edit, Link, View } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { flattenDeep, forIn, isEmpty, values, xor } from 'lodash-es'; import { forIn, isEmpty } from 'lodash-es';
import { Id } from '@tmagic/schema'; import { Id } from '@tmagic/schema';
import StageCore from '@tmagic/stage'; import StageCore from '@tmagic/stage';
import Icon from '../../../components/Icon.vue'; import Icon from '../../../components/Icon.vue';
import type { CodeBlockContent, Services } from '../../../type'; import type { CodeBlockContent, CodeRelation, Services } from '../../../type';
import { CodeDeleteErrorType, CodeDslList, CodeEditorMode, ListState } from '../../../type'; import { CodeDeleteErrorType, CodeDslList, CodeEditorMode, ListRelationState } from '../../../type';
import codeBlockEditor from './CodeBlockEditor.vue'; import codeBlockEditor from './CodeBlockEditor.vue';
@ -113,10 +113,9 @@ const props = defineProps<{
}>(); }>();
const services = inject<Services>('services'); const services = inject<Services>('services');
// const codeHooks = inject<string[]>('codeHooks') || [];
// //
const state = reactive<ListState>({ const state = reactive<ListRelationState>({
codeList: [], codeList: [],
bindComps: {}, bindComps: {},
}); });
@ -124,13 +123,15 @@ const state = reactive<ListState>({
const editable = computed(() => services?.codeBlockService.getEditStatus()); const editable = computed(() => services?.codeBlockService.getEditStatus());
// ID // ID
const getBindCompsByCodeId = (codeId: string) => { const getBindCompsByCodeId = (codeId: string, codeBlockContent: CodeBlockContent) => {
const bindCompIds = services?.codeBlockService.getCodeRelationById(codeId) || []; if (isEmpty(codeBlockContent) || isEmpty(codeBlockContent.comps)) {
if (isEmpty(bindCompIds)) {
state.bindComps[codeId] = []; state.bindComps[codeId] = [];
return; return;
} }
const compsInfo = bindCompIds.map((compId) => ({ const compsField = codeBlockContent.comps as CodeRelation;
const bindCompIds = Object.keys(compsField);
const bindCompsFiltered = bindCompIds.filter((compId) => !isEmpty(compsField[compId]));
const compsInfo = bindCompsFiltered.map((compId) => ({
id: compId, id: compId,
name: getCompName(compId), name: getCompName(compId),
})); }));
@ -143,7 +144,7 @@ const initList = async () => {
if (!codeDsl) return; if (!codeDsl) return;
state.codeList = []; state.codeList = [];
forIn(codeDsl, (value: CodeBlockContent, codeId: string) => { forIn(codeDsl, (value: CodeBlockContent, codeId: string) => {
getBindCompsByCodeId(codeId); getBindCompsByCodeId(codeId, value);
state.codeList.push({ state.codeList.push({
id: codeId, id: codeId,
name: value.name, name: value.name,
@ -160,22 +161,7 @@ watch(
}, },
{ {
immediate: true, immediate: true,
}, deep: true,
);
//
watch(
() => services?.codeBlockService.getCompRelation(),
(curRelation, oldRelation) => {
forIn(curRelation, (codeArr, compId) => {
let oldCodeArr: string[] = [];
if (oldRelation) {
oldCodeArr = oldRelation[compId];
}
//
const diffCodeIds = xor(codeArr, oldCodeArr);
diffCodeIds.forEach((codeId) => getBindCompsByCodeId(codeId));
});
}, },
); );
@ -219,15 +205,14 @@ const editCode = async (key: string) => {
// //
const deleteCode = (key: string) => { const deleteCode = (key: string) => {
const compRelation = services?.codeBlockService.getCompRelation(); const existBinds = !!(state.bindComps[key]?.length > 0);
const codeIds = flattenDeep(values(compRelation));
const undeleteableList = services?.codeBlockService.getUndeletableList() || []; const undeleteableList = services?.codeBlockService.getUndeletableList() || [];
if (!codeIds.includes(key) && !undeleteableList.includes(key)) { if (!existBinds && !undeleteableList.includes(key)) {
// //
services?.codeBlockService.deleteCodeDslByIds([key]); services?.codeBlockService.deleteCodeDslByIds([key]);
} else { } else {
if (typeof props.customError === 'function') { if (typeof props.customError === 'function') {
props.customError(key, codeIds.includes(key) ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE); props.customError(key, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
} else { } else {
ElMessage.error('代码块删除失败'); ElMessage.error('代码块删除失败');
} }

View File

@ -17,14 +17,14 @@
*/ */
import { reactive } from 'vue'; import { reactive } from 'vue';
import { forIn, isEmpty, keys, omit, pick } from 'lodash-es'; import { cloneDeep, forIn, isEmpty, keys, omit, pick } from 'lodash-es';
import { Id } from '@tmagic/schema'; import { Id, MNode } from '@tmagic/schema';
import editorService from '../services/editor'; import editorService from '../services/editor';
import type { CodeBlockContent, CodeBlockDSL, CodeState, CompRelation } from '../type'; import type { CodeBlockContent, CodeBlockDSL, CodeState } from '../type';
import { CodeEditorMode } from '../type'; import { CodeEditorMode, CodeSelectOp } from '../type';
import { info } from '../utils/logger'; import { error, info } from '../utils/logger';
import BaseService from './BaseService'; import BaseService from './BaseService';
@ -36,7 +36,6 @@ class CodeBlock extends BaseService {
editable: true, editable: true,
mode: CodeEditorMode.EDITOR, mode: CodeEditorMode.EDITOR,
combineIds: [], combineIds: [],
compRelation: {},
undeletableList: [], undeletableList: [],
}); });
@ -52,7 +51,6 @@ class CodeBlock extends BaseService {
'setEditStatus', 'setEditStatus',
'setMode', 'setMode',
'setCombineIds', 'setCombineIds',
'setCompRelation',
'setUndeleteableList', 'setUndeleteableList',
'deleteCodeDslByIds', 'deleteCodeDslByIds',
]); ]);
@ -102,9 +100,14 @@ class CodeBlock extends BaseService {
*/ */
public async setCodeDslById(id: string, codeConfig: CodeBlockContent): Promise<void> { public async setCodeDslById(id: string, codeConfig: CodeBlockContent): Promise<void> {
let codeDsl = await this.getCodeDsl(); let codeDsl = await this.getCodeDsl();
if (!codeDsl) return;
const existContent = codeDsl[id] || {};
codeDsl = { codeDsl = {
...codeDsl, ...codeDsl,
[id]: codeConfig, [id]: {
...existContent,
...codeConfig,
},
}; };
await this.setCodeDsl(codeDsl); await this.setCodeDsl(codeDsl);
} }
@ -226,41 +229,73 @@ class CodeBlock extends BaseService {
} }
/** /**
* *
* @param {number | string} compId id * @param {Id} id
* @param {string[]} codeIds id数组 * @param {string[]} diffCodeIds id数组
* @param {CodeSelectOp} opFlag
* @param {string} hook hook名称
* @returns {void} * @returns {void}
*/ */
public async setCompRelation(compId: number | string, codeIds: string[]) { public async setCombineRelation(compId: Id, diffCodeIds: string[], opFlag: CodeSelectOp, hook: string) {
if (!compId) return; const codeDsl = cloneDeep(await this.getCodeDsl());
this.state.compRelation = { if (!codeDsl) return;
...this.state.compRelation, if (opFlag === CodeSelectOp.DELETE) {
[compId]: codeIds, try {
}; diffCodeIds.forEach((codeId) => {
} const compsContent = codeDsl[codeId].comps;
const index = compsContent[compId].findIndex((item) => item === hook);
/** if (index !== -1) {
* compsContent[compId].splice(index, 1);
* @returns {CompRelation} }
*/ });
public getCompRelation(): CompRelation { } catch (e) {
return this.state.compRelation; error(e);
} throw new Error('解绑代码块失败');
/**
*
* @param {string} codeId id
* @returns {Id[]} id数组
*/
public getCodeRelationById(codeId: string): Id[] {
const codeRelation = reactive<Id[]>([]);
const compRelation = this.getCompRelation();
forIn(compRelation, (value, key) => {
if (value.includes(codeId)) {
codeRelation.push(key);
} }
}); } else if (opFlag === CodeSelectOp.ADD) {
return codeRelation; try {
diffCodeIds.forEach((codeId) => {
const compsContent = codeDsl[codeId].comps;
const existHooks = compsContent?.[compId];
if (isEmpty(existHooks)) {
// comps属性不存在或者comps为空新增
codeDsl[codeId].comps = {
...(codeDsl[codeId].comps || {}),
[compId]: [hook],
};
} else {
// 往已有的关系中添加hook
existHooks.push(hook);
}
});
} catch (e) {
error(e);
throw new Error('绑定代码块失败');
}
} else if (opFlag === CodeSelectOp.CHANGE) {
// 单选修改
forIn(codeDsl, (codeBlockContent, codeId) => {
if (codeId === diffCodeIds[0]) {
// 增加
codeBlockContent.comps = {
...(codeBlockContent?.comps || {}),
[compId]: [hook],
};
} else if (isEmpty(diffCodeIds) || codeId !== diffCodeIds[0]) {
// 清空或者移除之前的选项
const compHooks = codeBlockContent?.comps?.[compId];
// continue
if (!compHooks) return true;
const index = compHooks.findIndex((hookName) => hookName === hook);
if (index !== -1) {
compHooks.splice(index, 1);
// break
return false;
}
}
});
}
this.setCodeDsl(codeDsl);
} }
/** /**
@ -306,30 +341,42 @@ class CodeBlock extends BaseService {
} }
/** /**
* *
* @param {string} compId id * @param {string} compId id
* @param {string} codeId id * @param {string} codeId id
* @param {string[]} codeHooks hook名称 * @param {string[]} codeHooks hook名称
* @returns {boolean} * @returns {boolean}
*/ */
public async unbind(compId: Id, codeId: string, codeHooks: string[]): Promise<boolean> { // public async unbind(compId: Id, codeId: string, codeHooks: string[]): Promise<boolean> {
const nodeInfo = editorService.getNodeById(compId); // const nodeInfo = editorService.getNodeById(compId);
if (!nodeInfo) return false; // if (!nodeInfo) return false;
// 更新node节点信息 // // 更新node节点信息
codeHooks.forEach((hook) => { // codeHooks.forEach((hook) => {
if (!isEmpty(nodeInfo[hook])) { // if (!isEmpty(nodeInfo[hook])) {
const newHookInfo = nodeInfo[hook].filter((item: string) => item !== codeId); // const newHookInfo = nodeInfo[hook].filter((item: string) => item !== codeId);
nodeInfo[hook] = newHookInfo; // nodeInfo[hook] = newHookInfo;
editorService.update(nodeInfo); // editorService.update(nodeInfo);
} // }
}); // });
// 更新绑定关系 // // 更新绑定关系
const oldRelation = await this.getCompRelation(); // const oldRelation = await this.getCompRelation();
const oldCodeIds = oldRelation[compId]; // const oldCodeIds = oldRelation[compId];
// 不能直接splice修改原数组,会导致绑定关系更新错乱 // // 不能直接splice修改原数组,会导致绑定关系更新错乱
const newCodeIds = oldCodeIds.filter((item) => item !== codeId); // const newCodeIds = oldCodeIds.filter((item) => item !== codeId);
this.setCompRelation(compId, newCodeIds); // this.setCompRelation(compId, newCodeIds);
return true; // return true;
// }
/**
* id解除绑定关系
* @param {MNode} compId
* @returns void
*/
public async deleteCompsInRelation(node: MNode) {
const codeDsl = cloneDeep(await this.getCodeDsl());
if (!codeDsl) return;
this.recurseNodes(node, codeDsl);
this.setCodeDsl(codeDsl);
} }
public destroy() { public destroy() {
@ -339,9 +386,22 @@ class CodeBlock extends BaseService {
this.state.editable = true; this.state.editable = true;
this.state.mode = CodeEditorMode.EDITOR; this.state.mode = CodeEditorMode.EDITOR;
this.state.combineIds = []; this.state.combineIds = [];
this.state.compRelation = {};
this.state.undeletableList = []; this.state.undeletableList = [];
} }
// 删除组件时 如果是容器 需要遍历删除其包含节点的绑定信息
private recurseNodes(node: MNode, codeDsl: CodeBlockDSL) {
if (!node.id) return;
forIn(codeDsl, (codeBlockContent) => {
const compsContent = codeBlockContent.comps || {};
codeBlockContent.comps = omit(compsContent, node.id);
});
if (!isEmpty(node.items)) {
node.items.forEach((item: MNode) => {
this.recurseNodes(item, codeDsl);
});
}
}
} }
export type CodeBlockService = CodeBlock; export type CodeBlockService = CodeBlock;

View File

@ -24,6 +24,7 @@ import { NodeType } from '@tmagic/schema';
import StageCore from '@tmagic/stage'; import StageCore from '@tmagic/stage';
import { getNodePath, isNumber, isPage, isPop } from '@tmagic/utils'; import { getNodePath, isNumber, isPage, isPop } from '@tmagic/utils';
import codeBlockService from '../services/codeBlock';
import historyService, { StepValue } from '../services/history'; import historyService, { StepValue } from '../services/history';
import storageService, { Protocol } from '../services/storage'; import storageService, { Protocol } from '../services/storage';
import type { AddMNode, EditorNodeInfo, PastePosition, StoreState } from '../type'; import type { AddMNode, EditorNodeInfo, PastePosition, StoreState } from '../type';
@ -421,6 +422,9 @@ class Editor extends BaseService {
} }
this.addModifiedNodeId(parent.id); this.addModifiedNodeId(parent.id);
// 通知codeBlockService解除绑定关系
codeBlockService.deleteCompsInRelation(node);
} }
/** /**

View File

@ -317,6 +317,10 @@ export interface CodeBlockContent {
name: string; name: string;
/** 代码块内容 */ /** 代码块内容 */
content: string; content: string;
/** 代码块与组件的绑定关系 */
comps?: CodeRelation;
/** 扩展字段 */
[propName: string]: any;
} }
export type CodeState = { export type CodeState = {
@ -332,12 +336,15 @@ export type CodeState = {
mode: CodeEditorMode; mode: CodeEditorMode;
/** list模式下左侧展示的代码列表 */ /** list模式下左侧展示的代码列表 */
combineIds: string[]; combineIds: string[];
/** 组件和代码块的绑定关系 */
compRelation: CompRelation;
/** 为业务逻辑预留的不可删除的代码块列表,由业务逻辑维护(如代码块上线后不可删除) */ /** 为业务逻辑预留的不可删除的代码块列表,由业务逻辑维护(如代码块上线后不可删除) */
undeletableList: string[]; undeletableList: string[];
}; };
export type CodeRelation = {
/** 组件id:['created'] */
[compId: string | number]: string[];
};
export enum CodeEditorMode { export enum CodeEditorMode {
/** 左侧菜单,右侧代码 */ /** 左侧菜单,右侧代码 */
LIST = 'list', LIST = 'list',
@ -345,11 +352,6 @@ export enum CodeEditorMode {
EDITOR = 'editor', EDITOR = 'editor',
} }
export type CompRelation = {
/** 代码块绑定关系组件id-代码块id数组 */
[compId: Id]: string[];
};
export interface CodeDslList { export interface CodeDslList {
/** 代码块id */ /** 代码块id */
id: string; id: string;
@ -364,6 +366,9 @@ export interface CodeDslList {
export interface ListState { export interface ListState {
/** 代码块列表 */ /** 代码块列表 */
codeList: CodeDslList[]; codeList: CodeDslList[];
}
export interface ListRelationState extends ListState {
/** 与代码块绑定的组件id信息 */ /** 与代码块绑定的组件id信息 */
bindComps: { bindComps: {
/** 代码块id : 组件信息 */ /** 代码块id : 组件信息 */
@ -377,3 +382,12 @@ export enum CodeDeleteErrorType {
/** 代码块存在绑定关系 */ /** 代码块存在绑定关系 */
BIND = 'bind', BIND = 'bind',
} }
export enum CodeSelectOp {
/** 增加 */
ADD = 'add',
/** 删除 */
DELETE = 'delete',
/** 单选修改 */
CHANGE = 'change',
}

View File

@ -25,11 +25,18 @@ export default {
name: 'getData', name: 'getData',
// eslint-disable-next-line no-eval // eslint-disable-next-line no-eval
content: eval(`(vm) => {\n console.log("this is getData function")\n}`), content: eval(`(vm) => {\n console.log("this is getData function")\n}`),
comps: {
page_299: ['mounted', 'created'],
},
}, },
code_5316: { code_5316: {
name: 'getList', name: 'getList',
// eslint-disable-next-line no-eval // eslint-disable-next-line no-eval
content: eval(`(vm) => {\n console.log("this is getList function")\n}`), content: eval(`(vm) => {\n console.log("this is getList function")\n}`),
comps: {
text_9027: ['created'],
page_299: ['created'],
},
}, },
}, },
items: [ items: [
@ -56,7 +63,7 @@ export default {
fontWeight: '', fontWeight: '',
}, },
events: [], events: [],
created: ['code_5316'], created: ['code_5316', 'code_5336'],
mounted: ['code_5336'], mounted: ['code_5336'],
items: [ items: [
{ {