feat(editor): 支持代码块维度查看与组件的绑定关系,并支持从代码块列表解除绑定

This commit is contained in:
parisma 2022-09-21 12:53:02 +08:00 committed by jia000
parent 5de3eeda71
commit bfaa8317e3
9 changed files with 278 additions and 48 deletions

View File

@ -65,7 +65,7 @@
<script lang="ts">
import { defineComponent, onUnmounted, PropType, provide, reactive, toRaw, watch } from 'vue';
import { isEmpty } from 'lodash-es';
import { isEmpty,union } from 'lodash-es';
import { EventOption } from '@tmagic/core';
import type { FormConfig } from '@tmagic/form';
@ -210,6 +210,12 @@ 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'],
@ -230,8 +236,21 @@ export default defineComponent({
const initCodeRelation = (rootValue: MNode) => {
if (isEmpty(rootValue.items)) return;
rootValue.items.forEach((nodeValue: MNode) => {
if (!isEmpty(nodeValue.created)) {
codeBlockService.setCompRelation(nodeValue.id, nodeValue.created);
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);
@ -351,6 +370,7 @@ export default defineComponent({
containerHighlightType: props.containerHighlightType,
}),
);
provide('codeHooks',props.codeHooks)
return services;
},

View File

@ -35,13 +35,14 @@
<script lang="ts" setup>
import { computed, defineEmits, defineProps, inject, ref, watchEffect } from 'vue';
import { ElMessage } from 'element-plus';
import { map } from 'lodash-es';
import { map, union } from 'lodash-es';
import { SelectConfig } from '@tmagic/form';
import type { Services } from '../type';
import { EditorMode } from '../type';
import { CodeEditorMode } from '../type';
const services = inject<Services>('services');
const codeHooks = inject<string[]>('codeHooks');
const emit = defineEmits(['change']);
@ -79,7 +80,6 @@ const fieldKey = ref('');
const combineIds = ref<string[]>([]);
watchEffect(async () => {
if (!combineIds.value) return;
const combineNames = await Promise.all(
combineIds.value.map(async (id) => {
const { name = '' } = (await services?.codeBlockService.getCodeContentById(id)) || {};
@ -90,20 +90,25 @@ watchEffect(async () => {
});
const changeHandler = async (value: any) => {
await setCombineRelation(value);
await setCombineRelation();
emit('change', value);
};
//
const setCombineRelation = async (selectedIds: string[] | string) => {
if (typeof selectedIds === 'string') {
// select
combineIds.value = [selectedIds];
} else {
combineIds.value = selectedIds;
}
const setCombineRelation = async () => {
//
combineIds.value = [];
// 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);
//
@ -115,8 +120,8 @@ const viewHandler = async () => {
ElMessage.error('请先绑定代码块');
return;
}
await setCombineRelation(props.model[props.name]);
await services?.codeBlockService.setMode(EditorMode.LIST);
await setCombineRelation();
await services?.codeBlockService.setMode(CodeEditorMode.LIST);
services?.codeBlockService.setCodeEditorContent(true, combineIds.value[0]);
};
</script>

View File

@ -9,7 +9,7 @@
>
<layout v-model:left="left" :min-left="45" class="code-editor-layout">
<!-- 左侧列表 -->
<template #left v-if="mode === EditorMode.LIST">
<template #left v-if="mode === CodeEditorMode.LIST">
<el-tree
v-if="!isEmpty(state.codeList)"
ref="tree"
@ -35,7 +35,7 @@
<div
v-if="!isEmpty(codeConfig)"
:class="[
mode === EditorMode.LIST
mode === CodeEditorMode.LIST
? 'm-editor-code-block-editor-panel-list-mode'
: 'm-editor-code-block-editor-panel',
]"
@ -82,7 +82,7 @@ import { ElMessage } from 'element-plus';
import { forIn, isEmpty } from 'lodash-es';
import type { CodeBlockContent, CodeDslList, ListState, Services } from '../../../type';
import { EditorMode } from '../../../type';
import { CodeEditorMode } from '../../../type';
import MagicCodeEditor from '../../CodeEditor.vue';
import Layout from '../../Layout.vue';
@ -132,7 +132,7 @@ const saveCode = async (): Promise<boolean> => {
try {
//
const codeContent = codeEditor.value.getEditor().getValue();
const codeContent = codeEditor.value.getEditor()?.getValue();
/* eslint no-eval: "off" */
codeConfig.value.content = eval(codeContent);
} catch (e: any) {

View File

@ -25,21 +25,51 @@
:data="state.codeList"
:highlight-current="true"
:filter-node-method="filterNode"
@node-click="toggleCombineRelation"
>
<template #default="{ data }">
<div :id="data.id" class="list-container">
<div class="list-item">
<div class="code-name">{{ data.name }}{{ data.id }}</div>
<!-- 右侧工具栏 -->
<div class="right-tool">
<el-tooltip effect="dark" :content="editable ? '编辑' : '查看'" placement="bottom">
<Icon :icon="editable ? Edit : View" class="edit-icon" @click="editCode(`${data.id}`)"></Icon>
<Icon :icon="editable ? Edit : View" class="edit-icon" @click.stop="editCode(`${data.id}`)"></Icon>
</el-tooltip>
<el-tooltip
effect="dark"
content="查看绑定关系"
placement="bottom"
v-if="state.bindComps[data.id] && state.bindComps[data.id].length > 0"
>
<Icon :icon="Link" class="edit-icon" @click.stop="toggleCombineRelation(data)"></Icon>
</el-tooltip>
<el-tooltip effect="dark" content="删除" placement="bottom" v-if="editable">
<Icon :icon="Close" class="edit-icon" @click="deleteCode(`${data.id}`)"></Icon>
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.id}`)"></Icon>
</el-tooltip>
<slot name="code-block-panel-tool" :id="data.id"></slot>
</div>
</div>
<!-- 展示代码块下绑定的组件 -->
<div
class="code-comp-map-wrapper"
v-if="data.showRelation && state.bindComps[data.id] && state.bindComps[data.id].length > 0"
>
<svg class="arrow-left" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa="">
<path
fill="currentColor"
d="M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"
></path>
</svg>
<el-button
v-for="(comp, index) in state.bindComps[data.id]"
:key="index"
class="code-comp"
size="small"
:plain="true"
>{{ comp.name }}<Icon :icon="Close" class="comp-delete-icon" @click.stop="unbind(comp.id, data.id)"></Icon
></el-button>
</div>
</div>
</template>
</el-tree>
@ -54,41 +84,104 @@
</template>
<script lang="ts" setup>
import { computed, inject, reactive, ref, watchEffect } from 'vue';
import { Close, Edit, View } from '@element-plus/icons-vue';
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 } from 'lodash-es';
import { flattenDeep, forIn, isEmpty, values, xor } from 'lodash-es';
import { Id } from '@tmagic/schema';
import Icon from '../../../components/Icon.vue';
import type { CodeBlockContent, Services } from '../../../type';
import { CodeDslList, EditorMode, ErrorType, ListState } from '../../../type';
import { CodeDeleteErrorType, CodeDslList, CodeEditorMode, ListState } from '../../../type';
import codeBlockEditor from './CodeBlockEditor.vue';
const props = defineProps<{
customError?: (id: string, errorType: ErrorType) => any;
customError?: (id: string, errorType: CodeDeleteErrorType) => any;
}>();
const services = inject<Services>('services');
const codeHooks = inject<string[]>('codeHooks') || [];
//
const state = reactive<ListState>({
codeList: [],
bindComps: {},
});
const editable = computed(() => services?.codeBlockService.getEditStatus());
watchEffect(async () => {
// ID
const getBindCompsByCodeId = (codeId: string) => {
const bindCompIds = services?.codeBlockService.getCodeRelationById(codeId) || [];
if (isEmpty(bindCompIds)) {
state.bindComps[codeId] = [];
return;
}
const compsInfo = bindCompIds.map((compId) => ({
id: compId,
name: getCompName(compId),
}));
state.bindComps[codeId] = compsInfo;
};
//
const initList = async () => {
const codeDsl = (await services?.codeBlockService.getCodeDsl()) || null;
if (!codeDsl) return;
state.codeList = [];
forIn(codeDsl, (value: CodeBlockContent, key: string) => {
forIn(codeDsl, (value: CodeBlockContent, codeId: string) => {
getBindCompsByCodeId(codeId);
state.codeList.push({
id: key,
id: codeId,
name: value.name,
content: value.content,
showRelation: false,
});
});
});
};
watch(
() => services?.codeBlockService.getCodeDsl(),
() => {
initList();
},
{
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));
});
},
);
//
watch(
() => services?.editorService.get('node'),
(curNode) => {
if (!curNode?.id) return;
forIn(state.bindComps, (bindCompInfo) => {
bindCompInfo.forEach((comp) => {
if (comp.id === curNode.id) {
comp.name = curNode.name;
}
});
});
},
);
//
const createCodeBlock = async () => {
@ -101,7 +194,7 @@ const createCodeBlock = async () => {
name: '代码块',
content: `() => {\n // place your code here\n}`,
};
await codeBlockService.setMode(EditorMode.EDITOR);
await codeBlockService.setMode(CodeEditorMode.EDITOR);
const id = await codeBlockService.getUniqueId();
await codeBlockService.setCodeDslById(id, codeConfig);
codeBlockService.setCodeEditorContent(true, id);
@ -109,7 +202,7 @@ const createCodeBlock = async () => {
//
const editCode = async (key: string) => {
await services?.codeBlockService.setMode(EditorMode.EDITOR);
await services?.codeBlockService.setMode(CodeEditorMode.EDITOR);
services?.codeBlockService.setCodeEditorContent(true, key);
};
@ -123,7 +216,7 @@ const deleteCode = (key: string) => {
services?.codeBlockService.deleteCodeDslByIds([key]);
} else {
if (typeof props.customError === 'function') {
props.customError(key, codeIds.includes(key) ? ErrorType.BIND : ErrorType.UNDELETEABLE);
props.customError(key, codeIds.includes(key) ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
} else {
ElMessage.error('代码块删除失败');
}
@ -143,4 +236,28 @@ const filterNode = (value: string, data: CodeDslList): boolean => {
const filterTextChangeHandler = (val: string) => {
tree.value?.filter(val);
};
// /
const toggleCombineRelation = (data: CodeDslList) => {
const { id } = data;
const currentCode = state.codeList.find((item) => item.id === id);
if (!currentCode) return;
currentCode.showRelation = !currentCode?.showRelation;
};
// tag
const getCompName = (compId: Id): string => {
const node = services?.editorService.getNodeById(compId);
return node?.name || String(compId);
};
//
const unbind = async (compId: Id, codeId: string) => {
const res = await services?.codeBlockService.unbind(compId, codeId, codeHooks);
if (res) {
ElMessage.success('绑定关系解除成功');
} else {
ElMessage.error('绑定关系解除失败');
}
};
</script>

View File

@ -17,11 +17,13 @@
*/
import { reactive } from 'vue';
import { keys, omit, pick } from 'lodash-es';
import { forIn, isEmpty, keys, omit, pick } from 'lodash-es';
import { Id } from '@tmagic/schema';
import editorService from '../services/editor';
import type { CodeBlockContent, CodeBlockDSL, CodeState, CompRelation } from '../type';
import { EditorMode } from '../type';
import { CodeEditorMode } from '../type';
import { info } from '../utils/logger';
import BaseService from './BaseService';
@ -32,7 +34,7 @@ class CodeBlock extends BaseService {
codeDsl: null,
id: '',
editable: true,
mode: EditorMode.EDITOR,
mode: CodeEditorMode.EDITOR,
combineIds: [],
compRelation: {},
undeletableList: [],
@ -191,18 +193,18 @@ class CodeBlock extends BaseService {
/**
*
* @returns {EditorMode}
* @returns {CodeEditorMode}
*/
public getMode(): EditorMode {
public getMode(): CodeEditorMode {
return this.state.mode;
}
/**
*
* @param {EditorMode} mode
* @param {CodeEditorMode} mode
* @returns {void}
*/
public async setMode(mode: EditorMode): Promise<void> {
public async setMode(mode: CodeEditorMode): Promise<void> {
this.state.mode = mode;
}
@ -232,6 +234,7 @@ class CodeBlock extends BaseService {
public async setCompRelation(compId: number | string, codeIds: string[]) {
if (!compId) return;
this.state.compRelation = {
...this.state.compRelation,
[compId]: codeIds,
};
}
@ -244,6 +247,22 @@ class CodeBlock extends BaseService {
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);
}
});
return codeRelation;
}
/**
*
* @returns {string[]}
@ -286,12 +305,39 @@ class CodeBlock extends BaseService {
return await this.getUniqueId();
}
/**
*
* @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 destroy() {
this.state.isShowCodeEditor = false;
this.state.codeDsl = null;
this.state.id = '';
this.state.editable = true;
this.state.mode = EditorMode.EDITOR;
this.state.mode = CodeEditorMode.EDITOR;
this.state.combineIds = [];
this.state.compRelation = {};
this.state.undeletableList = [];

View File

@ -1,5 +1,8 @@
.m-editor-code-block-list {
margin-top: 5px;
.el-tree-node__content {
height: auto;
}
.code-header-wrapper {
display: flex;
align-items: center;
@ -47,6 +50,25 @@
margin-right: 60px;
}
}
.code-comp-map-wrapper {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-left: 20px;
margin-bottom: 5px;
.arrow-left {
transform: rotate(-45deg);
width: 20px;
height: 20px;
}
.code-comp {
margin-left: 5px;
padding: 5px;
.comp-delete-icon {
margin-left: 3px;
}
}
}
}
}

View File

@ -329,7 +329,7 @@ export type CodeState = {
/** 代码块是否可编辑 */
editable: boolean;
/** 代码编辑面板的展示模式 */
mode: EditorMode;
mode: CodeEditorMode;
/** list模式下左侧展示的代码列表 */
combineIds: string[];
/** 组件和代码块的绑定关系 */
@ -338,7 +338,7 @@ export type CodeState = {
undeletableList: string[];
};
export enum EditorMode {
export enum CodeEditorMode {
/** 左侧菜单,右侧代码 */
LIST = 'list',
/** 全屏代码 */
@ -347,7 +347,7 @@ export enum EditorMode {
export type CompRelation = {
/** 代码块绑定关系组件id-代码块id数组 */
[compId: string | number]: string[];
[compId: Id]: string[];
};
export interface CodeDslList {
@ -357,13 +357,21 @@ export interface CodeDslList {
name: string;
/** 代码块函数内容 */
content: any;
/** 是否展示代码绑定关系 */
showRelation?: boolean;
}
export interface ListState {
/** 代码块列表 */
codeList: CodeDslList[];
/** 与代码块绑定的组件id信息 */
bindComps: {
/** 代码块id : 组件信息 */
[id: string]: MNode[];
};
}
export enum ErrorType {
export enum CodeDeleteErrorType {
/** 代码块存在于不可删除列表中 */
UNDELETEABLE = 'undeleteable',
/** 代码块存在绑定关系 */

View File

@ -229,6 +229,12 @@ export const fillConfig = (config: FormConfig = []) => [
type: 'code-select',
labelWidth: '100px',
},
{
name: 'mounted',
text: 'mounted',
type: 'code-select',
labelWidth: '100px',
},
],
},
],

View File

@ -26,6 +26,11 @@ export default {
// eslint-disable-next-line no-eval
content: eval(`(vm) => {\n console.log("this is getData function")\n}`),
},
code_5316: {
name: 'getList',
// eslint-disable-next-line no-eval
content: eval(`(vm) => {\n console.log("this is getList function")\n}`),
},
},
items: [
{
@ -51,7 +56,8 @@ export default {
fontWeight: '',
},
events: [],
created: ['code_5336'],
created: ['code_5316'],
mounted: ['code_5336'],
items: [
{
type: 'text',
@ -76,7 +82,7 @@ export default {
text: 'Tmagic editor 营销活动编辑器',
multiple: true,
events: [],
created: [],
created: ['code_5316'],
},
{
type: 'qrcode',