<template>
|
<Layout>
|
<LayoutSider
|
:class="`left ${prefixCls}-sider`"
|
collapsible
|
collapsedWidth="0"
|
width="270"
|
:zeroWidthTriggerStyle="{
|
'margin-top': '-70px',
|
'background-color': 'gray',
|
}"
|
breakpoint="md"
|
>
|
<CollapseContainer title="基础控件">
|
<CollapseItem
|
:list="baseComponents"
|
:handleListPush="handleListPushDrag"
|
@add-attrs="handleAddAttrs"
|
@handle-list-push="handleListPush"
|
/>
|
</CollapseContainer>
|
<CollapseContainer title="自定义控件">
|
<CollapseItem
|
:list="customComponents"
|
@add-attrs="handleAddAttrs"
|
:handleListPush="handleListPushDrag"
|
@handle-list-push="handleListPush"
|
/>
|
</CollapseContainer>
|
<CollapseContainer title="布局控件">
|
<CollapseItem
|
:list="layoutComponents"
|
:handleListPush="handleListPushDrag"
|
@add-attrs="handleAddAttrs"
|
@handle-list-push="handleListPush"
|
/>
|
</CollapseContainer>
|
</LayoutSider>
|
<LayoutContent>
|
<Toolbar
|
@handle-open-json-modal="handleOpenModal(jsonModal!)"
|
@handle-open-import-json-modal="handleOpenModal(importJsonModal!)"
|
@handle-preview="handleOpenModal(eFormPreview!)"
|
@handle-preview2="handleOpenModal(eFormPreview2!)"
|
@handle-open-code-modal="handleOpenModal(codeModal!)"
|
@handle-clear-form-items="handleClearFormItems"
|
/>
|
<FormComponentPanel
|
:current-item="formConfig.currentItem"
|
:data="formConfig"
|
@handle-set-select-item="handleSetSelectItem"
|
/>
|
</LayoutContent>
|
<LayoutSider
|
:class="`right ${prefixCls}-sider`"
|
collapsible
|
:reverseArrow="true"
|
collapsedWidth="0"
|
width="270"
|
:zeroWidthTriggerStyle="{ 'margin-top': '-70px', 'background-color': 'gray' }"
|
breakpoint="lg"
|
>
|
<PropsPanel ref="propsPanel" :activeKey="formConfig.activeKey">
|
<template v-for="item of formConfig.schemas" #[`${item.component}Props`]="data">
|
<slot
|
:name="`${item.component}Props`"
|
v-bind="{ formItem: data, props: data.componentProps }"
|
></slot>
|
</template>
|
</PropsPanel>
|
</LayoutSider>
|
</Layout>
|
|
<JsonModal ref="jsonModal" />
|
<CodeModal ref="codeModal" />
|
<ImportJsonModal ref="importJsonModal" />
|
<VFormPreview ref="eFormPreview" :formConfig="formConfig" />
|
<VFormPreview2 ref="eFormPreview2" :formConfig="formConfig" />
|
</template>
|
|
<script lang="ts" setup>
|
import CollapseItem from './modules/CollapseItem.vue';
|
import FormComponentPanel from './modules/FormComponentPanel.vue';
|
import JsonModal from './components/JsonModal.vue';
|
import VFormPreview from '../VFormPreview/index.vue';
|
import VFormPreview2 from '../VFormPreview/useForm.vue';
|
|
import Toolbar from './modules/Toolbar.vue';
|
import PropsPanel from './modules/PropsPanel.vue';
|
import ImportJsonModal from './components/ImportJsonModal.vue';
|
import CodeModal from './components/CodeModal.vue';
|
|
import 'codemirror/mode/javascript/javascript';
|
|
import { ref, provide, Ref } from 'vue';
|
import { Layout, LayoutContent, LayoutSider } from 'ant-design-vue';
|
|
import { IVFormComponent, IFormConfig, PropsTabKey } from '../../typings/v-form-component';
|
import { formItemsForEach, generateKey } from '../../utils';
|
import { cloneDeep } from 'lodash-es';
|
import { baseComponents, customComponents, layoutComponents } from '../../core/formItemConfig';
|
import { useRefHistory, UseRefHistoryReturn } from '@vueuse/core';
|
import { globalConfigState } from './config/formItemPropsConfig';
|
import { IFormDesignMethods, IPropsPanel, IToolbarMethods } from '../../typings/form-type';
|
import { useDesign } from '@/hooks/web/useDesign';
|
|
import { CollapseContainer } from '@/components/Container';
|
|
defineProps({
|
title: {
|
type: String,
|
default: 'v-form-antd表单设计器',
|
},
|
});
|
const { prefixCls } = useDesign('form-design');
|
// 子组件实例
|
const propsPanel = ref<null | IPropsPanel>(null);
|
const jsonModal = ref<null | IToolbarMethods>(null);
|
const importJsonModal = ref<null | IToolbarMethods>(null);
|
const eFormPreview = ref<null | IToolbarMethods>(null);
|
const eFormPreview2 = ref<null | IToolbarMethods>(null);
|
|
const codeModal = ref<null | IToolbarMethods>(null);
|
|
const formModel = ref({});
|
// endregion
|
const formConfig = ref<IFormConfig>({
|
// 表单配置
|
schemas: [],
|
layout: 'horizontal',
|
labelLayout: 'flex',
|
labelWidth: 100,
|
labelCol: {},
|
wrapperCol: {},
|
currentItem: {
|
component: '',
|
componentProps: {},
|
},
|
activeKey: 1,
|
});
|
|
const setFormConfig = (config: IFormConfig) => {
|
//外部导入时,可能会缺少必要的信息。
|
config.schemas = config.schemas || [];
|
config.schemas.forEach((item) => {
|
item.colProps = item.colProps || { span: 24 };
|
item.componentProps = item.componentProps || {};
|
item.itemProps = item.itemProps || {};
|
});
|
formConfig.value = config as any;
|
};
|
// 获取历史记录,用于撤销和重构
|
const historyReturn = useRefHistory(formConfig as any, {
|
deep: true,
|
capacity: 20,
|
parse: (val: IFormConfig) => {
|
// 使用lodash.cloneDeep重新拷贝数据,把currentItem指向选中项
|
const formConfig = cloneDeep(val);
|
const { currentItem, schemas } = formConfig;
|
// 从formItems中查找选中项
|
|
const item = schemas && schemas.find((item) => item.key === currentItem?.key);
|
// 如果有,则赋值给当前项,如果没有,则切换属性面板
|
if (item) {
|
formConfig.currentItem = item;
|
}
|
return formConfig;
|
},
|
});
|
|
/**
|
* 选中表单项
|
* @param schema 当前选中的表单项
|
*/
|
const handleSetSelectItem = (schema: IVFormComponent) => {
|
formConfig.value.currentItem = schema as any;
|
handleChangePropsTabs(
|
schema.key ? (formConfig.value.activeKey! === 1 ? 2 : formConfig.value.activeKey!) : 1,
|
);
|
};
|
|
const setGlobalConfigState = (formItem: IVFormComponent) => {
|
formItem.colProps = formItem.colProps || {};
|
formItem.colProps.span = globalConfigState.span;
|
// console.log('setGlobalConfigState', formItem);
|
};
|
|
/**
|
* 添加属性
|
* @param schemas
|
* @param index
|
*/
|
const handleAddAttrs = (_formItems: IVFormComponent[], _index: number) => {};
|
|
const handleListPushDrag = (item: IVFormComponent) => {
|
const formItem = cloneDeep(item);
|
setGlobalConfigState(formItem);
|
generateKey(formItem);
|
|
return formItem;
|
};
|
/**
|
* 单击控件时添加到面板中
|
* @param item {IVFormComponent} 当前点击的组件
|
*/
|
const handleListPush = (item: IVFormComponent) => {
|
// console.log('handleListPush', item);
|
const formItem = cloneDeep(item);
|
setGlobalConfigState(formItem);
|
generateKey(formItem);
|
if (!formConfig.value.currentItem?.key) {
|
handleSetSelectItem(formItem);
|
formConfig.value.schemas && formConfig.value.schemas.push(formItem as any);
|
|
return;
|
}
|
handleCopy(formItem, false);
|
};
|
|
/**
|
* 复制表单项,如果表单项为栅格布局,则遍历所有自表单项重新生成key
|
* @param {IVFormComponent} formItem
|
* @return {IVFormComponent}
|
*/
|
const copyFormItem = (formItem: IVFormComponent) => {
|
const newFormItem = cloneDeep(formItem);
|
if (newFormItem.component === 'Grid') {
|
formItemsForEach([formItem], (item) => {
|
generateKey(item);
|
});
|
}
|
return newFormItem;
|
};
|
/**
|
* 复制或者添加表单,isCopy为true时则复制表单
|
* @param item {IVFormComponent} 当前点击的组件
|
* @param isCopy {boolean} 是否复制
|
*/
|
const handleCopy = (
|
item: IVFormComponent = formConfig.value.currentItem as IVFormComponent,
|
isCopy = true,
|
) => {
|
const key = formConfig.value.currentItem?.key;
|
/**
|
* 遍历当表单项配置,如果是复制,则复制一份表单项,如果不是复制,则直接添加到表单项中
|
* @param schemas
|
*/
|
const traverse = (schemas: IVFormComponent[]) => {
|
// 使用some遍历,找到目标后停止遍历
|
schemas.some((formItem: IVFormComponent, index: number) => {
|
if (formItem.key === key) {
|
// 判断是不是复制
|
isCopy
|
? schemas.splice(index, 0, copyFormItem(formItem))
|
: schemas.splice(index + 1, 0, item);
|
const event = {
|
newIndex: index + 1,
|
};
|
// 添加到表单项中
|
handleBeforeColAdd(event, schemas, isCopy);
|
return true;
|
}
|
if (['Grid', 'Tabs'].includes(formItem.component)) {
|
// 栅格布局
|
formItem.columns?.forEach((item) => {
|
traverse(item.children);
|
});
|
}
|
});
|
};
|
if (formConfig.value.schemas) {
|
traverse(formConfig.value.schemas as any);
|
}
|
};
|
|
/**
|
* 添加到表单中
|
* @param newIndex {object} 事件对象
|
* @param schemas {IVFormComponent[]} 表单项列表
|
* @param isCopy {boolean} 是否复制
|
*/
|
const handleBeforeColAdd = ({ newIndex }: any, schemas: IVFormComponent[], isCopy = false) => {
|
const item = schemas[newIndex];
|
isCopy && generateKey(item);
|
handleSetSelectItem(item);
|
};
|
|
/**
|
* 打开模态框
|
* @param Modal {IToolbarMethods}
|
*/
|
const handleOpenModal = (Modal: IToolbarMethods) => {
|
const config = cloneDeep(formConfig.value);
|
Modal?.showModal(config);
|
};
|
/**
|
* 切换属性面板
|
* @param key
|
*/
|
const handleChangePropsTabs = (key: PropsTabKey) => {
|
formConfig.value.activeKey = key;
|
};
|
/**
|
* 清空表单项列表
|
*/
|
const handleClearFormItems = () => {
|
formConfig.value.schemas = [];
|
handleSetSelectItem({ component: '' });
|
};
|
|
const setFormModel = (key, value) => (formModel.value[key] = value);
|
provide('formModel', formModel);
|
// 把祖先组件的方法项注入到子组件中,子组件可通过inject获取
|
provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel);
|
// region 注入给子组件的属性
|
// provide('currentItem', formConfig.value.currentItem)
|
|
// 把表单配置项注入到子组件中,子组件可通过inject获取,获取到的数据为响应式
|
provide<Ref<IFormConfig>>('formConfig', formConfig as any);
|
|
// 注入历史记录
|
provide<UseRefHistoryReturn<any, any>>('historyReturn', historyReturn);
|
|
// 把祖先组件的方法项注入到子组件中,子组件可通过inject获取
|
provide<IFormDesignMethods>('formDesignMethods', {
|
handleBeforeColAdd,
|
handleCopy,
|
handleListPush,
|
handleSetSelectItem,
|
handleAddAttrs,
|
setFormConfig,
|
});
|
|
// endregion
|
</script>
|
|
<style lang="less" scoped>
|
@prefix-cls: ~'@{namespace}-form-design';
|
|
[data-theme='dark'] {
|
.@{prefix-cls}-sider {
|
background-color: #1f1f1f;
|
}
|
}
|
|
[data-theme='light'] {
|
.@{prefix-cls}-sider {
|
background-color: #fff;
|
}
|
}
|
</style>
|