feat(core,editor,data-source,form,schema): 新增数据源方法配置,支持事件联动数据源方法

This commit is contained in:
roymondchen 2023-07-18 19:35:54 +08:00
parent 1a546c326c
commit 2a0680c707
43 changed files with 1292 additions and 968 deletions

View File

@ -31,6 +31,7 @@ import {
CodeBlockDSL,
CodeItemConfig,
CompItemConfig,
DataSourceItemConfig,
DeprecatedEventConfig,
EventConfig,
Id,
@ -321,6 +322,28 @@ class App extends EventEmitter {
}
}
public async dataSourceActionHandler(eventConfig: DataSourceItemConfig) {
const { dataSourceMethod = [], params = {} } = eventConfig;
const [id, methodName] = dataSourceMethod;
if (!id || !methodName) return;
const dataSource = this.dataSourceManager?.get(id);
if (!dataSource) return;
const methods = dataSource.getMethods() || [];
const method = methods.find((item) => item.name === methodName);
if (!method) return;
if (typeof method.content === 'function') {
await method.content({ app: this, params, dataSource });
}
}
public compiledNode(node: MNode, content: DataSourceManagerData, sourceId?: Id) {
return compiledNode(
(value: any) => {
@ -364,6 +387,8 @@ class App extends EventEmitter {
} else if (actionItem.actionType === ActionType.CODE) {
// 执行代码块
await this.codeActionHandler(actionItem as CodeItemConfig);
} else if (actionItem.actionType === ActionType.DATA_SOURCE) {
await this.dataSourceActionHandler(actionItem as DataSourceItemConfig);
}
}
} else {

View File

@ -102,8 +102,11 @@ class Node extends EventEmitter {
if (this.data[hook]?.hookType !== HookType.CODE || isEmpty(this.app.codeDsl)) return;
for (const item of this.data[hook].hookData) {
const { codeId, params = {} } = item;
if (this.app.codeDsl![codeId] && typeof this.app.codeDsl![codeId]?.content === 'function') {
await this.app.codeDsl![codeId].content({ app: this.app, params });
const functionContent = this.app.codeDsl?.[codeId]?.content;
if (typeof functionContent === 'function') {
await functionContent({ app: this.app, params });
}
}
}

View File

@ -17,7 +17,7 @@
*/
import EventEmitter from 'events';
import type { DataSchema } from '@tmagic/schema';
import type { CodeBlockContent, DataSchema } from '@tmagic/schema';
import type { DataSourceOptions } from '@data-source/types';
import { getDefaultValueFromFields } from '@data-source/util';
@ -35,12 +35,14 @@ export default class DataSource extends EventEmitter {
public data: Record<string, any> = {};
private fields: DataSchema[] = [];
private methods: CodeBlockContent[] = [];
constructor(options: DataSourceOptions) {
super();
this.id = options.schema.id;
this.setFields(options.schema.fields);
this.setMethods(options.schema.methods || []);
this.updateDefaultData();
}
@ -49,6 +51,14 @@ export default class DataSource extends EventEmitter {
this.fields = fields;
}
public setMethods(methods: CodeBlockContent[]) {
this.methods = methods;
}
public getMethods() {
return this.methods;
}
public setData(data: Record<string, any>) {
// todo: 校验数据,看是否符合 schema
this.data = data;

View File

@ -40,10 +40,6 @@
<template #code-block-panel-tool="{ id, data }">
<slot name="code-block-panel-tool" :id="id" :data="data"></slot>
</template>
<template #code-block-edit-panel-header="{ id }">
<slot name="code-block-edit-panel-header" :id="id"></slot>
</template>
</Sidebar>
</slot>
</template>

View File

@ -0,0 +1,141 @@
<template>
<MFormDrawer
ref="fomDrawer"
label-width="80px"
:title="content.name"
:width="size"
:config="functionConfig"
:values="content"
:disabled="disabled"
@submit="submitForm"
@error="errorHandler"
></MFormDrawer>
</template>
<script lang="ts" setup>
import { computed, inject, ref } from 'vue';
import { tMagicMessage } from '@tmagic/design';
import { ColumnConfig, FormState, MFormDrawer } from '@tmagic/form';
import type { CodeBlockContent } from '@tmagic/schema';
import type { Services } from '@editor/type';
import { getConfig } from '@editor/utils/config';
defineOptions({
name: 'MEditorCodeBlockEditor',
});
defineProps<{
content: CodeBlockContent;
disabled?: boolean;
}>();
const emit = defineEmits<{
submit: [values: CodeBlockContent];
}>();
const services = inject<Services>('services');
const size = computed(() => globalThis.document.body.clientWidth - (services?.uiService.get('columnWidth').left || 0));
const defaultParamColConfig: ColumnConfig = {
type: 'row',
label: '参数类型',
items: [
{
text: '参数类型',
labelWidth: '70px',
type: 'select',
name: 'type',
options: [
{
text: '数字',
label: '数字',
value: 'number',
},
{
text: '字符串',
label: '字符串',
value: 'text',
},
{
text: '组件',
label: '组件',
value: 'ui-select',
},
],
},
],
};
const functionConfig = computed(() => [
{
text: '名称',
name: 'name',
},
{
text: '注释',
name: 'desc',
},
{
type: 'table',
border: true,
text: '参数',
enableFullscreen: false,
name: 'params',
maxHeight: '300px',
dropSort: false,
items: [
{
type: 'text',
label: '参数名',
name: 'name',
},
{
type: 'text',
label: '注释',
name: 'extra',
},
services?.codeBlockService.getParamsColConfig() || defaultParamColConfig,
],
},
{
name: 'content',
type: 'vs-code',
options: inject('codeOptions', {}),
onChange: (formState: FormState | undefined, code: string) => {
try {
// js
getConfig('parseDSL')(code);
return code;
} catch (error: any) {
tMagicMessage.error(error.message);
throw error;
}
},
},
]);
const submitForm = async (values: CodeBlockContent) => {
emit('submit', values);
};
const errorHandler = (error: any) => {
tMagicMessage.error(error.message);
};
const fomDrawer = ref<InstanceType<typeof MFormDrawer>>();
defineExpose({
show() {
fomDrawer.value?.show();
},
hide() {
fomDrawer.value?.hide();
},
});
</script>

View File

@ -1,148 +0,0 @@
<template>
<div class="m-editor-wrapper" :class="isFullScreen ? 'fullScreen' : 'normal'">
<magic-code-editor
ref="codeEditor"
class="m-editor-container"
:init-values="`${codeContent}`"
@save="saveCodeDraft"
:language="language"
:options="codeOptions"
></magic-code-editor>
<div class="m-editor-content-bottom" v-if="editable">
<TMagicButton type="primary" class="button" @click="toggleFullScreen">
{{ isFullScreen ? '退出全屏' : '全屏' }}</TMagicButton
>
<TMagicButton type="primary" class="button" @click="saveAndClose">确认</TMagicButton>
<TMagicButton class="button" @click="close">关闭</TMagicButton>
</div>
<div class="m-editor-content-bottom" v-else>
<TMagicButton type="primary" class="button" @click="toggleFullScreen">
{{ isFullScreen ? '退出全屏' : '全屏' }}</TMagicButton
>
<TMagicButton class="button" @click="close">关闭</TMagicButton>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, inject, ref, watchEffect } from 'vue';
import type * as monaco from 'monaco-editor';
import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
import { Id } from '@tmagic/schema';
import { datetimeFormatter } from '@tmagic/utils';
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
import type { Services } from '@editor/type';
defineOptions({
name: 'MEditorCodeDraftEditor',
});
const props = withDefaults(
defineProps<{
/** 代码id */
id: Id;
/** 代码内容 */
content: string;
/** 是否可编辑 */
editable?: boolean;
/** 是否自动保存草稿 */
autoSaveDraft?: boolean;
/** 编辑器参数 */
codeOptions?: Object;
/** 编辑器语言 */
language?: string;
}>(),
{
editable: true,
autoSaveDraft: true,
},
);
const emit = defineEmits(['close', 'saveAndClose']);
const services = inject<Services>('services');
const codeContent = ref<string>('');
const editorContent = ref<string>('');
const codeEditor = ref<InstanceType<typeof MagicCodeEditor>>();
//
const originCodeContent = ref<string>('');
const isFullScreen = ref<boolean>(false);
const codeOptions = computed(() => ({
...props.codeOptions,
readOnly: !props.editable,
}));
watchEffect(() => {
codeContent.value = props.content;
if (!originCodeContent.value) {
//
originCodeContent.value = codeContent.value;
}
// 稿稿
const codeDraft = services?.codeBlockService.getCodeDraft(props.id);
if (codeDraft) {
codeContent.value = codeDraft;
}
});
// 稿
const saveCodeDraft = async (codeValue: string) => {
if (!props.autoSaveDraft) return;
if (originCodeContent.value === codeValue) {
// 稿稿
services?.codeBlockService.removeCodeDraft(props.id);
return;
}
services?.codeBlockService.setCodeDraft(props.id, codeValue);
tMagicMessage.success(`代码草稿成功保存到本地 ${datetimeFormatter(new Date())}`);
};
//
const saveAndClose = (): void => {
if (!codeEditor.value || !props.editable) return;
//
editorContent.value = (codeEditor.value.getEditor() as monaco.editor.IStandaloneCodeEditor)?.getValue();
emit('saveAndClose', editorContent.value);
};
//
const close = async (): Promise<void> => {
const codeDraft = services?.codeBlockService.getCodeDraft(props.id);
if (codeDraft) {
try {
await tMagicMessageBox.confirm('您有代码修改未保存,是否保存后再关闭?', '提示', {
confirmButtonText: '保存并关闭',
cancelButtonText: '直接关闭',
type: 'warning',
distinguishCancelAndClose: true,
});
//
saveAndClose();
} catch (action: any) {
if (action === 'cancel') {
// 稿
services?.codeBlockService.removeCodeDraft(props.id);
emit('close');
}
}
} else {
emit('close');
}
};
//
const toggleFullScreen = (): void => {
isFullScreen.value = !isFullScreen.value;
if (codeEditor.value) {
codeEditor.value.focus();
}
};
defineExpose({
saveAndClose,
close,
});
</script>

View File

@ -51,7 +51,6 @@ const codeParamsConfig = computed(() => getFormConfig(props.paramsConfig));
const onParamsChangeHandler = async () => {
try {
const value = await form.value?.submitForm(true);
console.log(value);
emit('change', value);
} catch (e) {
console.log(e);

View File

@ -1,198 +0,0 @@
<template>
<TMagicCard shadow="never">
<template #header>
<div class="code-name-wrapper">
<div class="code-name-label">代码块名称</div>
<TMagicInput class="code-name-input" v-model="codeName" :disabled="!editable" />
</div>
<div class="code-name-wrapper">
<div class="code-name-label">参数</div>
<m-form-table
style="width: 800px"
:config="tableConfig"
:model="tableModel"
:enableToggleMode="false"
:disabled="!editable"
name="params"
prop="params"
size="small"
>
</m-form-table>
</div>
</template>
<CodeDraftEditor
ref="codeDraftEditor"
:id="id"
:content="codeContent"
:editable="editable"
:autoSaveDraft="autoSaveDraft"
:codeOptions="codeOptions"
language="javascript"
@saveAndClose="saveAndClose"
@close="close"
></CodeDraftEditor>
</TMagicCard>
</template>
<script lang="ts" setup>
import { inject, provide, ref, watchEffect } from 'vue';
import { cloneDeep } from 'lodash-es';
import { TMagicCard, TMagicInput, tMagicMessage } from '@tmagic/design';
import { ColumnConfig, TableConfig } from '@tmagic/form';
import { CodeParam, Id } from '@tmagic/schema';
import type { Services } from '@editor/type';
import { getConfig } from '@editor/utils/config';
import CodeDraftEditor from './CodeDraftEditor.vue';
defineOptions({
name: 'MEditorFunctionEditor',
});
provide('mForm', null);
const defaultParamColConfig: ColumnConfig = {
type: 'row',
label: '参数类型',
items: [
{
text: '参数类型',
labelWidth: '70px',
type: 'select',
name: 'type',
options: [
{
text: '数字',
label: '数字',
value: 'number',
},
{
text: '字符串',
label: '字符串',
value: 'text',
},
],
},
],
};
const props = withDefaults(
defineProps<{
/** 代码块id */
id: Id;
/** 代码块名称 */
name: string;
/** 代码内容 */
content: string;
/** 是否可编辑 */
editable?: boolean;
/** 是否自动保存草稿 */
autoSaveDraft?: boolean;
/** 编辑器扩展参数 */
codeOptions?: object;
/** 代码参数扩展配置 */
paramsColConfig?: ColumnConfig;
}>(),
{
editable: true,
autoSaveDraft: true,
},
);
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: 'extra',
width: 200,
},
paramsColConfig,
],
};
const services = inject<Services>('services');
const codeName = ref<string>('');
const codeContent = ref<string>('');
const evalRes = ref(true);
const tableModel = ref<{ params: CodeParam[] }>();
watchEffect(() => {
codeName.value = props.name;
codeContent.value = props.content;
});
const initTableModel = (): void => {
const codeDsl = cloneDeep(services?.codeBlockService.getCodeDsl());
if (!codeDsl) return;
tableModel.value = {
params: codeDsl[props.id]?.params || [],
};
};
initTableModel();
//
const beforeSave = (codeValue: string): boolean => {
try {
// js
getConfig('parseDSL')(codeValue);
return true;
} catch (e: any) {
tMagicMessage.error(e.stack);
return false;
}
};
//
const saveCode = async (codeValue: string): Promise<void> => {
if (!props.editable) return;
evalRes.value = beforeSave(codeValue);
if (evalRes.value) {
// dsl
await services?.codeBlockService.setCodeDslById(props.id, {
name: codeName.value,
content: codeValue,
params: tableModel.value?.params || [],
});
tMagicMessage.success('代码成功保存到本地');
// 稿
services?.codeBlockService.removeCodeDraft(props.id);
}
};
//
const saveAndClose = async (codeValue: string): Promise<void> => {
await saveCode(codeValue);
if (evalRes.value) {
close();
}
};
//
const close = (): void => {
services?.codeBlockService.setCodeEditorShowStatus(false);
};
const codeDraftEditor = ref<InstanceType<typeof CodeDraftEditor>>();
defineExpose({
codeDraftEditor,
});
</script>

View File

@ -1,32 +1,45 @@
<template>
<magic-code-editor
:style="`height: ${height}`"
<MagicCodeEditor
:height="height"
:init-values="model[name]"
:language="language"
:options="config.options"
:options="{
...config.options,
readOnly: disabled,
}"
@save="save"
></magic-code-editor>
></MagicCodeEditor>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
defineOptions({
name: 'MEditorCode',
});
const emit = defineEmits(['change']);
const props = defineProps<{
model: any;
name: string;
config: {
language?: string;
options?: Object;
height?: string;
};
prop: string;
}>();
const props = withDefaults(
defineProps<{
config: {
language?: string;
options?: Object;
height?: string;
};
model: any;
name: string;
prop: string;
lastValues?: any;
disabled?: boolean;
size?: 'small' | 'default' | 'large';
}>(),
{
disabled: false,
},
);
const language = computed(() => props.config.language || 'javascript');
const height = computed(() => props.config.height || `${document.body.clientHeight - 168}px`);

View File

@ -9,18 +9,26 @@
@change="onParamsChangeHandler"
></m-form-container>
<!-- 查看/编辑按钮 -->
<Icon v-if="model[name]" class="icon" :icon="!disabled ? Edit : View" @click="editCode"></Icon>
<Icon v-if="model[name]" class="icon" :icon="!disabled ? Edit : View" @click="editCode(model[name])"></Icon>
</div>
<!-- 参数填写框 -->
<CodeParams
v-if="paramsConfig.length"
name="params"
:model="model"
:size="size"
:disabled="disabled"
:params-config="paramsConfig"
@change="onParamsChangeHandler"
></CodeParams>
<CodeBlockEditor
ref="codeBlockEditor"
v-if="codeConfig"
:disabled="disabled"
:content="codeConfig"
@submit="submitCodeBlockHandler"
></CodeBlockEditor>
</div>
</template>
@ -32,9 +40,11 @@ import { isEmpty, map } from 'lodash-es';
import { createValues } from '@tmagic/form';
import type { Id } from '@tmagic/schema';
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
import CodeParams from '@editor/components/CodeParams.vue';
import Icon from '@editor/components/Icon.vue';
import type { CodeParamStatement, CodeSelectColConfig, Services } from '@editor/type';
import { useCodeBlockEdit } from '@editor/utils/use-code-block-edit';
defineOptions({
name: 'MEditorCodeSelectCol',
@ -53,7 +63,9 @@ const props = withDefaults(
disabled?: boolean;
size: 'small' | 'default' | 'large';
}>(),
{},
{
disabled: false,
},
);
/**
@ -114,8 +126,5 @@ const onParamsChangeHandler = (value: any) => {
emit('change', props.model);
};
//
const editCode = () => {
services?.codeBlockService.setCodeEditorContent(true, props.model[props.name]);
};
const { codeBlockEditor, codeConfig, editCode, submitCodeBlockHandler } = useCodeBlockEdit(services?.codeBlockService);
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="m-editor-data-source-fields">
<MagicTable :data="model[name]" :columns="filedColumns"></MagicTable>
<MagicTable :data="model[name]" :columns="fieldColumns"></MagicTable>
<div class="m-editor-data-source-fields-footer">
<TMagicButton size="small" type="primary" :disabled="disabled" plain @click="newHandler()">添加</TMagicButton>
@ -12,6 +12,7 @@
:config="dataSourceFieldsConfig"
:values="fieldValues"
:parentValues="model[name]"
:disabled="disabled"
@submit="fieldChange"
></MFormDialog>
</div>
@ -35,6 +36,7 @@ const props = withDefaults(
};
model: any;
prop: string;
lastValues?: any;
disabled: boolean;
name: string;
}>(),
@ -50,27 +52,24 @@ const fieldValues = ref<Record<string, any>>({});
const filedTitle = ref('');
const newHandler = () => {
if (!addDialog.value) return;
fieldValues.value = {};
filedTitle.value = '新增属性';
addDialog.value.dialogVisible = true;
addDialog.value?.show();
};
const fieldChange = ({ index, ...value }: Record<string, any>) => {
if (!addDialog.value) return;
if (index > -1) {
props.model[props.name][index] = value;
} else {
props.model[props.name].push(value);
}
addDialog.value.dialogVisible = false;
addDialog.value?.hide();
emit('change', props.model[props.name]);
};
const filedColumns = [
const fieldColumns = [
{
label: '属性名称',
prop: 'title',
@ -90,13 +89,12 @@ const filedColumns = [
{
text: '编辑',
handler: (row: Record<string, any>, index: number) => {
if (!addDialog.value) return;
fieldValues.value = {
...row,
index,
};
filedTitle.value = `编辑${row.title}`;
addDialog.value.dialogVisible = true;
addDialog.value?.show();
},
},
{

View File

@ -0,0 +1,147 @@
<template>
<div class="m-fields-data-source-method-select">
<div class="data-source-method-select-container">
<m-form-container
class="select"
:config="cascaderConfig"
:model="model"
@change="onChangeHandler"
></m-form-container>
<Icon v-if="model[name]" class="icon" :icon="!disabled ? Edit : View" @click="editCodeHandler"></Icon>
</div>
<CodeParams
v-if="paramsConfig.length"
name="params"
:model="model"
:size="size"
:disabled="disabled"
:params-config="paramsConfig"
@change="onChangeHandler"
></CodeParams>
<CodeBlockEditor
ref="codeBlockEditor"
v-if="codeConfig"
:disabled="disabled"
:content="codeConfig"
@submit="submitCodeBlockHandler"
></CodeBlockEditor>
</div>
</template>
<script lang="ts" setup name="">
import { computed, inject, ref } from 'vue';
import { Edit, View } from '@element-plus/icons-vue';
import { createValues } from '@tmagic/form';
import type { CodeBlockContent, Id } from '@tmagic/schema';
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
import CodeParams from '@editor/components/CodeParams.vue';
import Icon from '@editor/components/Icon.vue';
import type { CodeParamStatement, DataSourceMethodSelectConfig, Services } from '@editor/type';
import { useDataSourceMethod } from '@editor/utils/use-data-source-method';
defineOptions({
name: 'MEditorDataSourceMethodSelect',
});
const services = inject<Services>('services');
const emit = defineEmits(['change']);
const props = withDefaults(
defineProps<{
config: DataSourceMethodSelectConfig;
model: any;
lastValues?: any;
prop: string;
name: string;
disabled?: boolean;
size: 'small' | 'default' | 'large';
}>(),
{
disabled: false,
},
);
const dataSources = computed(() => services?.dataSourceService.get('dataSources'));
const getParamItemsConfig = ([dataSourceId, medthodName]: [Id, string] = ['', '']): CodeParamStatement[] => {
if (!dataSourceId) return [];
const paramStatements = dataSources.value
?.find((item) => item.id === dataSourceId)
?.methods?.find((item) => item.name === medthodName)?.params;
if (!paramStatements) return [];
return paramStatements.map((paramState: CodeParamStatement) => ({
labelWidth: '100px',
text: paramState.name,
...paramState,
}));
};
const paramsConfig = ref<CodeParamStatement[]>(getParamItemsConfig(props.model.dataSourceMethod));
const setParamsConfig = (dataSourceMethod: [Id, string], formState: any = {}) => {
// codeIdmodelcodeIdparams
paramsConfig.value = dataSourceMethod ? getParamItemsConfig(dataSourceMethod) : [];
if (paramsConfig.value.length) {
props.model.params = createValues(formState, paramsConfig.value, {}, props.model.params);
} else {
props.model.params = {};
}
};
const cascaderConfig = {
type: 'cascader',
text: '数据源方法',
name: props.name,
labelWidth: '80px',
options: () =>
dataSources.value
?.filter((ds) => ds.methods?.length)
?.map((ds) => ({
label: `${ds.title}${ds.id}`,
value: ds.id,
children: ds.methods?.map((method) => ({
label: method.name,
value: method.name,
})),
})) || [],
onChange: (formState: any, dataSourceMethod: [Id, string]) => {
setParamsConfig(dataSourceMethod, formState);
return dataSourceMethod;
},
};
/**
* 参数值修改更新
*/
const onChangeHandler = (value: any) => {
props.model.params = value.params;
emit('change', props.model);
};
const { codeBlockEditor, codeConfig, editCode, submitCode } = useDataSourceMethod();
const editCodeHandler = () => {
const [id, name] = props.model[props.name];
const dataSource = services?.dataSourceService.getDataSourceById(id);
if (!dataSource) return;
editCode(dataSource, name);
setParamsConfig([id, name]);
};
const submitCodeBlockHandler = (value: CodeBlockContent) => {
submitCode(value);
};
</script>

View File

@ -0,0 +1,103 @@
<template>
<div class="m-editor-data-source-methods">
<MagicTable :data="model[name]" :columns="methodColumns"></MagicTable>
<div class="m-editor-data-source-methods-footer">
<TMagicButton size="small" type="primary" :disabled="disabled" plain @click="createCodeHandler"
>添加</TMagicButton
>
</div>
<CodeBlockEditor
ref="codeBlockEditor"
v-if="codeConfig"
:disabled="disabled"
:content="codeConfig"
@submit="submitCodeHandler"
></CodeBlockEditor>
</div>
</template>
<script setup lang="ts">
import { TMagicButton } from '@tmagic/design';
import type { CodeBlockContent } from '@tmagic/schema';
import { MagicTable } from '@tmagic/table';
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
import { useDataSourceMethod } from '@editor/utils/use-data-source-method';
import { CodeParamStatement } from '..';
defineOptions({
name: 'MEditorDataSourceMethods',
});
const props = withDefaults(
defineProps<{
config: {
type: 'data-source-methods';
};
model: any;
prop: string;
lastValues?: any;
disabled: boolean;
name: string;
}>(),
{
disabled: false,
},
);
const emit = defineEmits(['change']);
const { codeConfig, codeBlockEditor, createCode, editCode, deleteCode, submitCode } = useDataSourceMethod();
const methodColumns = [
{
label: '名称',
prop: 'name',
},
{
label: '注释',
prop: 'desc',
},
{
label: '参数',
prop: 'params',
formatter: (params: CodeParamStatement[]) => params.map((item) => item.name).join(', '),
},
{
label: '操作',
fixed: 'right',
actions: [
{
text: '编辑',
handler: (row: CodeBlockContent) => {
editCode(props.model, row.name);
emit('change', props.model[props.name]);
},
},
{
text: '删除',
buttonType: 'danger',
handler: (row: CodeBlockContent) => {
deleteCode(props.model, row.name);
emit('change', props.model[props.name]);
},
},
],
},
];
const createCodeHandler = () => {
createCode(props.model);
emit('change', props.model[props.name]);
};
const submitCodeHandler = (values: CodeBlockContent) => {
submitCode(values);
emit('change', props.model[props.name]);
};
</script>

View File

@ -42,7 +42,7 @@ import { TMagicButton } from '@tmagic/design';
import { FormState } from '@tmagic/form';
import { ActionType } from '@tmagic/schema';
import type { EventSelectConfig, Services } from '@editor/type';
import type { CodeSelectColConfig, DataSourceMethodSelectConfig, EventSelectConfig, Services } from '@editor/type';
defineOptions({
name: 'MEditorEventSelect',
@ -81,7 +81,7 @@ const actionTypeConfig = computed(() => {
const defaultActionTypeConfig = {
name: 'actionType',
text: '联动类型',
labelWidth: '70px',
labelWidth: '80px',
type: 'select',
defaultValue: ActionType.COMP,
options: () => [
@ -93,8 +93,15 @@ const actionTypeConfig = computed(() => {
{
text: '代码',
label: '代码',
disabled: !Object.keys(services?.codeBlockService.getCodeDsl() || {}).length,
value: ActionType.CODE,
},
{
text: '数据源',
label: '数据源',
disabled: !services?.dataSourceService.get('dataSources')?.filter((ds) => ds.methods?.length).length,
value: ActionType.DATA_SOURCE,
},
],
};
return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig };
@ -105,7 +112,7 @@ const targetCompConfig = computed(() => {
const defaultTargetCompConfig = {
name: 'to',
text: '联动组件',
labelWidth: '70px',
labelWidth: '80px',
type: 'ui-select',
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.actionType === ActionType.COMP,
};
@ -117,7 +124,7 @@ const compActionConfig = computed(() => {
const defaultCompActionConfig = {
name: 'method',
text: '动作',
labelWidth: '70px',
labelWidth: '80px',
type: 'select',
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.actionType === ActionType.COMP,
options: (mForm: FormState, { model }: any) => {
@ -135,14 +142,27 @@ const compActionConfig = computed(() => {
//
const codeActionConfig = computed(() => {
const defaultCodeActionConfig = {
const defaultCodeActionConfig: CodeSelectColConfig = {
type: 'code-select-col',
labelWidth: 0,
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.actionType === ActionType.CODE,
name: 'codeId',
disabled: () => !services?.codeBlockService.getEditStatus(),
display: (mForm, { model }) => model.actionType === ActionType.CODE,
};
return { ...defaultCodeActionConfig, ...props.config.codeActionConfig };
});
//
const dataSourceActionConfig = computed(() => {
const defaultDataSourceActionConfig: DataSourceMethodSelectConfig = {
type: 'data-source-method-select',
name: 'dataSourceMethod',
labelWidth: 0,
display: (mForm, { model }) => model.actionType === ActionType.DATA_SOURCE,
};
return { ...defaultDataSourceActionConfig, ...props.config.dataSourceActionConfig };
});
//
const tableConfig = computed(() => ({
type: 'table',
@ -188,7 +208,13 @@ const actionsConfig = computed(() => ({
name: 'actions',
expandAll: true,
enableToggleMode: false,
items: [actionTypeConfig.value, targetCompConfig.value, compActionConfig.value, codeActionConfig.value],
items: [
actionTypeConfig.value,
targetCompConfig.value,
compActionConfig.value,
codeActionConfig.value,
dataSourceActionConfig.value,
],
},
],
}));

View File

@ -1,7 +1,5 @@
<template>
<!-- 代码iconcdn链接https://cloudcache.tencent-cloud.com/qcloud/ui/static/government/0d463ed5-6407-4498-8865-3d05b5e70115.svg -->
<svg viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<defs><rect id="path-1" x="0" y="0" width="32" height="32"></rect></defs>
<g id="组件规范" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="03图标" transform="translate(-561.000000, -2356.000000)">

View File

@ -23,6 +23,8 @@ import CodeSelect from './fields/CodeSelect.vue';
import CodeSelectCol from './fields/CodeSelectCol.vue';
import DataSourceFields from './fields/DataSourceFields.vue';
import DataSourceInput from './fields/DataSourceInput.vue';
import DataSourceMethods from './fields/DataSourceMethods.vue';
import DataSourceMethodSelect from './fields/DataSourceMethodSelect.vue';
import DataSourceSelect from './fields/DataSourceSelect.vue';
import EventSelect from './fields/EventSelect.vue';
import KeyValue from './fields/KeyValue.vue';
@ -53,8 +55,10 @@ export { default as LayerPanel } from './layouts/sidebar/LayerPanel.vue';
export { default as CodeSelect } from './fields/CodeSelect.vue';
export { default as CodeSelectCol } from './fields/CodeSelectCol.vue';
export { default as DataSourceFields } from './fields/DataSourceFields.vue';
export { default as DataSourceMethods } from './fields/DataSourceMethods.vue';
export { default as DataSourceInput } from './fields/DataSourceInput.vue';
export { default as DataSourceSelect } from './fields/DataSourceSelect.vue';
export { default as DataSourceMethodSelect } from './fields/DataSourceMethodSelect.vue';
export { default as EventSelect } from './fields/EventSelect.vue';
export { default as KeyValue } from './fields/KeyValue.vue';
export { default as CodeBlockList } from './layouts/sidebar/code-block/CodeBlockList.vue';
@ -65,6 +69,7 @@ export { default as Icon } from './components/Icon.vue';
export { default as LayoutContainer } from './components/SplitView.vue';
export { default as SplitView } from './components/SplitView.vue';
export { default as Resizer } from './components/Resizer.vue';
export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
const defaultInstallOpt: InstallOptions = {
// eslint-disable-next-line no-eval
@ -90,5 +95,7 @@ export default {
app.component('m-fields-key-value', KeyValue);
app.component('m-fields-data-source-input', DataSourceInput);
app.component('m-fields-data-source-select', DataSourceSelect);
app.component('m-fields-data-source-methods', DataSourceMethods);
app.component('m-fields-data-source-method-select', DataSourceMethodSelect);
},
};

View File

@ -1,13 +1,32 @@
<template>
<div ref="codeEditor" class="magic-code-editor"></div>
<div :class="`magic-code-editor`">
<Teleport to="body" :disabled="!fullScreen">
<div
:class="`magic-code-editor-wrapper${fullScreen ? ' full-screen' : ''}`"
:style="!fullScreen && height ? `height: ${height}` : ''"
>
<TMagicButton
class="magic-code-editor-full-screen-icon"
circle
size="small"
:icon="FullScreen"
@click="fullScreenHandler"
></TMagicButton>
<div ref="codeEditor" class="magic-code-editor-content"></div>
</div>
</Teleport>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { FullScreen } from '@element-plus/icons-vue';
import { throttle } from 'lodash-es';
import * as monaco from 'monaco-editor';
import serialize from 'serialize-javascript';
import { TMagicButton } from '@tmagic/design';
defineOptions({
name: 'MEditorCodeEditor',
});
@ -21,6 +40,7 @@ const props = withDefaults(
options?: {
[key: string]: any;
};
height?: string;
autoSave?: boolean;
}>(),
{
@ -155,6 +175,17 @@ onUnmounted(() => {
resizeObserver.disconnect();
});
const fullScreen = ref(false);
const fullScreenHandler = () => {
fullScreen.value = !fullScreen.value;
setTimeout(() => {
vsEditor?.focus();
vsEditor?.layout();
vsDiffEditor?.focus();
vsDiffEditor?.layout();
});
};
defineExpose({
values,

View File

@ -58,14 +58,6 @@
<component v-else-if="config.slots?.codeBlockPanelTool" :is="config.slots.codeBlockPanelTool" />
</template>
<template
#code-block-edit-panel-header="{ id }"
v-if="config.$key === 'code-block' || config.slots?.codeBlockEditPanelHeader"
>
<slot v-if="config.$key === 'code-block'" name="code-block-edit-panel-header" :id="id"></slot>
<component v-else-if="config.slots?.codeBlockEditPanelHeader" :is="config.slots.codeBlockEditPanelHeader" />
</template>
<template
#layer-node-content="{ data: nodeData, node }"
v-if="config.$key === 'layer' || config.slots?.layerNodeContent"
@ -93,7 +85,7 @@ import MIcon from '@editor/components/Icon.vue';
import type { MenuButton, MenuComponent, SideComponent, SideItem } from '@editor/type';
import { SideBarData } from '@editor/type';
import CodeBlockList from './code-block/CodeBlockList.vue';
import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
import DataSourceListPanel from './data-source/DataSourceListPanel.vue';
import ComponentListPanel from './ComponentListPanel.vue';
import LayerPanel from './LayerPanel.vue';
@ -143,7 +135,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
type: 'component',
icon: EditPen,
text: '代码编辑',
component: CodeBlockList,
component: CodeBlockListPanel,
slots: {},
},
'data-source': {

View File

@ -1,100 +0,0 @@
<template>
<TMagicDrawer
class="code-editor-dialog"
:model-value="true"
:title="currentTitle"
:close-on-press-escape="true"
:append-to-body="true"
:show-close="false"
:close-on-click-modal="true"
:size="size"
:before-close="handleClose"
>
<SplitView v-model:left="left" :min-left="45" class="code-editor-layout">
<!-- 右侧区域 -->
<template #center>
<div v-if="!isEmpty(codeConfig)" class="m-editor-code-block-editor-panel">
<slot name="code-block-edit-panel-header" :id="id"></slot>
<FunctionEditor
ref="functionEditor"
v-if="codeConfig"
:id="id"
:name="codeConfig.name"
:content="codeConfig.content"
:paramsColConfig="paramsColConfig"
:editable="!!editable"
:autoSaveDraft="true"
:codeOptions="codeOptions"
></FunctionEditor>
</div>
</template>
</SplitView>
</TMagicDrawer>
</template>
<script lang="ts" setup>
import { computed, inject, reactive, ref, watchEffect } from 'vue';
import { cloneDeep, forIn, isEmpty } from 'lodash-es';
import { TMagicDrawer } from '@tmagic/design';
import { ColumnConfig } from '@tmagic/form';
import { CodeBlockContent } from '@tmagic/schema';
import FunctionEditor from '@editor/components/FunctionEditor.vue';
import SplitView from '@editor/components/SplitView.vue';
import type { ListState, Services } from '@editor/type';
import { serializeConfig } from '@editor/utils/editor';
defineOptions({
name: 'MEditorCodeBlockEditor',
});
const services = inject<Services>('services');
const codeOptions = inject('codeOptions', {});
defineProps<{
paramsColConfig?: ColumnConfig;
}>();
const size = computed(() => globalThis.document.body.clientWidth - (services?.uiService.get('columnWidth').left || 0));
const left = ref(200);
const currentTitle = ref('');
//
const codeConfig = ref<CodeBlockContent | null>(null);
// select(ListState)
const state = reactive<ListState>({
codeList: [],
});
const id = computed(() => services?.codeBlockService.getId() || '');
const editable = computed(() => services?.codeBlockService.getEditStatus());
// id
const selectedIds = computed(() => services?.codeBlockService.getCombineIds() || []);
watchEffect(async () => {
codeConfig.value = cloneDeep(await services?.codeBlockService.getCodeContentById(id.value)) || null;
if (!codeConfig.value) return;
codeConfig.value.content = serializeConfig(codeConfig.value.content);
});
watchEffect(async () => {
const codeDsl = (await services?.codeBlockService.getCodeDslByIds(selectedIds.value)) || null;
if (!codeDsl) return;
state.codeList = [];
forIn(codeDsl, (value: CodeBlockContent, key: string) => {
state.codeList.push({
id: key,
name: value.name,
});
});
currentTitle.value = state.codeList[0]?.name || '';
});
const functionEditor = ref<InstanceType<typeof FunctionEditor>>();
const handleClose = async () => {
// codeDraftEditor
await functionEditor.value?.codeDraftEditor?.close();
};
</script>

View File

@ -1,97 +1,69 @@
<template>
<TMagicScrollbar class="m-editor-code-block-list m-editor-dep-list-panel">
<slot name="code-block-panel-header">
<div class="search-wrapper">
<SearchInput @search="filterTextChangeHandler"></SearchInput>
<TMagicButton class="create-code-button" type="primary" size="small" @click="createCodeBlock" v-if="editable"
>新增</TMagicButton
>
</div>
</slot>
<!-- 代码块列表 -->
<TMagicTree
ref="tree"
class="magic-editor-layer-tree"
node-key="id"
empty-text="暂无代码块"
:default-expanded-keys="expandedKeys"
:expand-on-click-node="false"
:data="codeList"
:props="treeProps"
:highlight-current="true"
:filter-node-method="filterNode"
@node-click="clickHandler"
>
<template #default="{ data }">
<div :id="data.id" class="list-container">
<div class="list-item">
<CodeIcon v-if="data.type === 'code'" class="codeIcon"></CodeIcon>
<AppManageIcon v-if="data.type === 'node'" class="compIcon"></AppManageIcon>
<span class="name" :class="{ code: data.type === 'code', hook: data.type === 'key' }"
>{{ data.name }} ({{ data.id }})</span
>
<!-- 右侧工具栏 -->
<div class="right-tool" v-if="data.type === 'code'">
<TMagicTooltip effect="dark" :content="editable ? '编辑' : '查看'" placement="bottom">
<Icon :icon="editable ? Edit : View" class="edit-icon" @click.stop="editCode(`${data.id}`)"></Icon>
</TMagicTooltip>
<TMagicTooltip effect="dark" content="删除" placement="bottom" v-if="editable">
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.id}`)"></Icon>
</TMagicTooltip>
<slot name="code-block-panel-tool" :id="data.id" :data="data.codeBlockContent"></slot>
</div>
<TMagicTree
ref="tree"
class="magic-editor-layer-tree"
node-key="id"
empty-text="暂无代码块"
:default-expanded-keys="expandedKeys"
:expand-on-click-node="false"
:data="codeList"
:props="{
children: 'children',
label: 'name',
value: 'id',
}"
:highlight-current="true"
:filter-node-method="filterNode"
@node-click="clickHandler"
>
<template #default="{ data }">
<div :id="data.id" class="list-container">
<div class="list-item">
<CodeIcon v-if="data.type === 'code'" class="codeIcon"></CodeIcon>
<AppManageIcon v-if="data.type === 'node'" class="compIcon"></AppManageIcon>
<span class="name" :class="{ code: data.type === 'code', hook: data.type === 'key' }"
>{{ data.name }} ({{ data.id }})</span
>
<!-- 右侧工具栏 -->
<div class="right-tool" v-if="data.type === 'code'">
<TMagicTooltip effect="dark" :content="editable ? '编辑' : '查看'" placement="bottom">
<Icon :icon="editable ? Edit : View" class="edit-icon" @click.stop="editCode(data.id)"></Icon>
</TMagicTooltip>
<TMagicTooltip v-if="editable" effect="dark" content="删除" placement="bottom">
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.id}`)"></Icon>
</TMagicTooltip>
<slot name="code-block-panel-tool" :id="data.id" :data="data.codeBlockContent"></slot>
</div>
</div>
</template>
</TMagicTree>
<!-- 代码块编辑区 -->
<CodeBlockEditor v-if="isShowCodeBlockEditor" :paramsColConfig="paramsColConfig">
<template #code-block-edit-panel-header="{ id }">
<slot name="code-block-edit-panel-header" :id="id"></slot>
</template>
</CodeBlockEditor>
</TMagicScrollbar>
</div>
</template>
</TMagicTree>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { computed, inject, ref } from 'vue';
import { Close, Edit, View } from '@element-plus/icons-vue';
import {
TMagicButton,
tMagicMessage,
tMagicMessageBox,
TMagicScrollbar,
TMagicTooltip,
TMagicTree,
} from '@tmagic/design';
import { ColumnConfig } from '@tmagic/form';
import { CodeBlockContent, Id } from '@tmagic/schema';
import { tMagicMessage, tMagicMessageBox, TMagicTooltip, TMagicTree } from '@tmagic/design';
import type { Id } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
import SearchInput from '@editor/components/SearchInput.vue';
import AppManageIcon from '@editor/icons/AppManageIcon.vue';
import CodeIcon from '@editor/icons/CodeIcon.vue';
import { CodeDeleteErrorType, CodeDslItem, Services } from '@editor/type';
import CodeBlockEditor from './CodeBlockEditor.vue';
defineOptions({
name: 'MEditorCodeBlockList',
});
const props = defineProps<{
customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
paramsColConfig?: ColumnConfig;
}>();
const treeProps = {
children: 'children',
label: 'name',
value: 'id',
};
const emit = defineEmits<{
edit: [id: string];
remove: [id: string];
}>();
const { codeBlockService, depService, editorService } = inject<Services>('services') || {};
@ -114,64 +86,10 @@ const codeList = computed(() =>
};
}),
);
//
const expandedKeys = computed(() => codeList.value.map((item) => item.id));
const editable = computed(() => codeBlockService?.getEditStatus());
//
const isShowCodeBlockEditor = computed(() => codeBlockService?.getCodeEditorShowStatus() || false);
//
const createCodeBlock = async () => {
if (!codeBlockService) {
tMagicMessage.error('新增代码块失败');
return;
}
const codeConfig: CodeBlockContent = {
name: '代码块',
content: `({app, params}) => {\n // place your code here\n}`,
params: [],
};
const id = await codeBlockService.getUniqueId();
await codeBlockService.setCodeDslById(id, codeConfig);
codeBlockService.setCodeEditorContent(true, id);
};
//
const editCode = async (key: Id) => {
codeBlockService?.setCodeEditorContent(true, key);
};
//
const deleteCode = async (key: Id) => {
const currentCode = codeList.value.find((codeItem) => codeItem.id === key);
const existBinds = Boolean(currentCode?.children.length);
const undeleteableList = codeBlockService?.getUndeletableList() || [];
if (!existBinds && !undeleteableList.includes(key)) {
await tMagicMessageBox.confirm('确定删除该代码块吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
//
codeBlockService?.deleteCodeDslByIds([key]);
} else {
if (typeof props.customError === 'function') {
props.customError(key, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
} else {
tMagicMessage.error('代码块删除失败');
}
}
};
const tree = ref<InstanceType<typeof TMagicTree>>();
const filterNode = (value: string, data: CodeDslItem): boolean => {
if (!value) {
return true;
@ -179,10 +97,6 @@ const filterNode = (value: string, data: CodeDslItem): boolean => {
return `${data.name}${data.id}`.toLocaleLowerCase().indexOf(value.toLocaleLowerCase()) !== -1;
};
const filterTextChangeHandler = (val: string) => {
tree.value?.filter(val);
};
//
const selectComp = (compId: Id) => {
const stage = editorService?.get('stage');
@ -197,4 +111,39 @@ const clickHandler = (data: any, node: any) => {
selectComp(node.parent.data.id);
}
};
//
const editCode = (id: string) => {
emit('edit', id);
};
const deleteCode = async (id: string) => {
const currentCode = codeList.value.find((codeItem) => codeItem.id === id);
const existBinds = Boolean(currentCode?.children.length);
const undeleteableList = codeBlockService?.getUndeletableList() || [];
if (!existBinds && !undeleteableList.includes(id)) {
await tMagicMessageBox.confirm('确定删除该代码块吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
//
emit('remove', id);
} else {
if (typeof props.customError === 'function') {
props.customError(id, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
} else {
tMagicMessage.error('代码块删除失败');
}
}
};
const tree = ref<InstanceType<typeof TMagicTree>>();
defineExpose({
filter(val: string) {
tree.value?.filter(val);
},
});
</script>

View File

@ -0,0 +1,64 @@
<template>
<TMagicScrollbar class="m-editor-code-block-list m-editor-dep-list-panel">
<slot name="code-block-panel-header">
<div class="search-wrapper">
<SearchInput @search="filterTextChangeHandler"></SearchInput>
<TMagicButton v-if="editable" class="create-code-button" type="primary" size="small" @click="createCodeBlock"
>新增</TMagicButton
>
</div>
</slot>
<!-- 代码块列表 -->
<CodeBlockList
ref="codeBlockList"
:custom-error="customError"
@edit="editCode"
@remove="deleteCode"
></CodeBlockList>
<!-- 代码块编辑区 -->
<CodeBlockEditor
v-if="codeConfig"
ref="codeBlockEditor"
:disabled="!editable"
:content="codeConfig"
@submit="submitCodeBlockHandler"
></CodeBlockEditor>
</TMagicScrollbar>
</template>
<script setup lang="ts">
import { computed, inject, ref } from 'vue';
import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
import type { Id } from '@tmagic/schema';
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
import SearchInput from '@editor/components/SearchInput.vue';
import type { CodeDeleteErrorType, Services } from '@editor/type';
import { useCodeBlockEdit } from '@editor/utils/use-code-block-edit';
import CodeBlockList from './CodeBlockList.vue';
defineOptions({
name: 'MEditorCodeBlockListPanel',
});
defineProps<{
customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
}>();
const { codeBlockService } = inject<Services>('services') || {};
const editable = computed(() => codeBlockService?.getEditStatus());
const { codeBlockEditor, codeConfig, editCode, deleteCode, createCodeBlock, submitCodeBlockHandler } =
useCodeBlockEdit(codeBlockService);
const codeBlockList = ref<InstanceType<typeof CodeBlockList>>();
const filterTextChangeHandler = (val: string) => {
codeBlockList.value?.filter(val);
};
</script>

View File

@ -1,29 +1,23 @@
<template>
<TMagicDrawer
v-model="visible"
<MFormDrawer
ref="fomDrawer"
label-width="80px"
:title="title"
:close-on-press-escape="true"
:append-to-body="true"
:show-close="true"
:close-on-click-modal="true"
:size="size"
>
<MForm ref="form" :config="dataSourceConfig" :init-values="initValues" @change="changeHandler"></MForm>
<template #footer>
<div>
<TMagicButton type="primary" @click="submitHandler">确定</TMagicButton>
<TMagicButton @click="hide">关闭</TMagicButton>
</div>
</template>
</TMagicDrawer>
:width="size"
:config="dataSourceConfig"
:values="initValues"
:disabled="disabled"
@change="changeHandler"
@submit="submitHandler"
@error="errorHandler"
></MFormDrawer>
</template>
<script setup lang="ts">
import { computed, inject, ref, watchEffect } from 'vue';
import { TMagicButton, TMagicDrawer, tMagicMessage } from '@tmagic/design';
import { MForm } from '@tmagic/form';
import { tMagicMessage } from '@tmagic/design';
import { MFormDrawer } from '@tmagic/form';
import type { Services } from '@editor/type';
@ -34,6 +28,7 @@ defineOptions({
const props = defineProps<{
title?: string;
values: any;
disabled: boolean;
}>();
const type = ref('base');
@ -46,7 +41,7 @@ const size = computed(() => globalThis.document.body.clientWidth - (services?.ui
const dataSourceConfig = computed(() => services?.dataSourceService.getFormConfig(type.value) || []);
const form = ref<InstanceType<typeof MForm>>();
const fomDrawer = ref<InstanceType<typeof MFormDrawer>>();
const initValues = ref({});
@ -63,26 +58,21 @@ const changeHandler = (value: Record<string, any>) => {
initValues.value = value;
};
const submitHandler = async () => {
try {
const values = await form.value?.submitForm();
emit('submit', values);
} catch (error: any) {
tMagicMessage.error(error.message);
}
const submitHandler = (values: any) => {
emit('submit', values);
};
const visible = ref(false);
const hide = () => {
visible.value = false;
const errorHandler = (error: any) => {
tMagicMessage.error(error.message);
};
defineExpose({
show() {
visible.value = true;
fomDrawer.value?.show();
},
hide,
hide() {
fomDrawer.value?.hide();
},
});
</script>

View File

@ -0,0 +1,111 @@
<template>
<TMagicTree
ref="tree"
class="magic-editor-layer-tree"
node-key="id"
empty-text="暂无代码块"
default-expand-all
:expand-on-click-node="false"
:data="list"
:highlight-current="true"
@node-click="clickHandler"
>
<template #default="{ data }">
<div :id="data.id" class="list-container">
<div class="list-item">
<Icon v-if="data.type === 'code'" class="codeIcon" :icon="Coin"></Icon>
<Icon v-if="data.type === 'node'" class="compIcon" :icon="Aim"></Icon>
<span class="name" :class="{ code: data.type === 'code', hook: data.type === 'key' }"
>{{ data.name }}{{ data.id }}</span
>
<!-- 右侧工具栏 -->
<div class="right-tool" v-if="data.type === 'code'">
<TMagicTooltip effect="dark" :content="editable ? '编辑' : '查看'" placement="bottom">
<Icon :icon="editable ? Edit : View" class="edit-icon" @click.stop="editHandler(`${data.id}`)"></Icon>
</TMagicTooltip>
<TMagicTooltip v-if="editable" effect="dark" content="删除" placement="bottom">
<Icon :icon="Close" class="edit-icon" @click.stop="removeHandler(`${data.id}`)"></Icon>
</TMagicTooltip>
<slot name="data-source-panel-tool" :id="data.id" :data="data.codeBlockContent"></slot>
</div>
</div>
</div>
</template>
</TMagicTree>
</template>
<script lang="ts" setup>
import { computed, inject, ref } from 'vue';
import { Aim, Close, Coin, Edit, View } from '@element-plus/icons-vue';
import { tMagicMessageBox, TMagicTooltip, TMagicTree } from '@tmagic/design';
import { Id } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
import type { Services } from '@editor/type';
defineOptions({
name: 'MEditorDataSourceList',
});
const emit = defineEmits<{
edit: [id: string];
remove: [id: string];
}>();
const { depService, editorService, dataSourceService } = inject<Services>('services') || {};
const editable = computed(() => dataSourceService?.get('editable') ?? true);
const list = computed(() =>
Object.values(depService?.targets['data-source'] || {}).map((target) => ({
id: target.id,
name: target.name,
type: 'code',
children: Object.entries(target.deps).map(([id, dep]) => ({
name: dep.name,
type: 'node',
id,
children: dep.keys.map((key) => ({ name: key, id: key, type: 'key' })),
})),
})),
);
const editHandler = (id: string) => {
emit('edit', id);
};
const removeHandler = async (id: string) => {
await tMagicMessageBox.confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
emit('remove', id);
};
//
const selectComp = (compId: Id) => {
const stage = editorService?.get('stage');
editorService?.select(compId);
stage?.select(compId);
};
const clickHandler = (data: any, node: any) => {
if (data.type === 'node') {
selectComp(data.id);
} else if (data.type === 'key') {
selectComp(node.parent.data.id);
}
};
const tree = ref<InstanceType<typeof TMagicTree>>();
defineExpose({
filter(val: string) {
debugger;
tree.value?.filter(val);
},
});
</script>

View File

@ -2,46 +2,15 @@
<TMagicScrollbar class="data-source-list-panel m-editor-dep-list-panel">
<div class="search-wrapper">
<SearchInput @search="filterTextChangeHandler"></SearchInput>
<TMagicButton type="primary" size="small" @click="addHandler">新增</TMagicButton>
<TMagicButton v-if="editable" type="primary" size="small" @click="addHandler">新增</TMagicButton>
</div>
<!-- 数据源列表 -->
<TMagicTree
ref="tree"
class="magic-editor-layer-tree"
node-key="id"
empty-text="暂无代码块"
default-expand-all
:expand-on-click-node="false"
:data="list"
:highlight-current="true"
@node-click="clickHandler"
>
<template #default="{ data }">
<div :id="data.id" class="list-container">
<div class="list-item">
<Icon v-if="data.type === 'code'" class="codeIcon" :icon="Coin"></Icon>
<Icon v-if="data.type === 'node'" class="compIcon" :icon="Aim"></Icon>
<span class="name" :class="{ code: data.type === 'code', hook: data.type === 'key' }"
>{{ data.name }}{{ data.id }}</span
>
<!-- 右侧工具栏 -->
<div class="right-tool" v-if="data.type === 'code'">
<TMagicTooltip effect="dark" content="编辑" placement="bottom">
<Icon class="edit-icon" :icon="Edit" @click.stop="editHandler(`${data.id}`)"></Icon>
</TMagicTooltip>
<TMagicTooltip effect="dark" content="删除" placement="bottom">
<Icon :icon="Close" class="edit-icon" @click.stop="removeHandler(`${data.id}`)"></Icon>
</TMagicTooltip>
<slot name="data-source-panel-tool" :id="data.id" :data="data.codeBlockContent"></slot>
</div>
</div>
</div>
</template>
</TMagicTree>
<DataSourceList @edit="editHandler" @remove="removeHandler"></DataSourceList>
<DataSourceConfigPanel
ref="editDialog"
:disabled="!editable"
:values="dataSourceValues"
:title="typeof dataSourceValues.id !== 'undefined' ? `编辑${dataSourceValues.title}` : '新增'"
@submit="submitDataSourceHandler"
@ -51,42 +20,28 @@
<script setup lang="ts" name="MEditorDataSourceListPanel">
import { computed, inject, ref } from 'vue';
import { Aim, Close, Coin, Edit } from '@element-plus/icons-vue';
import { TMagicButton, tMagicMessageBox, TMagicScrollbar, TMagicTooltip, TMagicTree } from '@tmagic/design';
import { DataSourceSchema, Id } from '@tmagic/schema';
import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
import type { DataSourceSchema } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
import SearchInput from '@editor/components/SearchInput.vue';
import type { Services } from '@editor/type';
import DataSourceConfigPanel from './DataSourceConfigPanel.vue';
import DataSourceList from './DataSourceList.vue';
defineOptions({
name: 'MEditorDataSourceListPanel',
});
const services = inject<Partial<Services>>('services', {});
const { dataSourceService, depService, editorService } = inject<Services>('services') || {};
const list = computed(() =>
Object.values(depService?.targets['data-source'] || {}).map((target) => ({
id: target.id,
name: target.name,
type: 'code',
children: Object.entries(target.deps).map(([id, dep]) => ({
name: dep.name,
type: 'node',
id,
children: dep.keys.map((key) => ({ name: key, id: key, type: 'key' })),
})),
})),
);
const { dataSourceService } = inject<Services>('services') || {};
const editDialog = ref<InstanceType<typeof DataSourceConfigPanel>>();
const dataSourceValues = ref<Record<string, any>>({});
const editable = computed(() => dataSourceService?.get('editable') ?? true);
const addHandler = () => {
if (!editDialog.value) return;
@ -96,7 +51,7 @@ const addHandler = () => {
};
const editHandler = (id: string) => {
if (!editDialog.value || !services) return;
if (!editDialog.value) return;
dataSourceValues.value = {
...dataSourceService?.getDataSourceById(id),
@ -105,19 +60,11 @@ const editHandler = (id: string) => {
editDialog.value.show();
};
const removeHandler = async (id: string) => {
await tMagicMessageBox.confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
const removeHandler = (id: string) => {
dataSourceService?.remove(id);
};
const submitDataSourceHandler = (value: DataSourceSchema) => {
if (!services) return;
if (value.id) {
dataSourceService?.update(value);
} else {
@ -127,24 +74,9 @@ const submitDataSourceHandler = (value: DataSourceSchema) => {
editDialog.value?.hide();
};
const tree = ref<InstanceType<typeof TMagicTree>>();
const dataSourceList = ref<InstanceType<typeof DataSourceList>>();
const filterTextChangeHandler = (val: string) => {
tree.value?.filter(val);
};
//
const selectComp = (compId: Id) => {
const stage = editorService?.get('stage');
editorService?.select(compId);
stage?.select(compId);
};
const clickHandler = (data: any, node: any) => {
if (data.type === 'node') {
selectComp(data.id);
} else if (data.type === 'key') {
selectComp(node.parent.data.id);
}
dataSourceList.value?.filter(val);
};
</script>

View File

@ -19,7 +19,8 @@
import { reactive } from 'vue';
import { keys, pick } from 'lodash-es';
import { CodeBlockContent, CodeBlockDSL, Id } from '@tmagic/schema';
import type { ColumnConfig } from '@tmagic/form';
import type { CodeBlockContent, CodeBlockDSL, Id } from '@tmagic/schema';
import type { CodeState } from '@editor/type';
import { CODE_DRAFT_STORAGE_KEY } from '@editor/type';
@ -29,23 +30,15 @@ import BaseService from './BaseService';
class CodeBlock extends BaseService {
private state = reactive<CodeState>({
isShowCodeEditor: false,
codeDsl: null,
id: '',
editable: true,
combineIds: [],
undeletableList: [],
paramsColConfig: undefined,
});
constructor() {
super([
'setCodeDslById',
'setCodeEditorShowStatus',
'setEditStatus',
'setCombineIds',
'setUndeleteableList',
'deleteCodeDslByIds',
]);
super(['setCodeDslById', 'setEditStatus', 'setCombineIds', 'setUndeleteableList', 'deleteCodeDslByIds']);
}
/**
@ -97,7 +90,10 @@ class CodeBlock extends BaseService {
if (codeConfig.content) {
// 在保存的时候转换代码内容
const parseDSL = getConfig('parseDSL');
codeConfigProcessed.content = parseDSL(codeConfig.content);
if (typeof codeConfig.content === 'string') {
codeConfig.content = parseDSL<(...args: any[]) => any>(codeConfig.content);
}
codeConfigProcessed.content = codeConfig.content;
}
const existContent = codeDsl[id] || {};
@ -120,43 +116,6 @@ class CodeBlock extends BaseService {
return pick(codeDsl, ids) as CodeBlockDSL;
}
/**
*
* @param {boolean} status
* @returns {void}
*/
public async setCodeEditorShowStatus(status: boolean): Promise<void> {
this.state.isShowCodeEditor = status;
}
/**
*
* @returns {boolean}
*/
public getCodeEditorShowStatus(): boolean {
return this.state.isShowCodeEditor;
}
/**
*
* @param {boolean} status
* @param {Id} id id
* @returns {void}
*/
public setCodeEditorContent(status: boolean, id: Id): void {
if (!id) return;
this.setId(id);
this.state.isShowCodeEditor = status;
}
/**
*
* @returns {CodeBlockContent | null}
*/
public getCurrentDsl(): CodeBlockContent | null {
return this.getCodeContentById(this.state.id);
}
/**
*
* @returns {boolean}
@ -174,24 +133,6 @@ class CodeBlock extends BaseService {
this.state.editable = status;
}
/**
* ID
* @param {Id} id id
* @returns {void}
*/
public setId(id: Id): void {
if (!id) return;
this.state.id = id;
}
/**
* ID
* @returns {Id} id id
*/
public getId(): Id {
return this.state.id;
}
/**
* id数组
* @param {string[]} ids id数组
@ -263,11 +204,19 @@ class CodeBlock extends BaseService {
});
}
public setParamsColConfig(config: ColumnConfig): void {
this.state.paramsColConfig = config;
}
public getParamsColConfig(): ColumnConfig | undefined {
return this.state.paramsColConfig;
}
/**
* id
* @returns {Id} id
*/
public async getUniqueId(): Promise<Id> {
public async getUniqueId(): Promise<string> {
const newId = `code_${Math.random().toString(10).substring(2).substring(0, 4)}`;
// 判断是否重复
const dsl = await this.getCodeDsl();
@ -277,9 +226,7 @@ class CodeBlock extends BaseService {
}
public resetState() {
this.state.isShowCodeEditor = false;
this.state.codeDsl = null;
this.state.id = '';
this.state.editable = true;
this.state.combineIds = [];
this.state.undeletableList = [];

View File

@ -11,6 +11,7 @@ import BaseService from './BaseService';
interface State {
dataSources: DataSourceSchema[];
editable: boolean;
configs: Record<string, FormConfig>;
}
@ -18,6 +19,7 @@ type StateKey = keyof State;
class DataSource extends BaseService {
private state = reactive<State>({
dataSources: [],
editable: true,
configs: {},
});

View File

@ -24,125 +24,3 @@
display: none;
}
}
.el-dialog.is-fullscreen.code-editor-dialog {
overflow: hidden;
}
.code-editor-dialog {
.tmagic-design-card {
.t-card__header {
display: block;
}
}
.el-dialog__body,
.t-dialog__body {
height: 90%;
padding-top: 10px;
}
.tmagic-design-card {
height: 100%;
background: #fff;
.el-card__body,
.t-card__body {
height: 100%;
background: #fff;
}
}
.code-editor-layout {
height: 100%;
border: 1px solid #e4e7ed;
.side-tree {
height: 100%;
overflow: auto;
.el-tree-node__label {
width: 100%;
}
.list-container {
width: 100%;
overflow: hidden;
margin-left: -10px;
.list-item {
width: 100%;
margin: 10px 0;
line-height: 30px;
.code-name {
width: 100%;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
}
}
.code-name-wrapper {
display: flex;
align-items: center;
font-size: 16px;
margin-bottom: 10px;
.code-name-label {
margin-right: 10px;
}
.code-name-input {
width: 300px;
}
}
.m-editor-code-block-editor-panel-list-mode {
height: 100%;
z-index: 10;
background: #fff;
.el-card {
border: 0;
}
}
.m-editor-code-block-editor-panel {
position: absolute;
top: 0;
left: 4px;
width: calc(100% - 9px);
height: 100%;
z-index: 10;
background: #fff;
.el-card {
border: 0;
}
}
.m-editor-wrapper {
height: 100%;
&.fullScreen {
height: 100%;
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 100;
}
.m-editor-container {
height: calc(100% - 45px);
}
.m-editor-content-bottom {
height: 45px;
width: 100%;
display: flex;
justify-content: end;
background: #fff;
position: absolute;
right: 20px;
bottom: 0;
> button {
height: 30px;
margin-top: 5px;
}
}
}
}

View File

@ -1,7 +1,32 @@
.magic-code-editor {
width: 100%;
}
.margin {
margin: 0;
.magic-code-editor-wrapper {
width: 100%;
height: 100%;
position: relative;
&.full-screen {
position: fixed;
z-index: 10000;
top: 0;
left: 0;
}
.magic-code-editor-content {
width: 100%;
height: 100%;
.margin {
margin: 0;
}
}
.magic-code-editor-full-screen-icon {
position: absolute;
top: 5px;
right: 0;
z-index: 1;
}
}

View File

@ -0,0 +1,13 @@
.m-editor-data-source-methods {
width: 100%;
.tmagic-design-table {
width: 100%;
}
.m-editor-data-source-methods-footer {
display: flex;
justify-content: flex-end;
margin-top: 15px;
}
}

View File

@ -19,18 +19,21 @@
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.12);
}
}
.m-fields-code-select-col {
.m-fields-code-select-col,
.m-fields-data-source-method-select {
width: 100%;
.code-select-container {
display: flex;
align-items: center;
.select {
flex: 10 0 100px;
}
.icon {
flex: 1 0 20px;
cursor: pointer;
margin-right: 5px;
}
}
.code-select-container,
.data-source-method-select-container {
display: flex;
align-items: center;
.select {
flex: 10 0 100px;
}
.icon {
flex: 1 0 20px;
cursor: pointer;
margin-right: 5px;
}
}

View File

@ -19,5 +19,6 @@
@import "./dep-list.scss";
@import "./data-source.scss";
@import "./data-source-fields.scss";
@import "./data-source-methods.scss";
@import "./data-source-input.scss";
@import "./key-value.scss";

View File

@ -18,7 +18,7 @@
import type { Component } from 'vue';
import type { FilterFunction, FormConfig, FormItem } from '@tmagic/form';
import type { ColumnConfig, FilterFunction, FormConfig, FormItem } from '@tmagic/form';
import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
import type StageCore from '@tmagic/stage';
import type {
@ -45,7 +45,7 @@ export type BeforeAdd = (config: MNode, parent: MContainer) => Promise<MNode> |
export type GetConfig = (config: FormConfig) => Promise<FormConfig> | FormConfig;
export interface InstallOptions {
parseDSL: (dsl: string) => MApp;
parseDSL: <T = any>(dsl: string) => T;
[key: string]: any;
}
@ -334,18 +334,15 @@ export interface ScrollViewerEvent {
}
export type CodeState = {
/** 是否展示代码块编辑区 */
isShowCodeEditor: boolean;
/** 代码块DSL数据源 */
codeDsl: CodeBlockDSL | null;
/** 当前选中的代码块id */
id: Id;
/** 代码块是否可编辑 */
editable: boolean;
/** list模式下左侧展示的代码列表 */
combineIds: string[];
/** 为业务逻辑预留的不可删除的代码块列表,由业务逻辑维护(如代码块上线后不可删除) */
undeletableList: Id[];
paramsColConfig?: ColumnConfig;
};
export type HookData = {
@ -429,6 +426,8 @@ export interface EventSelectConfig {
compActionConfig?: FormItem;
/** 联动代码配置 */
codeActionConfig?: FormItem;
/** 联动数据源配置 */
dataSourceActionConfig?: FormItem;
}
export enum KeyBindingCommand {
@ -493,3 +492,11 @@ export interface CodeSelectColConfig {
disabled?: boolean | FilterFunction;
display?: boolean | FilterFunction;
}
export interface DataSourceMethodSelectConfig {
type: 'data-source-method-select';
name: string;
labelWidth?: number | string;
disabled?: boolean | FilterFunction;
display?: boolean | FilterFunction;
}

View File

@ -17,6 +17,17 @@ const fillConfig = (config: FormConfig): FormConfig => [
},
],
},
{
type: 'panel',
title: '方法定义',
items: [
{
name: 'methods',
type: 'data-source-methods',
defaultValue: [],
},
],
},
];
export const getFormConfig = (type: string, configs: Record<string, FormConfig>): FormConfig => {

View File

@ -0,0 +1,84 @@
import { nextTick, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { tMagicMessage } from '@tmagic/design';
import type { CodeBlockContent } from '@tmagic/schema';
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
import type { CodeBlockService } from '@editor/services/codeBlock';
export const useCodeBlockEdit = (codeBlockService?: CodeBlockService) => {
const codeConfig = ref<CodeBlockContent>();
const codeId = ref<string>();
const codeBlockEditor = ref<InstanceType<typeof CodeBlockEditor>>();
// 新增代码块
const createCodeBlock = async () => {
if (!codeBlockService) {
tMagicMessage.error('新增代码块失败');
return;
}
codeConfig.value = {
name: '代码块',
content: `({app, params}) => {\n // place your code here\n}`,
params: [],
};
codeId.value = await codeBlockService.getUniqueId();
await nextTick();
codeBlockEditor.value?.show();
};
// 编辑代码块
const editCode = async (id: string) => {
const codeBlock = await codeBlockService?.getCodeContentById(id);
if (!codeBlock) {
tMagicMessage.error('获取代码块内容失败');
return;
}
let codeContent = codeBlock.content;
if (typeof codeContent !== 'string') {
codeContent = codeContent.toString();
}
codeConfig.value = {
...cloneDeep(codeBlock),
content: codeContent,
};
codeId.value = id;
await nextTick();
codeBlockEditor.value?.show();
};
// 删除代码块
const deleteCode = async (key: string) => {
codeBlockService?.deleteCodeDslByIds([key]);
};
const submitCodeBlockHandler = async (values: CodeBlockContent) => {
if (!codeId.value) return;
await codeBlockService?.setCodeDslById(codeId.value, values);
codeBlockEditor.value?.hide();
};
return {
codeId,
codeConfig,
codeBlockEditor,
createCodeBlock,
editCode,
deleteCode,
submitCodeBlockHandler,
};
};

View File

@ -0,0 +1,101 @@
import { nextTick, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { tMagicMessage } from '@tmagic/design';
import type { CodeBlockContent, DataSourceSchema } from '@tmagic/schema';
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
import { getConfig } from './config';
export const useDataSourceMethod = () => {
const codeConfig = ref<CodeBlockContent>();
const codeBlockEditor = ref<InstanceType<typeof CodeBlockEditor>>();
const dataSource = ref<DataSourceSchema>();
const dataSourceMethod = ref('');
return {
codeConfig,
codeBlockEditor,
createCode: async (model: DataSourceSchema) => {
codeConfig.value = {
name: '',
content: `({ params, dataSource, app }) => {\n // place your code here\n}`,
params: [],
};
await nextTick();
dataSource.value = model;
dataSourceMethod.value = '';
codeBlockEditor.value?.show();
},
editCode: async (model: DataSourceSchema, methodName: string) => {
const method = model.methods?.find((method) => method.name === methodName);
if (!method) {
tMagicMessage.error('获取数据源方法失败');
return;
}
let codeContent = method.content;
if (typeof codeContent !== 'string') {
codeContent = codeContent.toString();
}
codeConfig.value = {
...cloneDeep(method),
content: codeContent,
};
await nextTick();
dataSource.value = model;
dataSourceMethod.value = methodName;
codeBlockEditor.value?.show();
},
deleteCode: async (model: DataSourceSchema, methodName: string) => {
if (!model.methods) return;
const index = model.methods.findIndex((method) => method.name === methodName);
if (index === -1) {
return;
}
model.methods.splice(index, 1);
},
submitCode: (values: CodeBlockContent) => {
if (!dataSource.value) return;
if (!dataSource.value.methods) {
dataSource.value.methods = [];
}
if (values.content) {
// 在保存的时候转换代码内容
const parseDSL = getConfig('parseDSL');
if (typeof values.content === 'string') {
values.content = parseDSL<(...args: any[]) => any>(values.content);
}
}
if (dataSourceMethod.value) {
const index = dataSource.value.methods.findIndex((method) => method.name === dataSourceMethod.value);
dataSource.value.methods.splice(index, 1, values);
} else {
dataSource.value.methods.push(values);
}
codeBlockEditor.value?.hide();
},
};
};

View File

@ -118,10 +118,6 @@ const hasStep = computed(() => {
return false;
});
const cancel = () => {
dialogVisible.value = false;
};
const closeHandler = () => {
stepActive.value = 1;
emit('close');
@ -148,6 +144,18 @@ const changeHandler = (value: any) => {
emit('change', value);
};
const show = () => {
dialogVisible.value = true;
};
const hide = () => {
dialogVisible.value = false;
};
const cancel = () => {
hide();
};
defineExpose({
form,
saveFetch,
@ -155,5 +163,7 @@ defineExpose({
cancel,
save,
show,
hide,
});
</script>

View File

@ -0,0 +1,116 @@
<template>
<TMagicDrawer
class="m-form-drawer"
v-model="visible"
:title="title"
:close-on-press-escape="true"
:append-to-body="true"
:show-close="true"
:close-on-click-modal="true"
:size="width"
:zIndex="zIndex"
>
<div
v-if="visible"
class="m-drawer-body"
:style="`max-height: ${bodyHeight}; overflow-y: auto; overflow-x: hidden;`"
>
<Form
ref="form"
:size="size"
:disabled="disabled"
:config="config"
:init-values="values"
:parent-values="parentValues"
:label-width="labelWidth"
@change="changeHandler"
></Form>
<slot></slot>
</div>
<template #footer>
<TMagicRow class="dialog-footer">
<TMagicCol :span="12" style="text-align: left">
<div style="min-height: 1px">
<slot name="left"></slot>
</div>
</TMagicCol>
<TMagicCol :span="12">
<slot name="footer">
<TMagicButton @click="hide">关闭</TMagicButton>
<TMagicButton type="primary" @click="submitHandler" :loading="saveFetch">{{ confirmText }}</TMagicButton>
</slot>
</TMagicCol>
</TMagicRow>
</template>
</TMagicDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { TMagicButton, TMagicCol, TMagicDrawer, TMagicRow } from '@tmagic/design';
import Form from './Form.vue';
import type { FormConfig } from './schema';
defineOptions({
name: 'MFormDialog',
});
withDefaults(
defineProps<{
config?: FormConfig;
values?: Object;
parentValues?: Object;
width?: string | number;
labelWidth?: string;
disabled?: boolean;
title?: string;
zIndex?: number;
size?: 'small' | 'default' | 'large';
confirmText?: string;
}>(),
{
config: () => [],
values: () => ({}),
confirmText: '确定',
},
);
const emit = defineEmits(['close', 'submit', 'error', 'change']);
const form = ref<InstanceType<typeof Form>>();
const visible = ref(false);
const saveFetch = ref(false);
const bodyHeight = ref(`${document.body.clientHeight - 194}px`);
const submitHandler = async () => {
try {
const values = await form.value?.submitForm();
emit('submit', values);
} catch (e) {
emit('error', e);
}
};
const changeHandler = (value: any) => {
emit('change', value);
};
const show = () => {
visible.value = true;
};
const hide = () => {
visible.value = false;
};
defineExpose({
form,
saveFetch,
show,
hide,
});
</script>

View File

@ -2,8 +2,7 @@
<div
v-if="config"
:style="config.tip ? 'display: flex;align-items: baseline;' : ''"
:class="config.className"
class="m-form-container"
:class="`m-form-container m-container-${type || ''} ${config.className || ''}`"
>
<m-fields-hidden
v-if="type === 'hidden'"

View File

@ -56,6 +56,7 @@ export * from './utils/useAddField';
export { default as MForm } from './Form.vue';
export { default as MFormDialog } from './FormDialog.vue';
export { default as MFormDrawer } from './FormDrawer.vue';
export { default as MContainer } from './containers/Container.vue';
export { default as MFieldset } from './containers/Fieldset.vue';
export { default as MPanel } from './containers/Panel.vue';

View File

@ -0,0 +1,5 @@
.m-form-drawer {
.el-drawer__header {
margin: 0;
}
}

View File

@ -1,4 +1,5 @@
@use "./form-dialog.scss";
@use "./form-drawer.scss";
@use "./form.scss";
@use "./date-time.scss";
@use "./link.scss";

View File

@ -16,3 +16,9 @@
margin-left: 5px;
}
}
.m-container-panel {
&:not(:last-of-type) {
margin-bottom: 20px;
}
}

View File

@ -30,6 +30,8 @@ export enum ActionType {
COMP = 'comp',
/** 联动代码 */
CODE = 'code',
/** 数据源 */
DATA_SOURCE = 'data-source',
}
export interface DataSourceDeps {
@ -71,7 +73,16 @@ export interface CompItemConfig {
method: string;
}
export type EventActionItem = CompItemConfig | CodeItemConfig;
export interface DataSourceItemConfig {
/** 动作类型 */
actionType: ActionType;
/** [数据源id, 方法] */
dataSourceMethod: [string, string];
/** 代码参数 */
params?: object;
}
export type EventActionItem = CompItemConfig | CodeItemConfig | DataSourceItemConfig;
export interface MComponent {
/** 组件ID默认为${type}_${number}}形式, 如page_123 */
@ -124,9 +135,11 @@ export interface CodeBlockContent {
/** 代码块名称 */
name: string;
/** 代码块内容 */
content: any;
content: ((...args: any[]) => any) | string;
/** 参数定义 */
params: CodeParam[] | [];
/** 注释 */
desc?: string;
/** 扩展字段 */
[propName: string]: any;
}
@ -137,6 +150,7 @@ export interface CodeParam {
/** 扩展字段 */
[propName: string]: any;
}
export interface PastePosition {
left?: number;
top?: number;
@ -176,6 +190,8 @@ export interface DataSourceSchema {
description?: string;
/** 字段列表 */
fields: DataSchema[];
/** 方法列表 */
methods?: CodeBlockContent[];
/** 扩展字段 */
[key: string]: any;
}