feat(editor,form,schema): 优化代码

Squash merge branch 'feature/parisma_881986193' into 'master'
1、扩展参数配置能力,支持参数类型定义,支持参数注释
2、修复代码块嵌套多层时绑定关系展示不正确的问题
3、支持在组件绑定位置编辑查看代码块
This commit is contained in:
parisma 2023-02-09 14:19:18 +08:00 committed by roymondchen
parent e42aee84bc
commit 0b537f5bff
11 changed files with 125 additions and 109 deletions

View File

@ -12,7 +12,7 @@
<TMagicButton type="primary" class="button" @click="toggleFullScreen"> <TMagicButton type="primary" class="button" @click="toggleFullScreen">
{{ isFullScreen ? '退出全屏' : '全屏' }}</TMagicButton {{ isFullScreen ? '退出全屏' : '全屏' }}</TMagicButton
> >
<TMagicButton type="primary" class="button" @click="saveCode">保存</TMagicButton> <TMagicButton type="primary" class="button" @click="saveAndClose">确认</TMagicButton>
<TMagicButton type="primary" class="button" @click="close">关闭</TMagicButton> <TMagicButton type="primary" class="button" @click="close">关闭</TMagicButton>
</div> </div>
<div class="m-editor-content-bottom" v-else> <div class="m-editor-content-bottom" v-else>
@ -54,7 +54,7 @@ const props = withDefaults(
autoSaveDraft: true, autoSaveDraft: true,
}, },
); );
const emit = defineEmits(['save', 'close', 'saveAndClose']); const emit = defineEmits(['close', 'saveAndClose']);
const services = inject<Services>('services'); const services = inject<Services>('services');
@ -95,14 +95,6 @@ const saveCodeDraft = async (codeValue: string) => {
tMagicMessage.success(`代码草稿保存成功 ${datetimeFormatter(new Date())}`); tMagicMessage.success(`代码草稿保存成功 ${datetimeFormatter(new Date())}`);
}; };
//
const saveCode = (): void => {
if (!codeEditor.value || !props.editable) return;
//
editorContent.value = (codeEditor.value.getEditor() as monaco.editor.IStandaloneCodeEditor)?.getValue();
emit('save', editorContent.value);
};
// //
const saveAndClose = (): void => { const saveAndClose = (): void => {
if (!codeEditor.value || !props.editable) return; if (!codeEditor.value || !props.editable) return;

View File

@ -6,9 +6,9 @@
<TMagicInput class="code-name-input" v-model="codeName" :disabled="!editable" /> <TMagicInput class="code-name-input" v-model="codeName" :disabled="!editable" />
</div> </div>
<div class="code-name-wrapper"> <div class="code-name-wrapper">
<div class="code-name-label">参数定义</div> <div class="code-name-label">参数</div>
<m-form-table <m-form-table
style="width: 320px" style="width: 800px"
:config="tableConfig" :config="tableConfig"
:model="tableModel" :model="tableModel"
:enableToggleMode="false" :enableToggleMode="false"
@ -26,7 +26,6 @@
:autoSaveDraft="autoSaveDraft" :autoSaveDraft="autoSaveDraft"
:codeOptions="codeOptions" :codeOptions="codeOptions"
language="javascript" language="javascript"
@save="saveCode"
@saveAndClose="saveAndClose" @saveAndClose="saveAndClose"
@close="close" @close="close"
></CodeDraftEditor> ></CodeDraftEditor>
@ -34,36 +33,56 @@
</template> </template>
<script lang="ts" setup name="MEditorFunctionEditor"> <script lang="ts" setup name="MEditorFunctionEditor">
import { inject, provide, ref, watchEffect } from 'vue'; import { inject, provide, ref, watchEffect } from 'vue';
import { cloneDeep } from 'lodash-es';
import { TMagicCard, TMagicInput, tMagicMessage } from '@tmagic/design'; import { TMagicCard, TMagicInput, tMagicMessage } from '@tmagic/design';
import { ColumnConfig, TableConfig } from '@tmagic/form';
import { CodeParam, Id } from '@tmagic/schema'; import { CodeParam, Id } from '@tmagic/schema';
import type { Services } from '../type'; import type { Services } from '../type';
import CodeDraftEditor from './CodeDraftEditor.vue'; import CodeDraftEditor from './CodeDraftEditor.vue';
const defaultParamColConfig: ColumnConfig = {
const tableConfig = { type: 'row',
border: true, label: '参数类型',
enableFullscreen: false,
name: 'params',
maxHeight: '150px',
items: [ items: [
{ {
type: 'text', text: '参数类型',
label: '参数名', labelWidth: '70px',
name: 'name', type: 'select',
name: 'type',
options: [
{
text: '数字',
label: '数字',
value: 'number',
},
{
text: '字符串',
label: '字符串',
value: 'text',
},
],
}, },
], ],
}; };
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
/** 代码块id */
id: Id; id: Id;
/** 代码块名称 */
name: string; name: string;
/** 代码内容 */
content: string; content: string;
/** 是否可编辑 */
editable?: boolean; editable?: boolean;
/** 是否自动保存草稿 */
autoSaveDraft?: boolean; autoSaveDraft?: boolean;
/** 编辑器扩展参数 */
codeOptions?: object; codeOptions?: object;
/** 代码参数扩展配置 */
paramsColConfig?: ColumnConfig;
}>(), }>(),
{ {
editable: true, editable: true,
@ -71,6 +90,32 @@ const props = withDefaults(
}, },
); );
const paramsColConfig = props.paramsColConfig || defaultParamColConfig;
const tableConfig: TableConfig = {
type: 'table',
border: true,
enableFullscreen: false,
name: 'params',
maxHeight: '300px',
dropSort: false,
items: [
{
type: 'text',
label: '参数名',
name: 'name',
width: 200,
},
{
type: 'text',
label: '参数注释',
name: 'tip',
width: 200,
},
paramsColConfig,
],
};
const emit = defineEmits(['change', 'field-input']); const emit = defineEmits(['change', 'field-input']);
const services = inject<Services>('services'); const services = inject<Services>('services');
@ -91,7 +136,7 @@ watchEffect(() => {
}); });
const initTableModel = (): void => { const initTableModel = (): void => {
const codeDsl = services?.codeBlockService.getCodeDslSync(); const codeDsl = cloneDeep(services?.codeBlockService.getCodeDslSync());
if (!codeDsl) return; if (!codeDsl) return;
tableModel.value = { tableModel.value = {
params: codeDsl[props.id]?.params || [], params: codeDsl[props.id]?.params || [],

View File

@ -9,17 +9,27 @@
:size="size" :size="size"
@change="changeHandler" @change="changeHandler"
> >
<template #operateCol="{ scope }">
<Icon
v-if="scope.row.codeId"
:icon="editable ? Edit : View"
class="edit-icon"
@click="editCode(scope.row.codeId)"
></Icon>
</template>
</m-form-table> </m-form-table>
</div> </div>
</template> </template>
<script lang="ts" setup name="MEditorCodeSelect"> <script lang="ts" setup name="MEditorCodeSelect">
import { computed, defineEmits, defineProps, inject, watch } from 'vue'; import { computed, defineEmits, defineProps, inject, watch } from 'vue';
import { Edit, View } from '@element-plus/icons-vue';
import { isEmpty, map } from 'lodash-es'; import { isEmpty, map } from 'lodash-es';
import { createValues, FormItem, FormState, TableConfig } from '@tmagic/form'; import { createValues, FormItem, FormState, TableConfig } from '@tmagic/form';
import { HookType, Id } from '@tmagic/schema'; import { HookType, Id } from '@tmagic/schema';
import Icon from '../components/Icon.vue';
import { CodeParamStatement, HookData, Services } from '../type'; import { CodeParamStatement, HookData, Services } from '../type';
const services = inject<Services>('services'); const services = inject<Services>('services');
const mForm = inject<FormState>('mForm'); const mForm = inject<FormState>('mForm');
@ -38,9 +48,10 @@ const codeDsl = computed(() => services?.codeBlockService.getCodeDslSync());
const tableConfig = computed<FormItem>(() => { const tableConfig = computed<FormItem>(() => {
const defaultConfig = { const defaultConfig = {
dropSort: true, dropSort: false,
enableFullscreen: false, enableFullscreen: false,
border: true, border: true,
operateColWidth: 60,
items: [ items: [
{ {
type: 'select', type: 'select',
@ -68,8 +79,8 @@ const tableConfig = computed<FormItem>(() => {
defaultValue: {}, defaultValue: {},
itemsFunction: (row: HookData) => { itemsFunction: (row: HookData) => {
const paramsConfig = getParamsConfig(row.codeId); const paramsConfig = getParamsConfig(row.codeId);
if (!row.params) row.params = {}; // 使createValues
if (isEmpty(row.params)) { if (isEmpty(row.params) || !row.params) {
createValues(mForm, paramsConfig, {}, row.params); createValues(mForm, paramsConfig, {}, row.params);
} }
return paramsConfig; return paramsConfig;
@ -83,6 +94,8 @@ const tableConfig = computed<FormItem>(() => {
}; };
}); });
const editable = computed(() => services?.codeBlockService.getEditStatus());
watch( watch(
() => props.model[props.name], () => props.model[props.name],
(value) => { (value) => {
@ -104,15 +117,18 @@ const changeHandler = async () => {
emit('change', props.model[props.name]); emit('change', props.model[props.name]);
}; };
const getParamsConfig = (codeId: Id) => { const getParamsConfig = (codeId: Id): CodeParamStatement[] => {
if (!codeDsl.value) return []; if (!codeDsl.value) return [];
const paramStatements = codeDsl.value[codeId]?.params; const paramStatements = codeDsl.value[codeId]?.params;
if (isEmpty(paramStatements)) return []; if (isEmpty(paramStatements)) return [];
return paramStatements.map((paramState: CodeParamStatement) => ({ return paramStatements.map((paramState: CodeParamStatement) => ({
name: paramState.name,
text: paramState.name,
labelWidth: '100px', labelWidth: '100px',
type: 'text', text: paramState.name,
...paramState,
})); }));
}; };
const editCode = (codeId: Id) => {
services?.codeBlockService.setCodeEditorContent(true, codeId);
};
</script> </script>

View File

@ -9,45 +9,18 @@
:show-close="false" :show-close="false"
> >
<Layout v-model:left="left" :min-left="45" class="code-editor-layout"> <Layout v-model:left="left" :min-left="45" class="code-editor-layout">
<!-- 左侧列表 -->
<template #left v-if="mode === CodeEditorMode.LIST">
<TMagicTree
v-if="!isEmpty(state.codeList)"
class="side-tree"
node-key="id"
empty-text="暂无代码块"
:data="state.codeList"
:highlight-current="true"
:current-node-key="state.codeList[0].id"
@node-click="selectHandler"
>
<template #default="{ data }">
<div :id="data.id" class="list-container">
<div class="list-item">
<div class="code-name">{{ data.name }}{{ data.id }}</div>
</div>
</div>
</template>
</TMagicTree>
</template>
<!-- 右侧区域 --> <!-- 右侧区域 -->
<template #center> <template #center>
<div <div v-if="!isEmpty(codeConfig)" class="m-editor-code-block-editor-panel">
v-if="!isEmpty(codeConfig)"
:class="[
mode === CodeEditorMode.LIST
? 'm-editor-code-block-editor-panel-list-mode'
: 'm-editor-code-block-editor-panel',
]"
>
<slot name="code-block-edit-panel-header" :id="id"></slot> <slot name="code-block-edit-panel-header" :id="id"></slot>
<FunctionEditor <FunctionEditor
v-if="codeConfig" v-if="codeConfig"
:id="id" :id="id"
:name="codeConfig.name" :name="codeConfig.name"
:content="codeConfig.content" :content="codeConfig.content"
:paramsColConfig="paramsColConfig"
:editable="!!editable" :editable="!!editable"
:autoSaveDraft="mode === CodeEditorMode.EDITOR" :autoSaveDraft="true"
:codeOptions="codeOptions" :codeOptions="codeOptions"
></FunctionEditor> ></FunctionEditor>
</div> </div>
@ -60,18 +33,22 @@
import { computed, inject, reactive, ref, watchEffect } from 'vue'; import { computed, inject, reactive, ref, watchEffect } from 'vue';
import { cloneDeep, forIn, isEmpty } from 'lodash-es'; import { cloneDeep, forIn, isEmpty } from 'lodash-es';
import { TMagicDialog, TMagicTree } from '@tmagic/design'; import { TMagicDialog } from '@tmagic/design';
import { ColumnConfig } from '@tmagic/form';
import { CodeBlockContent } from '@tmagic/schema'; import { CodeBlockContent } from '@tmagic/schema';
import FunctionEditor from '../../../components/FunctionEditor.vue'; import FunctionEditor from '../../../components/FunctionEditor.vue';
import Layout from '../../../components/Layout.vue'; import Layout from '../../../components/Layout.vue';
import type { CodeDslItem, ListState, Services } from '../../../type'; import type { ListState, Services } from '../../../type';
import { CodeEditorMode } from '../../../type';
import { serializeConfig } from '../../../utils/editor'; import { serializeConfig } from '../../../utils/editor';
const services = inject<Services>('services'); const services = inject<Services>('services');
const codeOptions = inject('codeOptions', {}); const codeOptions = inject('codeOptions', {});
defineProps<{
paramsColConfig?: ColumnConfig;
}>();
const left = ref(200); const left = ref(200);
const currentTitle = ref(''); const currentTitle = ref('');
// //
@ -81,7 +58,6 @@ const state = reactive<ListState>({
codeList: [], codeList: [],
}); });
const mode = computed(() => services?.codeBlockService.getMode());
const id = computed(() => services?.codeBlockService.getId() || ''); const id = computed(() => services?.codeBlockService.getId() || '');
const editable = computed(() => services?.codeBlockService.getEditStatus()); const editable = computed(() => services?.codeBlockService.getEditStatus());
// id // id
@ -105,9 +81,4 @@ watchEffect(async () => {
}); });
currentTitle.value = state.codeList[0]?.name || ''; currentTitle.value = state.codeList[0]?.name || '';
}); });
const selectHandler = (data: CodeDslItem) => {
services?.codeBlockService.setId(data.id);
currentTitle.value = data.name;
};
</script> </script>

View File

@ -78,7 +78,7 @@
</TMagicScrollbar> </TMagicScrollbar>
<!-- 代码块编辑区 --> <!-- 代码块编辑区 -->
<code-block-editor v-if="isShowCodeBlockEditor"> <code-block-editor v-if="isShowCodeBlockEditor" :paramsColConfig="paramsColConfig">
<template #code-block-edit-panel-header="{ id }"> <template #code-block-edit-panel-header="{ id }">
<slot name="code-block-edit-panel-header" :id="id"></slot> <slot name="code-block-edit-panel-header" :id="id"></slot>
</template> </template>
@ -92,16 +92,18 @@ import { Close, Edit, Link, View } from '@element-plus/icons-vue';
import { cloneDeep, forIn, isEmpty } from 'lodash-es'; import { cloneDeep, forIn, isEmpty } from 'lodash-es';
import { TMagicButton, TMagicInput, tMagicMessage, TMagicScrollbar, TMagicTooltip, TMagicTree } from '@tmagic/design'; import { TMagicButton, TMagicInput, tMagicMessage, TMagicScrollbar, TMagicTooltip, TMagicTree } from '@tmagic/design';
import { ColumnConfig } from '@tmagic/form';
import { CodeBlockContent, Id } from '@tmagic/schema'; import { CodeBlockContent, Id } from '@tmagic/schema';
import Icon from '../../../components/Icon.vue'; import Icon from '../../../components/Icon.vue';
import type { CodeRelation, Services } from '../../../type'; import type { CodeRelation, Services } from '../../../type';
import { CodeDeleteErrorType, CodeDslItem, CodeEditorMode, ListState } from '../../../type'; import { CodeDeleteErrorType, CodeDslItem, ListState } from '../../../type';
import codeBlockEditor from './CodeBlockEditor.vue'; import codeBlockEditor from './CodeBlockEditor.vue';
const props = defineProps<{ const props = defineProps<{
customError?: (id: Id, errorType: CodeDeleteErrorType) => any; customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
paramsColConfig?: ColumnConfig;
}>(); }>();
const services = inject<Services>('services'); const services = inject<Services>('services');
@ -168,7 +170,6 @@ const createCodeBlock = async () => {
content: `({app, params}) => {\n // place your code here\n}`, content: `({app, params}) => {\n // place your code here\n}`,
params: [], params: [],
}; };
await codeBlockService.setMode(CodeEditorMode.EDITOR);
const id = await codeBlockService.getUniqueId(); const id = await codeBlockService.getUniqueId();
await codeBlockService.setCodeDslById(id, codeConfig); await codeBlockService.setCodeDslById(id, codeConfig);
codeBlockService.setCodeEditorContent(true, id); codeBlockService.setCodeEditorContent(true, id);
@ -176,7 +177,6 @@ const createCodeBlock = async () => {
// //
const editCode = async (key: Id) => { const editCode = async (key: Id) => {
await services?.codeBlockService.setMode(CodeEditorMode.EDITOR);
services?.codeBlockService.setCodeEditorContent(true, key); services?.codeBlockService.setCodeEditorContent(true, key);
}; };

View File

@ -23,7 +23,7 @@ import { CodeBlockContent, CodeBlockDSL, HookType, Id, MNode } from '@tmagic/sch
import editorService from '../services/editor'; import editorService from '../services/editor';
import type { CodeRelation, CodeState, HookData } from '../type'; import type { CodeRelation, CodeState, HookData } from '../type';
import { CODE_DRAFT_STORAGE_KEY, CodeEditorMode } from '../type'; import { CODE_DRAFT_STORAGE_KEY } from '../type';
import { info } from '../utils/logger'; import { info } from '../utils/logger';
import BaseService from './BaseService'; import BaseService from './BaseService';
@ -34,7 +34,6 @@ class CodeBlock extends BaseService {
codeDsl: null, codeDsl: null,
id: '', id: '',
editable: true, editable: true,
mode: CodeEditorMode.EDITOR,
combineIds: [], combineIds: [],
undeletableList: [], undeletableList: [],
relations: {}, relations: {},
@ -50,7 +49,6 @@ class CodeBlock extends BaseService {
'setCodeDslById', 'setCodeDslById',
'setCodeEditorShowStatus', 'setCodeEditorShowStatus',
'setEditStatus', 'setEditStatus',
'setMode',
'setCombineIds', 'setCombineIds',
'setUndeleteableList', 'setUndeleteableList',
'deleteCodeDslByIds', 'deleteCodeDslByIds',
@ -214,23 +212,6 @@ class CodeBlock extends BaseService {
return this.state.id; return this.state.id;
} }
/**
*
* @returns {CodeEditorMode}
*/
public getMode(): CodeEditorMode {
return this.state.mode;
}
/**
*
* @param {CodeEditorMode} mode
* @returns {void}
*/
public async setMode(mode: CodeEditorMode): Promise<void> {
this.state.mode = mode;
}
/** /**
* id数组 * id数组
* @param {string[]} ids id数组 * @param {string[]} ids id数组
@ -349,7 +330,6 @@ class CodeBlock extends BaseService {
this.state.codeDsl = null; this.state.codeDsl = null;
this.state.id = ''; this.state.id = '';
this.state.editable = true; this.state.editable = true;
this.state.mode = CodeEditorMode.EDITOR;
this.state.combineIds = []; this.state.combineIds = [];
this.state.undeletableList = []; this.state.undeletableList = [];
} }
@ -386,6 +366,7 @@ class CodeBlock extends BaseService {
*/ */
private recurseMNode(node: MNode, relations: CodeRelation): void { private recurseMNode(node: MNode, relations: CodeRelation): void {
forIn(node, (value, key) => { forIn(node, (value, key) => {
let unConfirmedValue: MNode = { id: node.id };
if (value?.hookType === HookType.CODE && !isEmpty(value.hookData)) { if (value?.hookType === HookType.CODE && !isEmpty(value.hookData)) {
value.hookData.forEach((relationItem: HookData) => { value.hookData.forEach((relationItem: HookData) => {
// continue // continue
@ -399,6 +380,16 @@ class CodeBlock extends BaseService {
} }
codeItem[node.id].push(key); codeItem[node.id].push(key);
}); });
// continue
return;
}
if (typeof value === 'object') {
// 检查value内部是否有嵌套
unConfirmedValue = {
...unConfirmedValue,
...value,
};
this.recurseMNode(unConfirmedValue, relations);
} }
}); });
if (!isEmpty(node.items)) { if (!isEmpty(node.items)) {

View File

@ -78,6 +78,10 @@
.m-fields-code-select { .m-fields-code-select {
width: 100%; width: 100%;
.edit-icon {
cursor: pointer;
margin-right: 5px;
}
} }
.el-dialog.is-fullscreen.code-editor-dialog { .el-dialog.is-fullscreen.code-editor-dialog {

View File

@ -335,8 +335,6 @@ export type CodeState = {
id: Id; id: Id;
/** 代码块是否可编辑 */ /** 代码块是否可编辑 */
editable: boolean; editable: boolean;
/** 代码编辑面板的展示模式 */
mode: CodeEditorMode;
/** list模式下左侧展示的代码列表 */ /** list模式下左侧展示的代码列表 */
combineIds: string[]; combineIds: string[];
/** 为业务逻辑预留的不可删除的代码块列表,由业务逻辑维护(如代码块上线后不可删除) */ /** 为业务逻辑预留的不可删除的代码块列表,由业务逻辑维护(如代码块上线后不可删除) */
@ -359,13 +357,6 @@ export type CodeRelation = {
}; };
}; };
export enum CodeEditorMode {
/** 左侧菜单,右侧代码 */
LIST = 'list',
/** 全屏代码 */
EDITOR = 'editor',
}
export interface CodeDslItem { export interface CodeDslItem {
/** 代码块id */ /** 代码块id */
id: Id; id: Id;
@ -406,6 +397,7 @@ export interface CodeParamStatement {
name: string; name: string;
/** 参数类型 */ /** 参数类型 */
type?: string; type?: string;
[key: string]: any;
} }
export interface StepValue { export interface StepValue {

View File

@ -24,11 +24,12 @@
<TMagicTableColumn <TMagicTableColumn
label="操作" label="操作"
width="55" :width="config.operateColWidth || 55"
align="center" align="center"
:fixed="config.fixed === false ? undefined : 'left'" :fixed="config.fixed === false ? undefined : 'left'"
> >
<template v-slot="scope"> <template v-slot="scope">
<slot name="operateCol" :scope="scope"></slot>
<TMagicIcon <TMagicIcon
v-show="showDelete(scope.$index + 1 + pagecontext * pagesize - 1)" v-show="showDelete(scope.$index + 1 + pagecontext * pagesize - 1)"
class="m-table-delete-icon" class="m-table-delete-icon"

View File

@ -598,11 +598,11 @@ export interface PanelConfig extends FormItem, ContainerCommonConfig {
schematic?: string; schematic?: string;
} }
export interface ColumnConfig extends FormItem, ContainerCommonConfig { export interface ColumnConfig extends FormItem {
name: string; name?: string;
label: string; label: string;
width: string | number; width?: string | number;
sortable: boolean; sortable?: boolean;
[key: string]: any; [key: string]: any;
} }
@ -622,6 +622,8 @@ export interface TableConfig extends FormItem {
border?: boolean; border?: boolean;
/** 显示行号 */ /** 显示行号 */
showIndex?: boolean; showIndex?: boolean;
/** 操作栏宽度 */
operateColWidth?: number | string;
pagination?: boolean; pagination?: boolean;
enum?: any[]; enum?: any[];
/** 是否显示添加按钮 */ /** 是否显示添加按钮 */
@ -642,7 +644,7 @@ export interface TableConfig extends FormItem {
enableFullscreen?: boolean; enableFullscreen?: boolean;
fixed?: boolean; fixed?: boolean;
itemExtra?: string | FilterFunction; itemExtra?: string | FilterFunction;
rowKey: string; rowKey?: string;
} }
export interface GroupListConfig extends FormItem { export interface GroupListConfig extends FormItem {

View File

@ -90,6 +90,8 @@ export interface CodeBlockContent {
export interface CodeParam { export interface CodeParam {
/** 参数名 */ /** 参数名 */
name: string; name: string;
/** 扩展字段 */
[propName: string]: any;
} }
export interface PastePosition { export interface PastePosition {
left?: number; left?: number;