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> </template>
<script lang="ts" setup name="MEditorCodeDraftEditor"> <script lang="ts" setup name="MEditorCodeDraftEditor">
import { computed, inject, ref, watchEffect } from 'vue'; import { computed, inject, ref, watchEffect } from 'vue';
import type { Action } from 'element-plus';
import type * as monaco from 'monaco-editor'; import type * as monaco from 'monaco-editor';
import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design'; import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
@ -93,7 +92,7 @@ const saveCodeDraft = async (codeValue: string) => {
return; return;
} }
services?.codeBlockService.setCodeDraft(props.id, codeValue); 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 close = async (): Promise<void> => {
const codeDraft = services?.codeBlockService.getCodeDraft(props.id); const codeDraft = services?.codeBlockService.getCodeDraft(props.id);
if (codeDraft) { if (codeDraft) {
tMagicMessageBox try {
.confirm('您有代码修改未保存,是否保存后再关闭?', '提示', { await tMagicMessageBox.confirm('您有代码修改未保存,是否保存后再关闭?', '提示', {
confirmButtonText: '保存并关闭', confirmButtonText: '保存并关闭',
cancelButtonText: '直接关闭', cancelButtonText: '直接关闭',
type: 'warning', type: 'warning',
distinguishCancelAndClose: true, 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 { } else {
emit('close'); emit('close');
} }
@ -138,4 +136,9 @@ const toggleFullScreen = (): void => {
codeEditor.value.focus(); codeEditor.value.focus();
} }
}; };
defineExpose({
saveAndClose,
close,
});
</script> </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> </div>
</template> </template>
<CodeDraftEditor <CodeDraftEditor
ref="codeDraftEditor"
:id="id" :id="id"
:content="codeContent" :content="codeContent"
:editable="editable" :editable="editable"
@ -32,7 +33,7 @@
</TMagicCard> </TMagicCard>
</template> </template>
<script lang="ts" setup name="MEditorFunctionEditor"> <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 { cloneDeep } from 'lodash-es';
import { TMagicCard, TMagicInput, tMagicMessage } from '@tmagic/design'; 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 services = inject<Services>('services');
const codeName = ref<string>(''); const codeName = ref<string>('');
const codeContent = ref<string>(''); const codeContent = ref<string>('');
const evalRes = ref(true); const evalRes = ref(true);
provide('mForm', {
$emit: emit,
setField: () => {},
});
const tableModel = ref<{ params: CodeParam[] }>(); const tableModel = ref<{ params: CodeParam[] }>();
watchEffect(() => { watchEffect(() => {
codeName.value = props.name; codeName.value = props.name;
@ -169,7 +163,7 @@ const saveCode = async (codeValue: string): Promise<void> => {
content: codeValue, content: codeValue,
params: tableModel.value?.params || [], params: tableModel.value?.params || [],
}); });
tMagicMessage.success('代码保存成功'); tMagicMessage.success('代码成功保存到本地');
// 稿 // 稿
services?.codeBlockService.removeCodeDraft(props.id); services?.codeBlockService.removeCodeDraft(props.id);
} }
@ -187,4 +181,10 @@ const saveAndClose = async (codeValue: string): Promise<void> => {
const close = (): void => { const close = (): void => {
services?.codeBlockService.setCodeEditorShowStatus(false); services?.codeBlockService.setCodeEditorShowStatus(false);
}; };
const codeDraftEditor = ref<InstanceType<typeof CodeDraftEditor>>();
defineExpose({
codeDraftEditor,
});
</script> </script>

View File

@ -1,111 +1,43 @@
<template> <template>
<div class="m-fields-code-select" :class="config.className"> <div class="m-fields-code-select" :class="config.className">
<m-form-table <TMagicCard>
:config="tableConfig" <m-form-container :config="codeConfig" :model="model[name]" @change="changeHandler"> </m-form-container>
:model="model[name]" </TMagicCard>
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>
</div> </div>
</template> </template>
<script lang="ts" setup name="MEditorCodeSelect"> <script lang="ts" setup name="MEditorCodeSelect">
import { computed, defineEmits, defineProps, inject, watch } from 'vue'; import { computed, defineEmits, defineProps, watch } from 'vue';
import { Edit, View } from '@element-plus/icons-vue'; import { isEmpty } from 'lodash-es';
import { isEmpty, map } from 'lodash-es';
import { createValues, FormItem, FormState, TableConfig } from '@tmagic/form'; import { TMagicCard } from '@tmagic/design';
import { HookType, Id } from '@tmagic/schema'; 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 emit = defineEmits(['change']);
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
config: { config: {
tableConfig?: TableConfig;
className?: string; className?: string;
editable?: boolean;
}; };
model: any; model: any;
prop: string; prop: string;
name: string; name: string;
size: 'small' | 'default' | 'large'; size: 'small' | 'default' | 'large';
}>(), }>(),
{ {},
config: () => ({
editable: true,
}),
},
); );
const codeDsl = computed(() => services?.codeBlockService.getCodeDsl()); const codeConfig = computed(() => ({
type: 'group-list',
const tableConfig = computed<FormItem>(() => { name: 'hookData',
const defaultConfig = { enableToggleMode: false,
dropSort: false, items: [
enableFullscreen: false, {
border: true, type: 'code-select-col',
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());
watch( watch(
() => props.model[props.name], () => props.model[props.name],
@ -127,20 +59,4 @@ watch(
const changeHandler = async () => { const changeHandler = async () => {
emit('change', props.model[props.name]); 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> </script>

View File

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

View File

@ -1,13 +1,14 @@
<template> <template>
<div class="m-fields-event-select"> <div class="m-fields-event-select">
<m-form-container <m-form-table
v-if="isOldVersion" v-if="isOldVersion"
ref="eventForm" ref="eventForm"
:size="props.size" :size="props.size"
:model="model" :model="model"
name="events"
:config="tableConfig" :config="tableConfig"
@change="onChangeHandler" @change="onChangeHandler"
></m-form-container> ></m-form-table>
<div v-else class="fullWidth"> <div v-else class="fullWidth">
<TMagicButton class="create-button" type="primary" size="small" @click="addEvent()">添加事件</TMagicButton> <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" class="code-editor-dialog"
:model-value="true" :model-value="true"
:title="currentTitle" :title="currentTitle"
:close-on-press-escape="false" :close-on-press-escape="true"
:append-to-body="true" :append-to-body="true"
:show-close="false" :show-close="false"
:close-on-click-modal="false" :close-on-click-modal="true"
:size="size" :size="size"
:before-close="handleClose"
> >
<Layout v-model:left="left" :min-left="45" class="code-editor-layout"> <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"> <div v-if="!isEmpty(codeConfig)" class="m-editor-code-block-editor-panel">
<slot name="code-block-edit-panel-header" :id="id"></slot> <slot name="code-block-edit-panel-header" :id="id"></slot>
<FunctionEditor <FunctionEditor
ref="functionEditor"
v-if="codeConfig" v-if="codeConfig"
:id="id" :id="id"
:name="codeConfig.name" :name="codeConfig.name"
@ -84,4 +86,11 @@ watchEffect(async () => {
}); });
currentTitle.value = state.codeList[0]?.name || ''; currentTitle.value = state.codeList[0]?.name || '';
}); });
const functionEditor = ref<InstanceType<typeof FunctionEditor>>();
const handleClose = async () => {
// codeDraftEditor
await functionEditor.value?.codeDraftEditor?.close();
};
</script> </script>

View File

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

View File

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

View File

@ -21,4 +21,16 @@
} }
.m-fields-code-select-col { .m-fields-code-select-col {
width: 100%; 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', name: 'age',
type: 'number', type: 'number',
tip: '年纪',
}, },
{ {
name: 'studentName', name: 'studentName',
type: 'text', type: 'text',
tip: '学生姓名',
}, },
], ],
}, },