feat: 新增 ele infra 表单设计器模块
parent
c743932c5a
commit
ffc7e21d4a
|
@ -26,6 +26,8 @@
|
|||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@form-create/designer": "^3.2.6",
|
||||
"@form-create/element-ui": "^3.2.11",
|
||||
"@tinymce/tinymce-vue": "catalog:",
|
||||
"@vben/access": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useTitle } from '@vueuse/core';
|
|||
import { ElLoading } from 'element-plus';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
import { setupFormCreate } from '#/plugins/form-create';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import App from './app.vue';
|
||||
|
@ -58,6 +59,9 @@ async function bootstrap(namespace: string) {
|
|||
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
|
||||
// formCreate
|
||||
setupFormCreate(app);
|
||||
|
||||
// 动态更新标题
|
||||
watchEffect(() => {
|
||||
if (preferences.app.dynamicTitle) {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<!-- 数据字典 Select 选择器 -->
|
||||
<script lang="ts" setup>
|
||||
import type { DictSelectProps } from '../typing';
|
||||
|
||||
import { computed, useAttrs } from 'vue';
|
||||
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElOption,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { getDictObj, getIntDictOptions, getStrDictOptions } from '#/utils';
|
||||
|
||||
defineOptions({ name: 'DictSelect' });
|
||||
|
||||
const props = withDefaults(defineProps<DictSelectProps>(), {
|
||||
valueType: 'str',
|
||||
selectType: 'select',
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
// 获得字典配置
|
||||
// TODO @dhb:可以使用 getDictOptions 替代么?
|
||||
const getDictOptions = computed(() => {
|
||||
switch (props.valueType) {
|
||||
case 'bool': {
|
||||
return getDictObj(props.dictType, 'bool');
|
||||
}
|
||||
case 'int': {
|
||||
return getIntDictOptions(props.dictType);
|
||||
}
|
||||
case 'str': {
|
||||
return getStrDictOptions(props.dictType);
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElSelect v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
|
||||
<ElOption
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
:label="dict.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElRadioGroup v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
|
||||
<ElRadio
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
:key="index"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
<ElCheckboxGroup
|
||||
v-if="selectType === 'checkbox'"
|
||||
class="w-1/1"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<ElCheckbox
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
:key="index"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</template>
|
|
@ -0,0 +1,288 @@
|
|||
import type { ApiSelectProps } from '#/components/form-create/typing';
|
||||
|
||||
import { defineComponent, onMounted, ref, useAttrs } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElOption,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export const useApiSelect = (option: ApiSelectProps) => {
|
||||
return defineComponent({
|
||||
name: option.name,
|
||||
props: {
|
||||
// 选项标签
|
||||
labelField: {
|
||||
type: String,
|
||||
default: () => option.labelField ?? 'label',
|
||||
},
|
||||
// 选项的值
|
||||
valueField: {
|
||||
type: String,
|
||||
default: () => option.valueField ?? 'value',
|
||||
},
|
||||
// api 接口
|
||||
url: {
|
||||
type: String,
|
||||
default: () => option.url ?? '',
|
||||
},
|
||||
// 请求类型
|
||||
method: {
|
||||
type: String,
|
||||
default: 'GET',
|
||||
},
|
||||
// 选项解析函数
|
||||
parseFunc: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 请求参数
|
||||
data: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
|
||||
selectType: {
|
||||
type: String,
|
||||
default: 'select',
|
||||
},
|
||||
// 是否多选
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 是否远程搜索
|
||||
remote: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 远程搜索时携带的参数
|
||||
remoteField: {
|
||||
type: String,
|
||||
default: 'label',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const attrs = useAttrs();
|
||||
const options = ref<any[]>([]); // 下拉数据
|
||||
const loading = ref(false); // 是否正在从远程获取数据
|
||||
const queryParam = ref<any>(); // 当前输入的值
|
||||
const getOptions = async () => {
|
||||
options.value = [];
|
||||
// 接口选择器
|
||||
if (isEmpty(props.url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (props.method) {
|
||||
case 'GET': {
|
||||
let url: string = props.url;
|
||||
if (props.remote && queryParam.value !== undefined) {
|
||||
url = url.includes('?')
|
||||
? `${url}&${props.remoteField}=${queryParam.value}`
|
||||
: `${url}?${props.remoteField}=${queryParam.value}`;
|
||||
}
|
||||
parseOptions(await requestClient.get(url));
|
||||
break;
|
||||
}
|
||||
case 'POST': {
|
||||
const data: any = JSON.parse(props.data);
|
||||
if (props.remote) {
|
||||
data[props.remoteField] = queryParam.value;
|
||||
}
|
||||
parseOptions(await requestClient.post(props.url, data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function parseOptions(data: any) {
|
||||
// 情况一:如果有自定义解析函数优先使用自定义解析
|
||||
if (!isEmpty(props.parseFunc)) {
|
||||
options.value = parseFunc()?.(data);
|
||||
return;
|
||||
}
|
||||
// 情况二:返回的直接是一个列表
|
||||
if (Array.isArray(data)) {
|
||||
parseOptions0(data);
|
||||
return;
|
||||
}
|
||||
// 情况二:返回的是分页数据,尝试读取 list
|
||||
data = data.list;
|
||||
if (!!data && Array.isArray(data)) {
|
||||
parseOptions0(data);
|
||||
return;
|
||||
}
|
||||
// 情况三:不是 yudao-vue-pro 标准返回
|
||||
console.warn(
|
||||
`接口[${props.url}] 返回结果不是 yudao-vue-pro 标准返回建议采用自定义解析函数处理`,
|
||||
);
|
||||
}
|
||||
|
||||
function parseOptions0(data: any[]) {
|
||||
if (Array.isArray(data)) {
|
||||
options.value = data.map((item: any) => ({
|
||||
label: parseExpression(item, props.labelField),
|
||||
value: parseExpression(item, props.valueField),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
console.warn(`接口[${props.url}] 返回结果不是一个数组`);
|
||||
}
|
||||
|
||||
function parseFunc() {
|
||||
let parse: any = null;
|
||||
if (props.parseFunc) {
|
||||
// 解析字符串函数
|
||||
// eslint-disable-next-line no-new-func
|
||||
parse = new Function(`return ${props.parseFunc}`)();
|
||||
}
|
||||
return parse;
|
||||
}
|
||||
|
||||
function parseExpression(data: any, template: string) {
|
||||
// 检测是否使用了表达式
|
||||
if (!template.includes('${')) {
|
||||
return data[template];
|
||||
}
|
||||
// 正则表达式匹配模板字符串中的 ${...}
|
||||
const pattern = /\$\{([^}]*)\}/g;
|
||||
// 使用replace函数配合正则表达式和回调函数来进行替换
|
||||
return template.replaceAll(pattern, (_, expr) => {
|
||||
// expr 是匹配到的 ${} 内的表达式(这里是属性名),从 data 中获取对应的值
|
||||
const result = data[expr.trim()]; // 去除前后空白,以防用户输入带空格的属性名
|
||||
if (!result) {
|
||||
console.warn(
|
||||
`接口选择器选项模版[${template}][${expr.trim()}] 解析值失败结果为[${result}], 请检查属性名称是否存在于接口返回值中,存在则忽略此条!!!`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
const remoteMethod = async (query: any) => {
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
queryParam.value = query;
|
||||
await getOptions();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getOptions();
|
||||
});
|
||||
|
||||
const buildSelect = () => {
|
||||
if (props.multiple) {
|
||||
// fix:多写此步是为了解决 multiple 属性问题
|
||||
return (
|
||||
<ElSelect
|
||||
class="w-1/1"
|
||||
loading={loading.value}
|
||||
multiple
|
||||
{...attrs}
|
||||
// 远程搜索
|
||||
filterable={props.remote}
|
||||
remote={props.remote}
|
||||
{...(props.remote && { remoteMethod })}
|
||||
>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<ElOption key={index} label={item.label} value={item.value} />
|
||||
),
|
||||
)}
|
||||
</ElSelect>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ElSelect
|
||||
class="w-1/1"
|
||||
loading={loading.value}
|
||||
{...attrs}
|
||||
// 远程搜索
|
||||
filterable={props.remote}
|
||||
remote={props.remote}
|
||||
{...(props.remote && { remoteMethod })}
|
||||
>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<ElOption key={index} label={item.label} value={item.value} />
|
||||
),
|
||||
)}
|
||||
</ElSelect>
|
||||
);
|
||||
};
|
||||
const buildCheckbox = () => {
|
||||
if (isEmpty(options.value)) {
|
||||
options.value = [
|
||||
{ label: '选项1', value: '选项1' },
|
||||
{ label: '选项2', value: '选项2' },
|
||||
];
|
||||
}
|
||||
return (
|
||||
<ElCheckboxGroup class="w-1/1" {...attrs}>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<ElCheckbox key={index} label={item.value}>
|
||||
{item.label}
|
||||
</ElCheckbox>
|
||||
),
|
||||
)}
|
||||
</ElCheckboxGroup>
|
||||
);
|
||||
};
|
||||
const buildRadio = () => {
|
||||
if (isEmpty(options.value)) {
|
||||
options.value = [
|
||||
{ label: '选项1', value: '选项1' },
|
||||
{ label: '选项2', value: '选项2' },
|
||||
];
|
||||
}
|
||||
return (
|
||||
<ElRadioGroup class="w-1/1" {...attrs}>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<ElRadio key={index} label={item.value}>
|
||||
{item.label}
|
||||
</ElRadio>
|
||||
),
|
||||
)}
|
||||
</ElRadioGroup>
|
||||
);
|
||||
};
|
||||
return () => (
|
||||
<>
|
||||
{(() => {
|
||||
switch (props.selectType) {
|
||||
case 'checkbox': {
|
||||
return buildCheckbox();
|
||||
}
|
||||
case 'radio': {
|
||||
return buildRadio();
|
||||
}
|
||||
case 'select': {
|
||||
return buildSelect();
|
||||
}
|
||||
default: {
|
||||
return buildSelect();
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { defineComponent } from 'vue';
|
||||
|
||||
import ImageUpload from '#/components/upload/image-upload.vue';
|
||||
|
||||
export const useImagesUpload = () => {
|
||||
return defineComponent({
|
||||
name: 'ImagesUpload',
|
||||
props: {
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
maxNumber: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
// TODO: @dhb52 其实还是靠 props 默认参数起作用,没能从 formCreate 传递
|
||||
return (props: { maxNumber?: number; multiple?: boolean }) => (
|
||||
<ImageUpload maxNumber={props.maxNumber} multiple={props.multiple} />
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
import type { Ref } from 'vue';
|
||||
|
||||
import type { Menu } from '#/components/form-create/typing';
|
||||
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
|
||||
import { apiSelectRule } from '#/components/form-create/rules/data';
|
||||
|
||||
import {
|
||||
useDictSelectRule,
|
||||
useEditorRule,
|
||||
useSelectRule,
|
||||
useUploadFileRule,
|
||||
useUploadImageRule,
|
||||
useUploadImagesRule,
|
||||
} from './rules';
|
||||
|
||||
export function makeRequiredRule() {
|
||||
return {
|
||||
type: 'Required',
|
||||
field: 'formCreate$required',
|
||||
title: '是否必填',
|
||||
};
|
||||
}
|
||||
|
||||
export const localeProps = (
|
||||
t: (msg: string) => any,
|
||||
prefix: string,
|
||||
rules: any[],
|
||||
) => {
|
||||
return rules.map((rule: { field: string; title: any }) => {
|
||||
if (rule.field === 'formCreate$required') {
|
||||
rule.title = t('props.required') || rule.title;
|
||||
} else if (rule.field && rule.field !== '_optionType') {
|
||||
rule.title = t(`components.${prefix}.${rule.field}`) || rule.title;
|
||||
}
|
||||
return rule;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析表单组件的 field, title 等字段(递归,如果组件包含子组件)
|
||||
*
|
||||
* @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule
|
||||
* @param fields 解析后表单组件字段
|
||||
* @param parentTitle 如果是子表单,子表单的标题,默认为空
|
||||
*/
|
||||
export const parseFormFields = (
|
||||
rule: Record<string, any>,
|
||||
fields: Array<Record<string, any>> = [],
|
||||
parentTitle: string = '',
|
||||
) => {
|
||||
const { type, field, $required, title: tempTitle, children } = rule;
|
||||
if (field && tempTitle) {
|
||||
let title = tempTitle;
|
||||
if (parentTitle) {
|
||||
title = `${parentTitle}.${tempTitle}`;
|
||||
}
|
||||
let required = false;
|
||||
if ($required) {
|
||||
required = true;
|
||||
}
|
||||
fields.push({
|
||||
field,
|
||||
title,
|
||||
type,
|
||||
required,
|
||||
});
|
||||
// TODO 子表单 需要处理子表单字段
|
||||
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
|
||||
// // 解析子表单的字段
|
||||
// rule.props.rule.forEach((item) => {
|
||||
// parseFields(item, fieldsPermission, title)
|
||||
// })
|
||||
// }
|
||||
}
|
||||
if (children && Array.isArray(children)) {
|
||||
children.forEach((rule) => {
|
||||
parseFormFields(rule, fields);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单设计器增强 hook
|
||||
* 新增
|
||||
* - 文件上传
|
||||
* - 单图上传
|
||||
* - 多图上传
|
||||
* - 字典选择器
|
||||
* - 用户选择器
|
||||
* - 部门选择器
|
||||
* - 富文本
|
||||
*/
|
||||
export const useFormCreateDesigner = async (designer: Ref) => {
|
||||
const editorRule = useEditorRule();
|
||||
const uploadFileRule = useUploadFileRule();
|
||||
const uploadImageRule = useUploadImageRule();
|
||||
const uploadImagesRule = useUploadImagesRule();
|
||||
|
||||
/**
|
||||
* 构建表单组件
|
||||
*/
|
||||
const buildFormComponents = () => {
|
||||
// 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
|
||||
designer.value?.removeMenuItem('upload');
|
||||
// 移除自带的富文本组件规则,使用 editorRule 替代
|
||||
designer.value?.removeMenuItem('fc-editor');
|
||||
const components = [
|
||||
editorRule,
|
||||
uploadFileRule,
|
||||
uploadImageRule,
|
||||
uploadImagesRule,
|
||||
];
|
||||
components.forEach((component) => {
|
||||
// 插入组件规则
|
||||
designer.value?.addComponent(component);
|
||||
// 插入拖拽按钮到 `main` 分类下
|
||||
designer.value?.appendMenuItem('main', {
|
||||
icon: component.icon,
|
||||
name: component.name,
|
||||
label: component.label,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const userSelectRule = useSelectRule({
|
||||
name: 'UserSelect',
|
||||
label: '用户选择器',
|
||||
icon: 'icon-eye',
|
||||
});
|
||||
const deptSelectRule = useSelectRule({
|
||||
name: 'DeptSelect',
|
||||
label: '部门选择器',
|
||||
icon: 'icon-tree',
|
||||
});
|
||||
const dictSelectRule = useDictSelectRule();
|
||||
const apiSelectRule0 = useSelectRule({
|
||||
name: 'ApiSelect',
|
||||
label: '接口选择器',
|
||||
icon: 'icon-json',
|
||||
props: [...apiSelectRule],
|
||||
event: ['click', 'change', 'visibleChange', 'clear', 'blur', 'focus'],
|
||||
});
|
||||
|
||||
/**
|
||||
* 构建系统字段菜单
|
||||
*/
|
||||
const buildSystemMenu = () => {
|
||||
// 移除自带的下拉选择器组件,使用 currencySelectRule 替代
|
||||
// designer.value?.removeMenuItem('select')
|
||||
// designer.value?.removeMenuItem('radio')
|
||||
// designer.value?.removeMenuItem('checkbox')
|
||||
const components = [
|
||||
userSelectRule,
|
||||
deptSelectRule,
|
||||
dictSelectRule,
|
||||
apiSelectRule0,
|
||||
];
|
||||
const menu: Menu = {
|
||||
name: 'system',
|
||||
title: '系统字段',
|
||||
list: components.map((component) => {
|
||||
// 插入组件规则
|
||||
designer.value?.addComponent(component);
|
||||
// 插入拖拽按钮到 `system` 分类下
|
||||
return {
|
||||
icon: component.icon,
|
||||
name: component.name,
|
||||
label: component.label,
|
||||
};
|
||||
}),
|
||||
};
|
||||
designer.value?.addMenu(menu);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
buildFormComponents();
|
||||
buildSystemMenu();
|
||||
});
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export { useApiSelect } from './components/use-api-select';
|
||||
|
||||
export { useFormCreateDesigner } from './helpers';
|
|
@ -0,0 +1,182 @@
|
|||
/* eslint-disable no-template-curly-in-string */
|
||||
const selectRule = [
|
||||
{
|
||||
type: 'select',
|
||||
field: 'selectType',
|
||||
title: '选择器类型',
|
||||
value: 'select',
|
||||
options: [
|
||||
{ label: '下拉框', value: 'select' },
|
||||
{ label: '单选框', value: 'radio' },
|
||||
{ label: '多选框', value: 'checkbox' },
|
||||
],
|
||||
// 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性
|
||||
control: [
|
||||
{
|
||||
value: 'select',
|
||||
condition: '==',
|
||||
method: 'hidden',
|
||||
rule: [
|
||||
'multiple',
|
||||
'clearable',
|
||||
'collapseTags',
|
||||
'multipleLimit',
|
||||
'allowCreate',
|
||||
'filterable',
|
||||
'noMatchText',
|
||||
'remote',
|
||||
'remoteMethod',
|
||||
'reserveKeyword',
|
||||
'defaultFirstOption',
|
||||
'automaticDropdown',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'filterable',
|
||||
title: '是否可搜索',
|
||||
},
|
||||
{ type: 'switch', field: 'multiple', title: '是否多选' },
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否禁用',
|
||||
},
|
||||
{ type: 'switch', field: 'clearable', title: '是否可以清空选项' },
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'collapseTags',
|
||||
title: '多选时是否将选中值按文字的形式展示',
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'multipleLimit',
|
||||
title: '多选时用户最多可以选择的项目数,为 0 则不限制',
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'autocomplete',
|
||||
title: 'autocomplete 属性',
|
||||
},
|
||||
{ type: 'input', field: 'placeholder', title: '占位符' },
|
||||
{ type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
|
||||
{
|
||||
type: 'input',
|
||||
field: 'noMatchText',
|
||||
title: '搜索条件无匹配时显示的文字',
|
||||
},
|
||||
{ type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'reserveKeyword',
|
||||
title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'defaultFirstOption',
|
||||
title: '在输入框按下回车,选择第一个匹配项',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'popperAppendToBody',
|
||||
title: '是否将弹出框插入至 body 元素',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'automaticDropdown',
|
||||
title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单',
|
||||
},
|
||||
];
|
||||
|
||||
const apiSelectRule = [
|
||||
{
|
||||
type: 'input',
|
||||
field: 'url',
|
||||
title: 'url 地址',
|
||||
props: {
|
||||
placeholder: '/system/user/simple-list',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'method',
|
||||
title: '请求类型',
|
||||
value: 'GET',
|
||||
options: [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
],
|
||||
control: [
|
||||
{
|
||||
value: 'GET',
|
||||
condition: '!=',
|
||||
method: 'hidden',
|
||||
rule: [
|
||||
{
|
||||
type: 'input',
|
||||
field: 'data',
|
||||
title: '请求参数 JSON 格式',
|
||||
props: {
|
||||
autosize: true,
|
||||
type: 'textarea',
|
||||
placeholder: '{"type": 1}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'labelField',
|
||||
title: 'label 属性',
|
||||
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
|
||||
props: {
|
||||
placeholder: 'nickname',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'valueField',
|
||||
title: 'value 属性',
|
||||
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
|
||||
props: {
|
||||
placeholder: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'parseFunc',
|
||||
title: '选项解析函数',
|
||||
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
|
||||
(data: any)=>{ label: string; value: any }[]`,
|
||||
props: {
|
||||
autosize: true,
|
||||
rows: { minRows: 2, maxRows: 6 },
|
||||
type: 'textarea',
|
||||
placeholder: `
|
||||
function (data) {
|
||||
console.log(data)
|
||||
return data.list.map(item=> ({label: item.nickname,value: item.id}))
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'remote',
|
||||
info: '是否可搜索',
|
||||
title: '其中的选项是否从服务器远程加载',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'remoteField',
|
||||
title: '请求参数',
|
||||
info: '远程请求时请求携带的参数名称,如:name',
|
||||
},
|
||||
];
|
||||
|
||||
export { apiSelectRule, selectRule };
|
|
@ -0,0 +1,6 @@
|
|||
export { useDictSelectRule } from './use-dict-select';
|
||||
export { useEditorRule } from './use-editor-rule';
|
||||
export { useSelectRule } from './use-select-rule';
|
||||
export { useUploadFileRule } from './use-upload-file-rule';
|
||||
export { useUploadImageRule } from './use-upload-image-rule';
|
||||
export { useUploadImagesRule } from './use-upload-images-rule';
|
|
@ -0,0 +1,69 @@
|
|||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { buildUUID, cloneDeep } from '@vben/utils';
|
||||
|
||||
import * as DictDataApi from '#/api/system/dict/type';
|
||||
import {
|
||||
localeProps,
|
||||
makeRequiredRule,
|
||||
} from '#/components/form-create/helpers';
|
||||
import { selectRule } from '#/components/form-create/rules/data';
|
||||
|
||||
/**
|
||||
* 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule
|
||||
*/
|
||||
export const useDictSelectRule = () => {
|
||||
const label = '字典选择器';
|
||||
const name = 'DictSelect';
|
||||
const rules = cloneDeep(selectRule);
|
||||
const dictOptions = ref<{ label: string; value: string }[]>([]); // 字典类型下拉数据
|
||||
onMounted(async () => {
|
||||
const data = await DictDataApi.getSimpleDictTypeList();
|
||||
if (!data || data.length === 0) {
|
||||
return;
|
||||
}
|
||||
dictOptions.value =
|
||||
data?.map((item: DictDataApi.SystemDictTypeApi.DictType) => ({
|
||||
label: item.name,
|
||||
value: item.type,
|
||||
})) ?? [];
|
||||
});
|
||||
return {
|
||||
icon: 'icon-descriptions',
|
||||
label,
|
||||
name,
|
||||
rule() {
|
||||
return {
|
||||
type: name,
|
||||
field: buildUUID(),
|
||||
title: label,
|
||||
info: '',
|
||||
$required: false,
|
||||
};
|
||||
},
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'select',
|
||||
field: 'dictType',
|
||||
title: '字典类型',
|
||||
value: '',
|
||||
options: dictOptions.value,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'valueType',
|
||||
title: '字典值类型',
|
||||
value: 'str',
|
||||
options: [
|
||||
{ label: '数字', value: 'int' },
|
||||
{ label: '字符串', value: 'str' },
|
||||
{ label: '布尔值', value: 'bool' },
|
||||
],
|
||||
},
|
||||
...rules,
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
import { buildUUID } from '@vben/utils';
|
||||
|
||||
import {
|
||||
localeProps,
|
||||
makeRequiredRule,
|
||||
} from '#/components/form-create/helpers';
|
||||
|
||||
export const useEditorRule = () => {
|
||||
const label = '富文本';
|
||||
const name = 'Tinymce';
|
||||
return {
|
||||
icon: 'icon-editor',
|
||||
label,
|
||||
name,
|
||||
rule() {
|
||||
return {
|
||||
type: name,
|
||||
field: buildUUID(),
|
||||
title: label,
|
||||
info: '',
|
||||
$required: false,
|
||||
};
|
||||
},
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '高度',
|
||||
},
|
||||
{ type: 'switch', field: 'readonly', title: '是否只读' },
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
import type { SelectRuleOption } from '#/components/form-create/typing';
|
||||
|
||||
import { buildUUID, cloneDeep } from '@vben/utils';
|
||||
|
||||
import {
|
||||
localeProps,
|
||||
makeRequiredRule,
|
||||
} from '#/components/form-create/helpers';
|
||||
import { selectRule } from '#/components/form-create/rules/data';
|
||||
|
||||
/**
|
||||
* 通用选择器规则 hook
|
||||
*
|
||||
* @param option 规则配置
|
||||
*/
|
||||
export const useSelectRule = (option: SelectRuleOption) => {
|
||||
const label = option.label;
|
||||
const name = option.name;
|
||||
const rules = cloneDeep(selectRule);
|
||||
return {
|
||||
icon: option.icon,
|
||||
label,
|
||||
name,
|
||||
event: option.event,
|
||||
rule() {
|
||||
return {
|
||||
type: name,
|
||||
field: buildUUID(),
|
||||
title: label,
|
||||
info: '',
|
||||
$required: false,
|
||||
};
|
||||
},
|
||||
props(_: any, { t }: any) {
|
||||
if (!option.props) {
|
||||
option.props = [];
|
||||
}
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
...option.props,
|
||||
...rules,
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
import { buildUUID } from '@vben/utils';
|
||||
|
||||
import {
|
||||
localeProps,
|
||||
makeRequiredRule,
|
||||
} from '#/components/form-create/helpers';
|
||||
|
||||
export const useUploadFileRule = () => {
|
||||
const label = '文件上传';
|
||||
const name = 'FileUpload';
|
||||
return {
|
||||
icon: 'icon-upload',
|
||||
label,
|
||||
name,
|
||||
rule() {
|
||||
return {
|
||||
type: name,
|
||||
field: buildUUID(),
|
||||
title: label,
|
||||
info: '',
|
||||
$required: false,
|
||||
};
|
||||
},
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
title: '文件类型',
|
||||
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
|
||||
options: [
|
||||
{ label: 'doc', value: 'doc' },
|
||||
{ label: 'xls', value: 'xls' },
|
||||
{ label: 'ppt', value: 'ppt' },
|
||||
{ label: 'txt', value: 'txt' },
|
||||
{ label: 'pdf', value: 'pdf' },
|
||||
],
|
||||
props: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'autoUpload',
|
||||
title: '是否在选取文件后立即进行上传',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'isShowTip',
|
||||
title: '是否显示提示',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'limit',
|
||||
title: '数量限制',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否禁用',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
import { buildUUID } from '@vben/utils';
|
||||
|
||||
import {
|
||||
localeProps,
|
||||
makeRequiredRule,
|
||||
} from '#/components/form-create/helpers';
|
||||
|
||||
export const useUploadImageRule = () => {
|
||||
const label = '单图上传';
|
||||
const name = 'ImageUpload';
|
||||
return {
|
||||
icon: 'icon-image',
|
||||
label,
|
||||
name,
|
||||
rule() {
|
||||
return {
|
||||
type: name,
|
||||
field: buildUUID(),
|
||||
title: label,
|
||||
info: '',
|
||||
$required: false,
|
||||
};
|
||||
},
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
title: '图片类型限制',
|
||||
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
options: [
|
||||
{ label: 'image/apng', value: 'image/apng' },
|
||||
{ label: 'image/bmp', value: 'image/bmp' },
|
||||
{ label: 'image/gif', value: 'image/gif' },
|
||||
{ label: 'image/jpeg', value: 'image/jpeg' },
|
||||
{ label: 'image/pjpeg', value: 'image/pjpeg' },
|
||||
{ label: 'image/svg+xml', value: 'image/svg+xml' },
|
||||
{ label: 'image/tiff', value: 'image/tiff' },
|
||||
{ label: 'image/webp', value: 'image/webp' },
|
||||
{ label: 'image/x-icon', value: 'image/x-icon' },
|
||||
],
|
||||
props: {
|
||||
multiple: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '组件高度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'width',
|
||||
title: '组件宽度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'borderradius',
|
||||
title: '组件边框圆角',
|
||||
value: '8px',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否显示删除按钮',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'showBtnText',
|
||||
title: '是否显示按钮文字',
|
||||
value: true,
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
import { buildUUID } from '@vben/utils';
|
||||
|
||||
import {
|
||||
localeProps,
|
||||
makeRequiredRule,
|
||||
} from '#/components/form-create/helpers';
|
||||
|
||||
export const useUploadImagesRule = () => {
|
||||
const label = '多图上传';
|
||||
const name = 'ImagesUpload';
|
||||
return {
|
||||
icon: 'icon-image',
|
||||
label,
|
||||
name,
|
||||
rule() {
|
||||
return {
|
||||
type: name,
|
||||
field: buildUUID(),
|
||||
title: label,
|
||||
info: '',
|
||||
$required: false,
|
||||
};
|
||||
},
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
title: '图片类型限制',
|
||||
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
options: [
|
||||
{ label: 'image/apng', value: 'image/apng' },
|
||||
{ label: 'image/bmp', value: 'image/bmp' },
|
||||
{ label: 'image/gif', value: 'image/gif' },
|
||||
{ label: 'image/jpeg', value: 'image/jpeg' },
|
||||
{ label: 'image/pjpeg', value: 'image/pjpeg' },
|
||||
{ label: 'image/svg+xml', value: 'image/svg+xml' },
|
||||
{ label: 'image/tiff', value: 'image/tiff' },
|
||||
{ label: 'image/webp', value: 'image/webp' },
|
||||
{ label: 'image/x-icon', value: 'image/x-icon' },
|
||||
],
|
||||
props: {
|
||||
multiple: true,
|
||||
maxNumber: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'limit',
|
||||
title: '数量限制',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '组件高度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'width',
|
||||
title: '组件宽度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'borderradius',
|
||||
title: '组件边框圆角',
|
||||
value: '8px',
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
import type { Rule } from '@form-create/element-ui'; // 左侧拖拽按钮
|
||||
|
||||
/** 数据字典 Select 选择器组件 Props 类型 */
|
||||
export interface DictSelectProps {
|
||||
dictType: string; // 字典类型
|
||||
valueType?: 'bool' | 'int' | 'str'; // 字典值类型 TODO @芋艿:'boolean' | 'number' | 'string';需要和 vue3 一起统一!
|
||||
selectType?: 'checkbox' | 'radio' | 'select'; // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio
|
||||
formCreateInject?: any;
|
||||
}
|
||||
|
||||
/** 左侧拖拽按钮 */
|
||||
export interface MenuItem {
|
||||
label: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/** 左侧拖拽按钮分类 */
|
||||
export interface Menu {
|
||||
title: string;
|
||||
name: string;
|
||||
list: MenuItem[];
|
||||
}
|
||||
|
||||
export type MenuList = Array<Menu>;
|
||||
|
||||
// TODO @dhb52:MenuList、Menu、MenuItem、DragRule 这几个,是不是没用到呀?
|
||||
// 拖拽组件的规则
|
||||
export interface DragRule {
|
||||
icon: string;
|
||||
name: string;
|
||||
label: string;
|
||||
children?: string;
|
||||
inside?: true;
|
||||
drag?: string | true;
|
||||
dragBtn?: false;
|
||||
mask?: false;
|
||||
|
||||
rule(): Rule;
|
||||
|
||||
props(v: any, v1: any): Rule[];
|
||||
}
|
||||
|
||||
/** 通用 API 下拉组件 Props 类型 */
|
||||
export interface ApiSelectProps {
|
||||
name: string; // 组件名称
|
||||
labelField?: string; // 选项标签
|
||||
valueField?: string; // 选项的值
|
||||
url?: string; // url 接口
|
||||
isDict?: boolean; // 是否字典选择器
|
||||
}
|
||||
|
||||
/** 选择组件规则配置类型 */
|
||||
export interface SelectRuleOption {
|
||||
label: string; // label 名称
|
||||
name: string; // 组件名称
|
||||
icon: string; // 组件图标
|
||||
props?: any[]; // 组件规则
|
||||
event?: any[]; // 事件配置
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import type { App, Component } from 'vue';
|
||||
|
||||
import FcDesigner from '@form-create/designer';
|
||||
import formCreate from '@form-create/element-ui';
|
||||
import install from '@form-create/element-ui/auto-import';
|
||||
// 👇使用 form-create 需额外全局引入 element plus 组件
|
||||
import {
|
||||
ElAlert,
|
||||
ElAside,
|
||||
ElBadge,
|
||||
ElCard,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElContainer,
|
||||
ElDivider,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElFooter,
|
||||
ElHeader,
|
||||
ElMain,
|
||||
ElMenu,
|
||||
ElMenuItem,
|
||||
ElMessage,
|
||||
ElPopconfirm,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
ElTag,
|
||||
ElText,
|
||||
ElTransfer,
|
||||
} from 'element-plus';
|
||||
|
||||
// ======================= 自定义组件 =======================
|
||||
import { useApiSelect } from '#/components/form-create';
|
||||
import DictSelect from '#/components/form-create/components/dict-select.vue';
|
||||
import { useImagesUpload } from '#/components/form-create/components/use-images-upload';
|
||||
import { Tinymce } from '#/components/tinymce';
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
|
||||
const UserSelect = useApiSelect({
|
||||
name: 'UserSelect',
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
url: '/system/user/simple-list',
|
||||
});
|
||||
const DeptSelect = useApiSelect({
|
||||
name: 'DeptSelect',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
url: '/system/dept/simple-list',
|
||||
});
|
||||
const ApiSelect = useApiSelect({
|
||||
name: 'ApiSelect',
|
||||
});
|
||||
const ImagesUpload = useImagesUpload();
|
||||
|
||||
const components = [
|
||||
ImageUpload,
|
||||
ImagesUpload,
|
||||
FileUpload,
|
||||
Tinymce,
|
||||
DictSelect,
|
||||
UserSelect,
|
||||
DeptSelect,
|
||||
ApiSelect,
|
||||
ElAlert,
|
||||
ElTransfer,
|
||||
ElAside,
|
||||
ElContainer,
|
||||
ElDivider,
|
||||
ElHeader,
|
||||
ElMain,
|
||||
ElPopconfirm,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
ElBadge,
|
||||
ElTag,
|
||||
ElText,
|
||||
ElMenu,
|
||||
ElMenuItem,
|
||||
ElFooter,
|
||||
ElMessage,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElCard,
|
||||
];
|
||||
|
||||
// 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档
|
||||
export const setupFormCreate = (app: App<Element>) => {
|
||||
components.forEach((component) => {
|
||||
app.component(component.name as string, component as Component);
|
||||
});
|
||||
formCreate.use(install);
|
||||
app.use(formCreate);
|
||||
app.use(FcDesigner);
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
<!-- eslint-disable no-useless-escape -->
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, unref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { isString } from '@vben/utils';
|
||||
|
||||
import FcDesigner from '@form-create/designer';
|
||||
import formCreate from '@form-create/element-ui';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { ElButton, ElMessage } from 'element-plus';
|
||||
import hljs from 'highlight.js';
|
||||
import xml from 'highlight.js/lib/languages/java';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
|
||||
import { useFormCreateDesigner } from '#/components/form-create';
|
||||
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
defineOptions({ name: 'InfraBuild' });
|
||||
|
||||
const [Modal, modalApi] = useVbenModal();
|
||||
|
||||
const designer = ref(); // 表单设计器
|
||||
|
||||
// 表单设计器配置
|
||||
const designerConfig = ref({
|
||||
switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
|
||||
autoActive: true, // 是否自动选中拖入的组件
|
||||
useTemplate: false, // 是否生成vue2语法的模板组件
|
||||
formOptions: {
|
||||
form: {
|
||||
labelWidth: '100px', // 设置默认的 label 宽度为 100px
|
||||
},
|
||||
}, // 定义表单配置默认值
|
||||
fieldReadonly: false, // 配置field是否可以编辑
|
||||
hiddenDragMenu: false, // 隐藏拖拽操作按钮
|
||||
hiddenDragBtn: false, // 隐藏拖拽按钮
|
||||
hiddenMenu: [], // 隐藏部分菜单
|
||||
hiddenItem: [], // 隐藏部分组件
|
||||
hiddenItemConfig: {}, // 隐藏组件的部分配置项
|
||||
disabledItemConfig: {}, // 禁用组件的部分配置项
|
||||
showSaveBtn: false, // 是否显示保存按钮
|
||||
showConfig: true, // 是否显示右侧的配置界面
|
||||
showBaseForm: true, // 是否显示组件的基础配置表单
|
||||
showControl: true, // 是否显示组件联动
|
||||
showPropsForm: true, // 是否显示组件的属性配置表单
|
||||
showEventForm: true, // 是否显示组件的事件配置表单
|
||||
showValidateForm: true, // 是否显示组件的验证配置表单
|
||||
showFormConfig: true, // 是否显示表单配置
|
||||
showInputData: true, // 是否显示录入按钮
|
||||
showDevice: true, // 是否显示多端适配选项
|
||||
appendConfigData: [], // 定义渲染规则所需的formData
|
||||
});
|
||||
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const dialogTitle = ref(''); // 弹窗的标题
|
||||
const formType = ref(-1); // 表单的类型:0 - 生成 JSON;1 - 生成 Options;2 - 生成组件
|
||||
const formData = ref(''); // 表单数据
|
||||
useFormCreateDesigner(designer); // 表单设计器增强
|
||||
|
||||
/** 打开弹窗 */
|
||||
const openModel = (title: string) => {
|
||||
dialogVisible.value = true;
|
||||
dialogTitle.value = title;
|
||||
modalApi.open();
|
||||
};
|
||||
|
||||
/** 生成 JSON */
|
||||
const showJson = () => {
|
||||
openModel('生成 JSON');
|
||||
formType.value = 0;
|
||||
formData.value = designer.value.getRule();
|
||||
};
|
||||
|
||||
/** 生成 Options */
|
||||
const showOption = () => {
|
||||
openModel('生成 Options');
|
||||
formType.value = 1;
|
||||
formData.value = designer.value.getOption();
|
||||
};
|
||||
|
||||
/** 生成组件 */
|
||||
const showTemplate = () => {
|
||||
openModel('生成组件');
|
||||
formType.value = 2;
|
||||
formData.value = makeTemplate();
|
||||
};
|
||||
|
||||
const makeTemplate = () => {
|
||||
const rule = designer.value.getRule();
|
||||
const opt = designer.value.getOption();
|
||||
return `<template>
|
||||
<form-create
|
||||
v-model:api="fApi"
|
||||
:rule="rule"
|
||||
:option="option"
|
||||
@submit="onSubmit"
|
||||
></form-create>
|
||||
</template>
|
||||
<script setup lang=ts>
|
||||
const faps = ref(null)
|
||||
const rule = ref('')
|
||||
const option = ref('')
|
||||
const init = () => {
|
||||
rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}')
|
||||
option.value = formCreate.parseJson('${JSON.stringify(opt, null, 2)}')
|
||||
}
|
||||
const onSubmit = (formData) => {
|
||||
//todo 提交表单
|
||||
}
|
||||
init()
|
||||
<\/script>`;
|
||||
};
|
||||
|
||||
/** 复制 */
|
||||
const copy = async (text: string) => {
|
||||
const textToCopy = JSON.stringify(text, null, 2);
|
||||
const { copy, copied, isSupported } = useClipboard({ source: textToCopy });
|
||||
if (isSupported) {
|
||||
await copy();
|
||||
if (unref(copied)) {
|
||||
ElMessage.success('复制成功');
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('复制失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 代码高亮
|
||||
*/
|
||||
const highlightedCode = (code: string) => {
|
||||
// 处理语言和代码
|
||||
let language = 'json';
|
||||
if (formType.value === 2) {
|
||||
language = 'xml';
|
||||
}
|
||||
// debugger
|
||||
if (!isString(code)) {
|
||||
code = JSON.stringify(code, null, 2);
|
||||
}
|
||||
// 高亮
|
||||
const result = hljs.highlight(code, { language, ignoreIllegals: true });
|
||||
return result.value || ' ';
|
||||
};
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 注册代码高亮的各种语言
|
||||
hljs.registerLanguage('xml', xml);
|
||||
hljs.registerLanguage('json', json);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FcDesigner ref="designer" height="90vh" :config="designerConfig">
|
||||
<template #handle>
|
||||
<ElButton size="small" type="primary" plain @click="showJson">
|
||||
生成JSON
|
||||
</ElButton>
|
||||
<ElButton size="small" type="primary" plain @click="showOption">
|
||||
生成Options
|
||||
</ElButton>
|
||||
<ElButton size="small" type="primary" plain @click="showTemplate">
|
||||
生成组件
|
||||
</ElButton>
|
||||
</template>
|
||||
</FcDesigner>
|
||||
|
||||
<!-- 弹窗:表单预览 -->
|
||||
<Modal :title="dialogTitle" :footer="false" :fullscreen-button="false">
|
||||
<div>
|
||||
<ElButton style="float: right" @click="copy(formData)"> 复制 </ElButton>
|
||||
<div>
|
||||
<pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
|
@ -762,6 +762,12 @@ importers:
|
|||
|
||||
apps/web-ele:
|
||||
dependencies:
|
||||
'@form-create/designer':
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.11(vue@3.5.13(typescript@5.8.3))
|
||||
'@form-create/element-ui':
|
||||
specifier: ^3.2.11
|
||||
version: 3.2.22(vue@3.5.13(typescript@5.8.3))
|
||||
'@tinymce/tinymce-vue':
|
||||
specifier: 'catalog:'
|
||||
version: 6.1.0(vue@3.5.13(typescript@5.8.3))
|
||||
|
@ -3630,6 +3636,27 @@ packages:
|
|||
'@form-create/component-antdv-upload@3.2.18':
|
||||
resolution: {integrity: sha512-cobjChcblnfO0ph4MunJDUiBLyRwpzekXo6MFRsB5iq9ln73UjLnyLps4YuM2KRZ/Cn9FEoWN1kYvTFf1zKdjg==}
|
||||
|
||||
'@form-create/component-elm-checkbox@3.2.18':
|
||||
resolution: {integrity: sha512-W8v4o+MZWPEJmIIWojKmnn87tFWpxyTbaIhWJU4Ca0S99YoXR7RdHKLt06HYwJixVLZqytNRj9BMxR4UZQ6JNg==}
|
||||
|
||||
'@form-create/component-elm-frame@3.2.18':
|
||||
resolution: {integrity: sha512-yob3jmO1xBbKfVfFNeO/xh80o1E2IbVx8NsnpTaaK9X0ARJFvhPvW53qX6TgJxdvzCGTsm/W6P6a4SaebQJtVQ==}
|
||||
|
||||
'@form-create/component-elm-group@3.2.22':
|
||||
resolution: {integrity: sha512-hy6ZqLpDITqDoTMc4Es2twVNffjXuX0HW7aY36+iyicnJj1Y9hMRj2HPbT4DNlQWZ3ybOb/AlcYN0BwIpW40qw==}
|
||||
|
||||
'@form-create/component-elm-radio@3.2.18':
|
||||
resolution: {integrity: sha512-kkb6xFOviqgoBRRLzsoZTnqKX9GSw2jaLCWWRPkwqEwA/aLNHRX0MuBdGNvpaaLaD1ph5g+N86GekHvvanbJlQ==}
|
||||
|
||||
'@form-create/component-elm-select@3.2.18':
|
||||
resolution: {integrity: sha512-gqBzPgNGJ6GwQ/pK/qCuoxQeM/fflNv7IqibETt2IFgutsGVM1lXYic8QJ/51YkuI0afkDKF+wAEbfB6zqaKIA==}
|
||||
|
||||
'@form-create/component-elm-tree@3.2.18':
|
||||
resolution: {integrity: sha512-s+0+NPh2t500pv4CA51dtwuWWlY2wW0qbL7ZE3steTBh8Z++7s+n/6y6joGPxTeP+7FkpfruA1TfJ7+5Ntpe1Q==}
|
||||
|
||||
'@form-create/component-elm-upload@3.2.18':
|
||||
resolution: {integrity: sha512-FVFJYarlk5+/Kjg9kJ9ElwyP8bt+DR0m/pPUMqmkEoUtv0Sr6nkk596THLjfyXV1WRFeoZMjzIS6qCyTnqWksQ==}
|
||||
|
||||
'@form-create/component-subform@3.1.34':
|
||||
resolution: {integrity: sha512-OJcFH/7MTHx7JLEjDK/weS27qfuFWAI+OK+gXTJ2jIt9aZkGWF/EWkjetiJLt5a0KMw4Z15wOS2XCY9pVK9vlA==}
|
||||
|
||||
|
@ -3641,6 +3668,16 @@ packages:
|
|||
peerDependencies:
|
||||
vue: ^3.5.13
|
||||
|
||||
'@form-create/designer@3.2.11':
|
||||
resolution: {integrity: sha512-5mPyeHFOj8n01LOVhibjX8OujD6RYBH8TF2Ol7n8QxaSqIcAFTz9PADIiX982REPxiZ6I8BqZa2t0OtYQtETpA==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.13
|
||||
|
||||
'@form-create/element-ui@3.2.22':
|
||||
resolution: {integrity: sha512-6UfJloHWwCDkei4dQjigk5JzaFQiwEISpY0Tc5plSyJg8bt7JdqCp6C9+OQYmjTYaurwzdTvgD9NfbKDFC8xEQ==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.13
|
||||
|
||||
'@form-create/utils@3.2.18':
|
||||
resolution: {integrity: sha512-C98bFPdFVMltiHQvEZqv4rVdhcqthJgvxMbWDlniL03HS5oyusnUvxUE8jf0I9zk5dZRDGmxKOUtzE3JDWP9nQ==}
|
||||
|
||||
|
@ -13298,6 +13335,34 @@ snapshots:
|
|||
dependencies:
|
||||
'@form-create/utils': 3.2.18
|
||||
|
||||
'@form-create/component-elm-checkbox@3.2.18':
|
||||
dependencies:
|
||||
'@form-create/utils': 3.2.18
|
||||
|
||||
'@form-create/component-elm-frame@3.2.18':
|
||||
dependencies:
|
||||
'@form-create/utils': 3.2.18
|
||||
|
||||
'@form-create/component-elm-group@3.2.22':
|
||||
dependencies:
|
||||
'@form-create/utils': 3.2.18
|
||||
|
||||
'@form-create/component-elm-radio@3.2.18':
|
||||
dependencies:
|
||||
'@form-create/utils': 3.2.18
|
||||
|
||||
'@form-create/component-elm-select@3.2.18':
|
||||
dependencies:
|
||||
'@form-create/utils': 3.2.18
|
||||
|
||||
'@form-create/component-elm-tree@3.2.18':
|
||||
dependencies:
|
||||
'@form-create/utils': 3.2.18
|
||||
|
||||
'@form-create/component-elm-upload@3.2.18':
|
||||
dependencies:
|
||||
'@form-create/utils': 3.2.18
|
||||
|
||||
'@form-create/component-subform@3.1.34': {}
|
||||
|
||||
'@form-create/component-wangeditor@3.2.14':
|
||||
|
@ -13309,6 +13374,33 @@ snapshots:
|
|||
'@form-create/utils': 3.2.18
|
||||
vue: 3.5.13(typescript@5.8.3)
|
||||
|
||||
'@form-create/designer@3.2.11(vue@3.5.13(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@form-create/component-wangeditor': 3.2.14
|
||||
'@form-create/element-ui': 3.2.22(vue@3.5.13(typescript@5.8.3))
|
||||
'@form-create/utils': 3.2.18
|
||||
codemirror: 6.65.7
|
||||
element-plus: 2.9.9(vue@3.5.13(typescript@5.8.3))
|
||||
js-beautify: 1.15.4
|
||||
vue: 3.5.13(typescript@5.8.3)
|
||||
vuedraggable: 4.1.0(vue@3.5.13(typescript@5.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
'@form-create/element-ui@3.2.22(vue@3.5.13(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@form-create/component-elm-checkbox': 3.2.18
|
||||
'@form-create/component-elm-frame': 3.2.18
|
||||
'@form-create/component-elm-group': 3.2.22
|
||||
'@form-create/component-elm-radio': 3.2.18
|
||||
'@form-create/component-elm-select': 3.2.18
|
||||
'@form-create/component-elm-tree': 3.2.18
|
||||
'@form-create/component-elm-upload': 3.2.18
|
||||
'@form-create/component-subform': 3.1.34
|
||||
'@form-create/core': 3.2.22(vue@3.5.13(typescript@5.8.3))
|
||||
'@form-create/utils': 3.2.18
|
||||
vue: 3.5.13(typescript@5.8.3)
|
||||
|
||||
'@form-create/utils@3.2.18': {}
|
||||
|
||||
'@gar/promisify@1.1.3': {}
|
||||
|
|
Loading…
Reference in New Issue