diff --git a/apps/web-ele/package.json b/apps/web-ele/package.json
index ccd69cf0..ac8b9f22 100644
--- a/apps/web-ele/package.json
+++ b/apps/web-ele/package.json
@@ -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:*",
diff --git a/apps/web-ele/src/bootstrap.ts b/apps/web-ele/src/bootstrap.ts
index be054f80..b82949fb 100644
--- a/apps/web-ele/src/bootstrap.ts
+++ b/apps/web-ele/src/bootstrap.ts
@@ -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) {
diff --git a/apps/web-ele/src/components/form-create/components/dict-select.vue b/apps/web-ele/src/components/form-create/components/dict-select.vue
new file mode 100644
index 00000000..b4e65d06
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/components/dict-select.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+ {{ dict.label }}
+
+
+
diff --git a/apps/web-ele/src/components/form-create/components/use-api-select.tsx b/apps/web-ele/src/components/form-create/components/use-api-select.tsx
new file mode 100644
index 00000000..6eee727d
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/components/use-api-select.tsx
@@ -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([]); // 下拉数据
+ const loading = ref(false); // 是否正在从远程获取数据
+ const queryParam = ref(); // 当前输入的值
+ 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 (
+
+ {options.value.map(
+ (item: { label: any; value: any }, index: any) => (
+
+ ),
+ )}
+
+ );
+ }
+ return (
+
+ {options.value.map(
+ (item: { label: any; value: any }, index: any) => (
+
+ ),
+ )}
+
+ );
+ };
+ const buildCheckbox = () => {
+ if (isEmpty(options.value)) {
+ options.value = [
+ { label: '选项1', value: '选项1' },
+ { label: '选项2', value: '选项2' },
+ ];
+ }
+ return (
+
+ {options.value.map(
+ (item: { label: any; value: any }, index: any) => (
+
+ {item.label}
+
+ ),
+ )}
+
+ );
+ };
+ const buildRadio = () => {
+ if (isEmpty(options.value)) {
+ options.value = [
+ { label: '选项1', value: '选项1' },
+ { label: '选项2', value: '选项2' },
+ ];
+ }
+ return (
+
+ {options.value.map(
+ (item: { label: any; value: any }, index: any) => (
+
+ {item.label}
+
+ ),
+ )}
+
+ );
+ };
+ return () => (
+ <>
+ {(() => {
+ switch (props.selectType) {
+ case 'checkbox': {
+ return buildCheckbox();
+ }
+ case 'radio': {
+ return buildRadio();
+ }
+ case 'select': {
+ return buildSelect();
+ }
+ default: {
+ return buildSelect();
+ }
+ }
+ })()}
+ >
+ );
+ },
+ });
+};
diff --git a/apps/web-ele/src/components/form-create/components/use-images-upload.tsx b/apps/web-ele/src/components/form-create/components/use-images-upload.tsx
new file mode 100644
index 00000000..4e821c6c
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/components/use-images-upload.tsx
@@ -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 }) => (
+
+ );
+ },
+ });
+};
diff --git a/apps/web-ele/src/components/form-create/helpers.ts b/apps/web-ele/src/components/form-create/helpers.ts
new file mode 100644
index 00000000..c647711c
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/helpers.ts
@@ -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,
+ fields: Array> = [],
+ 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();
+ });
+};
diff --git a/apps/web-ele/src/components/form-create/index.ts b/apps/web-ele/src/components/form-create/index.ts
new file mode 100644
index 00000000..b311e79e
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/index.ts
@@ -0,0 +1,3 @@
+export { useApiSelect } from './components/use-api-select';
+
+export { useFormCreateDesigner } from './helpers';
diff --git a/apps/web-ele/src/components/form-create/rules/data.ts b/apps/web-ele/src/components/form-create/rules/data.ts
new file mode 100644
index 00000000..2c6cee2c
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/rules/data.ts
@@ -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 };
diff --git a/apps/web-ele/src/components/form-create/rules/index.ts b/apps/web-ele/src/components/form-create/rules/index.ts
new file mode 100644
index 00000000..db306da3
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/rules/index.ts
@@ -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';
diff --git a/apps/web-ele/src/components/form-create/rules/use-dict-select.ts b/apps/web-ele/src/components/form-create/rules/use-dict-select.ts
new file mode 100644
index 00000000..c9c438e8
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/rules/use-dict-select.ts
@@ -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,
+ ]);
+ },
+ };
+};
diff --git a/apps/web-ele/src/components/form-create/rules/use-editor-rule.ts b/apps/web-ele/src/components/form-create/rules/use-editor-rule.ts
new file mode 100644
index 00000000..556baf0a
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/rules/use-editor-rule.ts
@@ -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: '是否只读' },
+ ]);
+ },
+ };
+};
diff --git a/apps/web-ele/src/components/form-create/rules/use-select-rule.ts b/apps/web-ele/src/components/form-create/rules/use-select-rule.ts
new file mode 100644
index 00000000..fd421378
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/rules/use-select-rule.ts
@@ -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,
+ ]);
+ },
+ };
+};
diff --git a/apps/web-ele/src/components/form-create/rules/use-upload-file-rule.ts b/apps/web-ele/src/components/form-create/rules/use-upload-file-rule.ts
new file mode 100644
index 00000000..55f5bea3
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/rules/use-upload-file-rule.ts
@@ -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,
+ },
+ ]);
+ },
+ };
+};
diff --git a/apps/web-ele/src/components/form-create/rules/use-upload-image-rule.ts b/apps/web-ele/src/components/form-create/rules/use-upload-image-rule.ts
new file mode 100644
index 00000000..70760b06
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/rules/use-upload-image-rule.ts
@@ -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,
+ },
+ ]);
+ },
+ };
+};
diff --git a/apps/web-ele/src/components/form-create/rules/use-upload-images-rule.ts b/apps/web-ele/src/components/form-create/rules/use-upload-images-rule.ts
new file mode 100644
index 00000000..c18a7b49
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/rules/use-upload-images-rule.ts
@@ -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',
+ },
+ ]);
+ },
+ };
+};
diff --git a/apps/web-ele/src/components/form-create/typing.ts b/apps/web-ele/src/components/form-create/typing.ts
new file mode 100644
index 00000000..13f98f97
--- /dev/null
+++ b/apps/web-ele/src/components/form-create/typing.ts
@@ -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