feat(editor): 代码编辑交互优化

1、代码列表中代码块和组件区分不够清晰,查看按钮太靠边,开发模式下未对齐
2、代码编辑/查看弹窗希望可以点击蒙层或者esc键退出
3、代码块绑定到组件的地方和事件绑定UI统一
4、在代码绑定的地方需要支持查看或者编辑
Bug:
1、旧格式的事件联动删除到只剩最后一个时无法成功删除
This commit is contained in:
parisma 2023-04-18 15:44:18 +08:00 committed by roymondchen
parent abc6835786
commit 51dadabc2c
13 changed files with 185 additions and 169 deletions

View File

@ -25,7 +25,6 @@
</template>
<script lang="ts" setup name="MEditorCodeDraftEditor">
import { computed, inject, ref, watchEffect } from 'vue';
import type { Action } from 'element-plus';
import type * as monaco from 'monaco-editor';
import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
@ -93,7 +92,7 @@ const saveCodeDraft = async (codeValue: string) => {
return;
}
services?.codeBlockService.setCodeDraft(props.id, codeValue);
tMagicMessage.success(`代码草稿保存成功 ${datetimeFormatter(new Date())}`);
tMagicMessage.success(`代码草稿成功保存到本地 ${datetimeFormatter(new Date())}`);
};
//
@ -108,24 +107,23 @@ const saveAndClose = (): void => {
const close = async (): Promise<void> => {
const codeDraft = services?.codeBlockService.getCodeDraft(props.id);
if (codeDraft) {
tMagicMessageBox
.confirm('您有代码修改未保存,是否保存后再关闭?', '提示', {
try {
await tMagicMessageBox.confirm('您有代码修改未保存,是否保存后再关闭?', '提示', {
confirmButtonText: '保存并关闭',
cancelButtonText: '直接关闭',
type: 'warning',
distinguishCancelAndClose: true,
})
.then(async () => {
//
saveAndClose();
})
.catch((action: Action) => {
if (action === 'cancel') {
// 稿
services?.codeBlockService.removeCodeDraft(props.id);
emit('close');
}
});
//
saveAndClose();
} catch (action: any) {
if (action === 'cancel') {
// 稿
services?.codeBlockService.removeCodeDraft(props.id);
emit('close');
}
}
} else {
emit('close');
}
@ -138,4 +136,9 @@ const toggleFullScreen = (): void => {
codeEditor.value.focus();
}
};
defineExpose({
saveAndClose,
close,
});
</script>

View File

@ -1,21 +0,0 @@
<template>
<svg width="2em" height="2em" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8.94034 2.55798L6.09333 13.1832L7.05925 13.442L9.90626 2.8168L8.94034 2.55798Z"
fill="currentColor"
fill-opacity="0.9"
></path>
<path
d="M2.14982 8.00001L5.57495 11.4251L4.86784 12.1323L1.15987 8.42428C0.925551 8.18996 0.925553 7.81006 1.15987 7.57575L4.86784 3.86777L5.57495 4.57488L2.14982 8.00001Z"
fill="currentColor"
fill-opacity="0.9"
></path>
<path
d="M13.846 8.00001L10.4054 11.4016L11.1085 12.1127L14.8368 8.42668C15.0744 8.19183 15.0744 7.80819 14.8368 7.57333L11.1085 3.88732L10.4054 4.59845L13.846 8.00001Z"
fill="currentColor"
fill-opacity="0.9"
></path>
</svg>
</template>
<script lang="ts" setup name="MEditorCodeIcon"></script>

View File

@ -20,6 +20,7 @@
</div>
</template>
<CodeDraftEditor
ref="codeDraftEditor"
:id="id"
:content="codeContent"
:editable="editable"
@ -32,7 +33,7 @@
</TMagicCard>
</template>
<script lang="ts" setup name="MEditorFunctionEditor">
import { inject, provide, ref, watchEffect } from 'vue';
import { inject, ref, watchEffect } from 'vue';
import { cloneDeep } from 'lodash-es';
import { TMagicCard, TMagicInput, tMagicMessage } from '@tmagic/design';
@ -116,19 +117,12 @@ const tableConfig: TableConfig = {
],
};
const emit = defineEmits(['change', 'field-input']);
const services = inject<Services>('services');
const codeName = ref<string>('');
const codeContent = ref<string>('');
const evalRes = ref(true);
provide('mForm', {
$emit: emit,
setField: () => {},
});
const tableModel = ref<{ params: CodeParam[] }>();
watchEffect(() => {
codeName.value = props.name;
@ -169,7 +163,7 @@ const saveCode = async (codeValue: string): Promise<void> => {
content: codeValue,
params: tableModel.value?.params || [],
});
tMagicMessage.success('代码保存成功');
tMagicMessage.success('代码成功保存到本地');
// 稿
services?.codeBlockService.removeCodeDraft(props.id);
}
@ -187,4 +181,10 @@ const saveAndClose = async (codeValue: string): Promise<void> => {
const close = (): void => {
services?.codeBlockService.setCodeEditorShowStatus(false);
};
const codeDraftEditor = ref<InstanceType<typeof CodeDraftEditor>>();
defineExpose({
codeDraftEditor,
});
</script>

View File

@ -1,111 +1,43 @@
<template>
<div class="m-fields-code-select" :class="config.className">
<m-form-table
:config="tableConfig"
:model="model[name]"
name="hookData"
:enableToggleMode="false"
:prop="prop"
:size="size"
@change="changeHandler"
>
<template #operateCol="{ scope }">
<Icon
v-if="scope.row.codeId && config.editable"
:icon="editable ? Edit : View"
class="edit-icon"
@click="editCode(scope.row.codeId)"
></Icon>
</template>
</m-form-table>
<TMagicCard>
<m-form-container :config="codeConfig" :model="model[name]" @change="changeHandler"> </m-form-container>
</TMagicCard>
</div>
</template>
<script lang="ts" setup name="MEditorCodeSelect">
import { computed, defineEmits, defineProps, inject, watch } from 'vue';
import { Edit, View } from '@element-plus/icons-vue';
import { isEmpty, map } from 'lodash-es';
import { computed, defineEmits, defineProps, watch } from 'vue';
import { isEmpty } from 'lodash-es';
import { createValues, FormItem, FormState, TableConfig } from '@tmagic/form';
import { HookType, Id } from '@tmagic/schema';
import { TMagicCard } from '@tmagic/design';
import { HookType } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
import type { CodeParamStatement, HookData, Services } from '@editor/type';
const services = inject<Services>('services');
const mForm = inject<FormState>('mForm');
const emit = defineEmits(['change']);
const props = withDefaults(
defineProps<{
config: {
tableConfig?: TableConfig;
className?: string;
editable?: boolean;
};
model: any;
prop: string;
name: string;
size: 'small' | 'default' | 'large';
}>(),
{
config: () => ({
editable: true,
}),
},
{},
);
const codeDsl = computed(() => services?.codeBlockService.getCodeDsl());
const tableConfig = computed<FormItem>(() => {
const defaultConfig = {
dropSort: false,
enableFullscreen: false,
border: true,
operateColWidth: 60,
items: [
{
type: 'select',
label: '代码块',
name: 'codeId',
width: '200px',
options: () => {
if (codeDsl.value) {
return map(codeDsl.value, (value, key) => ({
text: `${value.name}${key}`,
label: `${value.name}${key}`,
value: key,
}));
}
return [];
},
onChange: (formState: any, codeId: Id, { model }: any) => {
// itemscodeIdmodelcodeIdparams
model.params = {};
},
},
{
name: 'params',
label: '参数',
defaultValue: {},
itemsFunction: (row: HookData) => {
const paramsConfig = getParamsConfig(row.codeId);
// 使createValues
if (!row.params || isEmpty(row.params)) {
createValues(mForm, paramsConfig, {}, row.params);
}
return paramsConfig;
},
},
],
};
return {
...defaultConfig,
...props.config.tableConfig,
};
});
const editable = computed(() => services?.codeBlockService.getEditStatus());
const codeConfig = computed(() => ({
type: 'group-list',
name: 'hookData',
enableToggleMode: false,
items: [
{
type: 'code-select-col',
},
],
}));
watch(
() => props.model[props.name],
@ -127,20 +59,4 @@ watch(
const changeHandler = async () => {
emit('change', props.model[props.name]);
};
const getParamsConfig = (codeId: Id): CodeParamStatement[] => {
if (!codeDsl.value) return [];
const paramStatements = codeDsl.value[codeId]?.params;
if (isEmpty(paramStatements)) return [];
return paramStatements.map((paramState: CodeParamStatement) => ({
labelWidth: '100px',
text: paramState.name,
inline: true,
...paramState,
}));
};
const editCode = (codeId: Id) => {
services?.codeBlockService.setCodeEditorContent(true, codeId);
};
</script>

View File

@ -1,7 +1,16 @@
<template>
<div class="m-fields-code-select-col">
<!-- 代码块下拉框 -->
<m-form-container :config="selectConfig" :model="model" @change="onParamsChangeHandler"></m-form-container>
<div class="code-select-container">
<!-- 代码块下拉框 -->
<m-form-container
class="select"
:config="selectConfig"
:model="model"
@change="onParamsChangeHandler"
></m-form-container>
<!-- 查看/编辑按钮 -->
<Icon class="icon" :icon="editable ? Edit : View" @click="editCode"></Icon>
</div>
<!-- 参数填写框 -->
<m-form-container :config="codeParamsConfig" :model="model" @change="onParamsChangeHandler"></m-form-container>
</div>
@ -9,11 +18,13 @@
<script lang="ts" setup name="MEditorCodeSelectCol">
import { computed, defineEmits, defineProps, inject, ref, watch } from 'vue';
import { Edit, View } from '@element-plus/icons-vue';
import { isEmpty, map } from 'lodash-es';
import { createValues, FieldsetConfig, FormState } from '@tmagic/form';
import { Id } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
import type { CodeParamStatement, Services } from '@editor/type';
const services = inject<Services>('services');
@ -31,6 +42,7 @@ const props = withDefaults(
{},
);
const codeDsl = computed(() => services?.codeBlockService.getCodeDsl());
const editable = computed(() => services?.codeBlockService.getEditStatus());
const codeParamsConfig = ref<FieldsetConfig>({
type: 'fieldset',
items: [],
@ -115,4 +127,9 @@ watch(
immediate: true,
},
);
//
const editCode = () => {
services?.codeBlockService.setCodeEditorContent(true, props.model.codeId);
};
</script>

View File

@ -1,13 +1,14 @@
<template>
<div class="m-fields-event-select">
<m-form-container
<m-form-table
v-if="isOldVersion"
ref="eventForm"
:size="props.size"
:model="model"
name="events"
:config="tableConfig"
@change="onChangeHandler"
></m-form-container>
></m-form-table>
<div v-else class="fullWidth">
<TMagicButton class="create-button" type="primary" size="small" @click="addEvent()">添加事件</TMagicButton>

View File

@ -0,0 +1,11 @@
<template>
<!-- app-manage cdn链接https://cloudcache.tencent-cloud.com/qcloud/ui/static/console_aside_v4/6638c8a5-2e7f-477a-83b1-a413a0e4ba39.svg -->
<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#7C878E" fill-rule="evenodd">
<path d="M15.3,5.5 L8,0 L0.7,5.5 L8,11 L15.3,5.5 Z M8,2.5 L12,5.5 L8,8.5 L4,5.5 L8,2.5 Z" fill-rule="nonzero" />
<path d="M8 13.5L2.3 9.2 0.7 10.5 8 16 15.3 10.5 13.7 9.2z" />
</g>
</svg>
</template>
<script lang="ts" setup name="MEditorAppManageIcon"></script>

View File

@ -0,0 +1,35 @@
<template>
<!-- 代码iconcdn链接https://cloudcache.tencent-cloud.com/qcloud/ui/static/government/0d463ed5-6407-4498-8865-3d05b5e70115.svg -->
<svg
width="32px"
height="32px"
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 -->
<title>gsd-icon-line-Universal-code</title>
<desc>Created with Sketch.</desc>
<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)">
<g id="icon/line/Universal/code" transform="translate(561.000000, 2356.000000)">
<g id="路径">
<mask id="mask-2" fill="white"><use xlink:href="#path-1"></use></mask>
<use id="蒙版" fill="#D8D8D8" opacity="0" xlink:href="#path-1"></use>
<path
d="M21.9284587,7.9482233 L29.8079004,15.827665 C29.9055315,15.9252961 29.9055315,16.0835874 29.8079004,16.1812184 L21.9284587,24.0606602 C21.8308276,24.1582912 21.6725364,24.1582912 21.5749053,24.0606602 L20.3374684,22.8232233 C20.2419143,22.7276698 20.2398813,22.5740096 20.331369,22.4759832 L20.3374687,22.4696702 L26.8027181,16.0044417 L20.3374687,9.53921328 C20.2398372,9.44158265 20.2398369,9.2832914 20.3374679,9.18566017 L21.5749053,7.9482233 C21.6725364,7.85059223 21.8308276,7.85059223 21.9284587,7.9482233 Z M10.3999684,7.9482233 L11.6374053,9.18566017 C11.7329594,9.28121371 11.7349925,9.43487387 11.6435048,9.53290029 L11.637405,9.53921328 L5.17215562,16.0044417 L11.637405,22.4696702 C11.7329593,22.5652236 11.7349926,22.7188837 11.643505,22.8169103 L11.6374053,22.8232233 L10.3999684,24.0606602 C10.3023374,24.1582912 10.1440461,24.1582912 10.046415,24.0606602 L2.1669733,16.1812184 C2.06934223,16.0835874 2.06934223,15.9252961 2.1669733,15.827665 L10.046415,7.9482233 C10.1440461,7.85059223 10.3023374,7.85059223 10.3999684,7.9482233 Z M17.2612532,9.29310422 L18.9262468,9.83189578 C19.0576112,9.87440526 19.1296423,10.0153579 19.0871328,10.1467222 L15.0848232,22.514807 C15.0423138,22.6461714 14.9013612,22.7182025 14.7699968,22.675693 L13.1050032,22.1369014 C12.9736388,22.0943919 12.9016077,21.9534393 12.9441172,21.822075 L16.9464268,9.45399022 C16.9889362,9.32262585 17.1298888,9.25059474 17.2612532,9.29310422 Z"
id="形状"
fill="#1D1F24"
mask="url(#mask-2)"
></path>
</g>
</g>
<g id="icon切图" transform="translate(226.000000, 1782.000000)"></g>
</g>
</g>
</svg>
</template>
<script lang="ts" setup name="MEditorCodeIcon"></script>

View File

@ -3,11 +3,12 @@
class="code-editor-dialog"
:model-value="true"
:title="currentTitle"
:close-on-press-escape="false"
:close-on-press-escape="true"
:append-to-body="true"
:show-close="false"
:close-on-click-modal="false"
:close-on-click-modal="true"
:size="size"
:before-close="handleClose"
>
<Layout v-model:left="left" :min-left="45" class="code-editor-layout">
<!-- 右侧区域 -->
@ -15,6 +16,7 @@
<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"
@ -84,4 +86,11 @@ watchEffect(async () => {
});
currentTitle.value = state.codeList[0]?.name || '';
});
const functionEditor = ref<InstanceType<typeof FunctionEditor>>();
const handleClose = async () => {
// codeDraftEditor
await functionEditor.value?.codeDraftEditor?.close();
};
</script>

View File

@ -25,8 +25,11 @@
<template #default="{ data }">
<div :id="data.id" class="list-container">
<div class="list-item">
<CodeIcon style="width: 15px; margin-right: 5px" v-if="data.type === 'code'"></CodeIcon>
<span class="code-name">{{ data.name }}{{ data.id }}</span>
<CodeIcon v-if="data.type === 'code'" class="codeIcon"></CodeIcon>
<AppManageIcon v-if="data.type === 'node'" class="compIcon"></AppManageIcon>
<span class="code-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">
@ -59,9 +62,10 @@ import { TMagicButton, tMagicMessage, TMagicScrollbar, TMagicTooltip, TMagicTree
import { ColumnConfig } from '@tmagic/form';
import { CodeBlockContent, Id } from '@tmagic/schema';
import CodeIcon from '@editor/components/CodeIcon.vue';
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';
@ -75,21 +79,25 @@ const { codeBlockService, depService, editorService } = inject<Services>('servic
//
const codeList = computed(() =>
Object.values(depService?.targets['code-block'] || {}).map((target) => ({
id: target.id,
name: target.name,
type: 'code',
codeBlockContent: codeBlockService?.getCodeContentById(target.id),
children: Object.entries(target.deps).map(([id, dep]) => ({
Object.values(depService?.targets['code-block'] || {}).map((target) => {
//
const compNodes = Object.entries(target.deps).map(([id, dep]) => ({
name: dep.name,
type: 'node',
id,
children: dep.keys.map((key) => ({ name: key, id: key, type: 'key' })),
})),
})),
}));
return {
id: target.id,
name: target.name,
type: 'code',
codeBlockContent: codeBlockService?.getCodeContentById(target.id),
children: compNodes,
};
}),
);
//
//
const expandedKeys = computed(() => codeList.value.map((item) => item.id));
const editable = computed(() => codeBlockService?.getEditStatus());

View File

@ -36,21 +36,45 @@
display: flex;
align-items: center;
width: 100%;
align-items: center;
.right-tool {
display: flex;
width: fit-content !important;
align-items: center;
margin-right: 10px;
.edit-icon {
margin: 0 5px;
}
}
.codeIcon {
width: 20px;
height: 20px;
margin-right: 5px;
}
.compIcon {
width: 15px;
height: 15px;
margin-right: 5px;
}
.code-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 0 !important;
flex: 1;
line-height: 18px;
line-height: 22px;
&.code {
color: #000;
}
&.hook {
color: #909399;
}
}
}
}
@ -58,9 +82,8 @@
.m-fields-code-select {
width: 100%;
.edit-icon {
cursor: pointer;
margin-right: 5px;
.el-card__header {
display: none;
}
}

View File

@ -21,4 +21,16 @@
}
.m-fields-code-select-col {
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;
}
}
}

View File

@ -29,10 +29,12 @@ export default {
{
name: 'age',
type: 'number',
tip: '年纪',
},
{
name: 'studentName',
type: 'text',
tip: '学生姓名',
},
],
},