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">
import { defineComponent, onUnmounted, PropType, provide, reactive, toRaw, watch } from 'vue';
import { isEmpty,union } from 'lodash-es';
import { EventOption } from '@tmagic/core';
import type { FormConfig } from '@tmagic/form';
@ -210,12 +209,6 @@ export default defineComponent({
updateDragEl: {
type: Function as PropType<(el: HTMLDivElement, target: HTMLElement) => void>,
},
/** 可挂载代码块的生命周期 */
codeHooks: {
type: Array<string>,
default: () => ['created', 'mounted'],
},
},
emits: ['props-panel-mounted', 'update:modelValue'],
@ -233,38 +226,11 @@ export default defineComponent({
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(
() => props.modelValue,
(modelValue) => {
editorService.set('root', modelValue);
//
initCodeRelation(modelValue);
},
{
immediate: true,
@ -370,7 +336,6 @@ export default defineComponent({
containerHighlightType: props.containerHighlightType,
}),
);
provide('codeHooks',props.codeHooks)
return services;
},

View File

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

View File

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

View File

@ -17,14 +17,14 @@
*/
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 type { CodeBlockContent, CodeBlockDSL, CodeState, CompRelation } from '../type';
import { CodeEditorMode } from '../type';
import { info } from '../utils/logger';
import type { CodeBlockContent, CodeBlockDSL, CodeState } from '../type';
import { CodeEditorMode, CodeSelectOp } from '../type';
import { error, info } from '../utils/logger';
import BaseService from './BaseService';
@ -36,7 +36,6 @@ class CodeBlock extends BaseService {
editable: true,
mode: CodeEditorMode.EDITOR,
combineIds: [],
compRelation: {},
undeletableList: [],
});
@ -52,7 +51,6 @@ class CodeBlock extends BaseService {
'setEditStatus',
'setMode',
'setCombineIds',
'setCompRelation',
'setUndeleteableList',
'deleteCodeDslByIds',
]);
@ -102,9 +100,14 @@ class CodeBlock extends BaseService {
*/
public async setCodeDslById(id: string, codeConfig: CodeBlockContent): Promise<void> {
let codeDsl = await this.getCodeDsl();
if (!codeDsl) return;
const existContent = codeDsl[id] || {};
codeDsl = {
...codeDsl,
[id]: codeConfig,
[id]: {
...existContent,
...codeConfig,
},
};
await this.setCodeDsl(codeDsl);
}
@ -226,41 +229,73 @@ class CodeBlock extends BaseService {
}
/**
*
* @param {number | string} compId id
* @param {string[]} codeIds id数组
*
* @param {Id} id
* @param {string[]} diffCodeIds id数组
* @param {CodeSelectOp} opFlag
* @param {string} hook hook名称
* @returns {void}
*/
public async setCompRelation(compId: number | string, codeIds: string[]) {
if (!compId) return;
this.state.compRelation = {
...this.state.compRelation,
[compId]: codeIds,
};
}
/**
*
* @returns {CompRelation}
*/
public getCompRelation(): CompRelation {
return this.state.compRelation;
}
/**
*
* @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);
public async setCombineRelation(compId: Id, diffCodeIds: string[], opFlag: CodeSelectOp, hook: string) {
const codeDsl = cloneDeep(await this.getCodeDsl());
if (!codeDsl) return;
if (opFlag === CodeSelectOp.DELETE) {
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);
}
});
} catch (e) {
error(e);
throw new Error('解绑代码块失败');
}
});
return codeRelation;
} else if (opFlag === CodeSelectOp.ADD) {
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} codeId id
* @param {string[]} codeHooks hook名称
* @returns {boolean}
*/
public async unbind(compId: Id, codeId: string, codeHooks: string[]): Promise<boolean> {
const nodeInfo = editorService.getNodeById(compId);
if (!nodeInfo) return false;
// 更新node节点信息
codeHooks.forEach((hook) => {
if (!isEmpty(nodeInfo[hook])) {
const newHookInfo = nodeInfo[hook].filter((item: string) => item !== codeId);
nodeInfo[hook] = newHookInfo;
editorService.update(nodeInfo);
}
});
// 更新绑定关系
const oldRelation = await this.getCompRelation();
const oldCodeIds = oldRelation[compId];
// 不能直接splice修改原数组,会导致绑定关系更新错乱
const newCodeIds = oldCodeIds.filter((item) => item !== codeId);
this.setCompRelation(compId, newCodeIds);
return true;
// public async unbind(compId: Id, codeId: string, codeHooks: string[]): Promise<boolean> {
// const nodeInfo = editorService.getNodeById(compId);
// if (!nodeInfo) return false;
// // 更新node节点信息
// codeHooks.forEach((hook) => {
// if (!isEmpty(nodeInfo[hook])) {
// const newHookInfo = nodeInfo[hook].filter((item: string) => item !== codeId);
// nodeInfo[hook] = newHookInfo;
// editorService.update(nodeInfo);
// }
// });
// // 更新绑定关系
// const oldRelation = await this.getCompRelation();
// const oldCodeIds = oldRelation[compId];
// // 不能直接splice修改原数组,会导致绑定关系更新错乱
// const newCodeIds = oldCodeIds.filter((item) => item !== codeId);
// this.setCompRelation(compId, newCodeIds);
// 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() {
@ -339,9 +386,22 @@ class CodeBlock extends BaseService {
this.state.editable = true;
this.state.mode = CodeEditorMode.EDITOR;
this.state.combineIds = [];
this.state.compRelation = {};
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;

View File

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

View File

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

View File

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