mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-08-28 05:06:45 +08:00
feat(core,editor,data-source,form,schema): 新增数据源方法配置,支持事件联动数据源方法
This commit is contained in:
parent
1a546c326c
commit
2a0680c707
@ -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 {
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
141
packages/editor/src/components/CodeBlockEditor.vue
Normal file
141
packages/editor/src/components/CodeBlockEditor.vue
Normal 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>
|
@ -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>
|
@ -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);
|
||||
|
@ -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>
|
@ -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`);
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
147
packages/editor/src/fields/DataSourceMethodSelect.vue
Normal file
147
packages/editor/src/fields/DataSourceMethodSelect.vue
Normal 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 = {}) => {
|
||||
// 通过下拉框选择的codeId变化后修正model的值,避免写入其他codeId的params
|
||||
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>
|
103
packages/editor/src/fields/DataSourceMethods.vue
Normal file
103
packages/editor/src/fields/DataSourceMethods.vue
Normal 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>
|
@ -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,
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<!-- 代码icon,cdn链接: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)">
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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': {
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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 = [];
|
||||
|
@ -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: {},
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
13
packages/editor/src/theme/data-source-methods.scss
Normal file
13
packages/editor/src/theme/data-source-methods.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
84
packages/editor/src/utils/use-code-block-edit.ts
Normal file
84
packages/editor/src/utils/use-code-block-edit.ts
Normal 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,
|
||||
};
|
||||
};
|
101
packages/editor/src/utils/use-data-source-method.ts
Normal file
101
packages/editor/src/utils/use-data-source-method.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
};
|
@ -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>
|
||||
|
116
packages/form/src/FormDrawer.vue
Normal file
116
packages/form/src/FormDrawer.vue
Normal 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>
|
@ -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'"
|
||||
|
@ -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';
|
||||
|
5
packages/form/src/theme/form-drawer.scss
Normal file
5
packages/form/src/theme/form-drawer.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.m-form-drawer {
|
||||
.el-drawer__header {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
@use "./form-dialog.scss";
|
||||
@use "./form-drawer.scss";
|
||||
@use "./form.scss";
|
||||
@use "./date-time.scss";
|
||||
@use "./link.scss";
|
||||
|
@ -16,3 +16,9 @@
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.m-container-panel {
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user