From 1e07c9ee12e91721fc7d0f3a3c96d8307901d2b8 Mon Sep 17 00:00:00 2001 From: dhb52 Date: Sun, 20 Apr 2025 20:41:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20FormCreate=20?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/package.json | 6 + .../src/api/system/dict/type/index.ts | 9 + apps/web-antd/src/bootstrap.ts | 15 +- .../src/components/FormCreate/index.ts | 3 + .../FormCreate/src/components/DictSelect.vue | 85 ++++++ .../src/components/useApiSelect.tsx | 280 ++++++++++++++++++ .../components/FormCreate/src/config/index.ts | 6 + .../FormCreate/src/config/selectRule.ts | 182 ++++++++++++ .../src/config/useDictSelectRule.ts | 70 +++++ .../FormCreate/src/config/useEditorRule.ts | 35 +++ .../FormCreate/src/config/useSelectRule.ts | 46 +++ .../src/config/useUploadFileRule.ts | 83 ++++++ .../FormCreate/src/config/useUploadImgRule.ts | 92 ++++++ .../src/config/useUploadImgsRule.ts | 87 ++++++ .../components/FormCreate/src/type/index.ts | 52 ++++ .../FormCreate/src/useFormCreateDesigner.ts | 116 ++++++++ .../components/FormCreate/src/utils/index.ts | 65 ++++ .../src/locales/langs/en-US/common.json | 5 + .../src/locales/langs/zh-CN/common.json | 5 + apps/web-antd/src/plugins/formCreate/index.ts | 46 +++ apps/web-antd/src/router/index.ts | 2 +- .../src/router/routes/modules/demos.ts | 3 +- .../src/router/routes/modules/vben.ts | 1 + apps/web-antd/src/utils/dict.ts | 55 ++++ apps/web-antd/src/utils/index.ts | 43 +++ apps/web-antd/src/utils/is.ts | 125 ++++++++ apps/web-antd/src/views/infra/build/index.vue | 193 ++++++++++++ pnpm-lock.yaml | 170 +++++++++++ pnpm-workspace.yaml | 3 + 29 files changed, 1880 insertions(+), 3 deletions(-) create mode 100644 apps/web-antd/src/components/FormCreate/index.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/components/DictSelect.vue create mode 100644 apps/web-antd/src/components/FormCreate/src/components/useApiSelect.tsx create mode 100644 apps/web-antd/src/components/FormCreate/src/config/index.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/config/selectRule.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/config/useDictSelectRule.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/config/useEditorRule.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/config/useSelectRule.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/config/useUploadFileRule.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/config/useUploadImgRule.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/config/useUploadImgsRule.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/type/index.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/useFormCreateDesigner.ts create mode 100644 apps/web-antd/src/components/FormCreate/src/utils/index.ts create mode 100644 apps/web-antd/src/locales/langs/en-US/common.json create mode 100644 apps/web-antd/src/locales/langs/zh-CN/common.json create mode 100644 apps/web-antd/src/plugins/formCreate/index.ts create mode 100644 apps/web-antd/src/utils/index.ts create mode 100644 apps/web-antd/src/utils/is.ts create mode 100644 apps/web-antd/src/views/infra/build/index.vue diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 31deb3e25..feeaa5362 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -26,7 +26,10 @@ "#/*": "./src/*" }, "dependencies": { + "@form-create/ant-design-vue": "catalog:", + "@form-create/antd-designer": "catalog:", "@tinymce/tinymce-vue": "catalog:", + "@types/lodash.clonedeep": "catalog:", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", "@vben/constants": "workspace:*", @@ -43,11 +46,14 @@ "@vben/utils": "workspace:*", "@vueuse/core": "catalog:", "ant-design-vue": "catalog:", + "axios": "catalog:", "crypto-js": "catalog:", "dayjs": "catalog:", "highlight.js": "catalog:", + "lodash.clonedeep": "catalog:", "pinia": "catalog:", "vue": "catalog:", + "vue-dompurify-html": "catalog:", "vue-router": "catalog:" } } diff --git a/apps/web-antd/src/api/system/dict/type/index.ts b/apps/web-antd/src/api/system/dict/type/index.ts index c37be4844..8117663dc 100644 --- a/apps/web-antd/src/api/system/dict/type/index.ts +++ b/apps/web-antd/src/api/system/dict/type/index.ts @@ -1,5 +1,14 @@ import { requestClient } from '#/api/request'; +export type DictTypeVO = { + createTime: Date; + id: number | undefined; + name: string; + remark: string; + status: number; + type: string; +}; + export namespace SystemDictTypeApi { /** 字典类型 */ export type SystemDictType = { diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index e4aaf4057..116523ce9 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -1,4 +1,5 @@ import { createApp, watchEffect } from 'vue'; +import VueDOMPurifyHTML from 'vue-dompurify-html'; import { registerAccessDirective } from '@vben/access'; import { registerLoadingDirective } from '@vben/common-ui/es/loading'; @@ -7,9 +8,12 @@ import { initStores } from '@vben/stores'; import '@vben/styles'; import '@vben/styles/antd'; +import FcDesigner from '@form-create/antd-designer'; import { useTitle } from '@vueuse/core'; +import Antd from 'ant-design-vue'; import { $t, setupI18n } from '#/locales'; +import { setupFormCreate } from '#/plugins/formCreate'; import { initComponentAdapter } from './adapter/component'; import App from './app.vue'; @@ -39,7 +43,7 @@ async function bootstrap(namespace: string) { // 国际化 i18n 配置 await setupI18n(app); - // 配置 pinia-tore + // 配置 pinia-store await initStores(app, { namespace }); // 安装权限指令 @@ -52,6 +56,15 @@ async function bootstrap(namespace: string) { // 配置路由及路由守卫 app.use(router); + // formCreate + app.use(Antd); + app.use(FcDesigner); + // app.use(FcDesigner.formCreate); + setupFormCreate(app); + + // vue-dompurify-html + app.use(VueDOMPurifyHTML); + // 配置Motion插件 const { MotionPlugin } = await import('@vben/plugins/motion'); app.use(MotionPlugin); diff --git a/apps/web-antd/src/components/FormCreate/index.ts b/apps/web-antd/src/components/FormCreate/index.ts new file mode 100644 index 000000000..84b612db6 --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/index.ts @@ -0,0 +1,3 @@ +export { useApiSelect } from './src/components/useApiSelect'; + +export { useFormCreateDesigner } from './src/useFormCreateDesigner'; diff --git a/apps/web-antd/src/components/FormCreate/src/components/DictSelect.vue b/apps/web-antd/src/components/FormCreate/src/components/DictSelect.vue new file mode 100644 index 000000000..339c4fb4b --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/components/DictSelect.vue @@ -0,0 +1,85 @@ + + + + diff --git a/apps/web-antd/src/components/FormCreate/src/components/useApiSelect.tsx b/apps/web-antd/src/components/FormCreate/src/components/useApiSelect.tsx new file mode 100644 index 000000000..ee78263ed --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/components/useApiSelect.tsx @@ -0,0 +1,280 @@ +import type { ApiSelectProps } from '#/components/FormCreate/src/type'; + +import { defineComponent, onMounted, ref, useAttrs } from 'vue'; + +import { + Checkbox, + CheckboxGroup, + Radio, + RadioGroup, + Select, + SelectOption, +} from 'ant-design-vue'; + +import { requestClient } from '#/api/request'; +import { isEmpty } from '#/utils/is'; + +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 ( + + ); + } + return ( + + ); + }; + 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 () => ( + <> + {props.selectType === 'select' + ? buildSelect() + : props.selectType === 'radio' + ? buildRadio() + : // eslint-disable-next-line unicorn/no-nested-ternary + props.selectType === 'checkbox' + ? buildCheckbox() + : buildSelect()} + + ); + }, + }); +}; diff --git a/apps/web-antd/src/components/FormCreate/src/config/index.ts b/apps/web-antd/src/components/FormCreate/src/config/index.ts new file mode 100644 index 000000000..49b9d3bda --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/config/index.ts @@ -0,0 +1,6 @@ +export { useDictSelectRule } from './useDictSelectRule'; +export { useEditorRule } from './useEditorRule'; +export { useSelectRule } from './useSelectRule'; +export { useUploadFileRule } from './useUploadFileRule'; +export { useUploadImgRule } from './useUploadImgRule'; +export { useUploadImgsRule } from './useUploadImgsRule'; diff --git a/apps/web-antd/src/components/FormCreate/src/config/selectRule.ts b/apps/web-antd/src/components/FormCreate/src/config/selectRule.ts new file mode 100644 index 000000000..2c6cee2ce --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/config/selectRule.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-antd/src/components/FormCreate/src/config/useDictSelectRule.ts b/apps/web-antd/src/components/FormCreate/src/config/useDictSelectRule.ts new file mode 100644 index 000000000..10d6f1e53 --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/config/useDictSelectRule.ts @@ -0,0 +1,70 @@ +import { onMounted, ref } from 'vue'; + +import cloneDeep from 'lodash.clonedeep'; + +import * as DictDataApi from '#/api/system/dict/type'; +import { selectRule } from '#/components/FormCreate/src/config/selectRule'; +import { + localeProps, + makeRequiredRule, +} from '#/components/FormCreate/src/utils'; +import { generateUUID } from '#/utils'; + +/** + * 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 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.DictTypeVO) => ({ + label: item.name, + value: item.type, + })) ?? []; + }); + return { + icon: 'icon-descriptions', + label, + name, + rule() { + return { + type: name, + field: generateUUID(), + 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-antd/src/components/FormCreate/src/config/useEditorRule.ts b/apps/web-antd/src/components/FormCreate/src/config/useEditorRule.ts new file mode 100644 index 000000000..1cca13611 --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/config/useEditorRule.ts @@ -0,0 +1,35 @@ +import { + localeProps, + makeRequiredRule, +} from '#/components/FormCreate/src/utils'; +import { generateUUID } from '#/utils'; + +export const useEditorRule = () => { + const label = '富文本'; + const name = 'Editor'; + return { + icon: 'icon-editor', + label, + name, + rule() { + return { + type: name, + field: generateUUID(), + 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-antd/src/components/FormCreate/src/config/useSelectRule.ts b/apps/web-antd/src/components/FormCreate/src/config/useSelectRule.ts new file mode 100644 index 000000000..08284a2df --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/config/useSelectRule.ts @@ -0,0 +1,46 @@ +import type { SelectRuleOption } from '#/components/FormCreate/src/type'; + +import cloneDeep from 'lodash.clonedeep'; + +import { selectRule } from '#/components/FormCreate/src/config/selectRule'; +import { + localeProps, + makeRequiredRule, +} from '#/components/FormCreate/src/utils'; +import { generateUUID } from '#/utils'; + +/** + * 通用选择器规则 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: generateUUID(), + 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-antd/src/components/FormCreate/src/config/useUploadFileRule.ts b/apps/web-antd/src/components/FormCreate/src/config/useUploadFileRule.ts new file mode 100644 index 000000000..7a006ecaa --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/config/useUploadFileRule.ts @@ -0,0 +1,83 @@ +import { + localeProps, + makeRequiredRule, +} from '#/components/FormCreate/src/utils'; +import { generateUUID } from '#/utils'; + +export const useUploadFileRule = () => { + const label = '文件上传'; + const name = 'UploadFile'; + return { + icon: 'icon-upload', + label, + name, + rule() { + return { + type: name, + field: generateUUID(), + 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-antd/src/components/FormCreate/src/config/useUploadImgRule.ts b/apps/web-antd/src/components/FormCreate/src/config/useUploadImgRule.ts new file mode 100644 index 000000000..6bc419473 --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/config/useUploadImgRule.ts @@ -0,0 +1,92 @@ +import { + localeProps, + makeRequiredRule, +} from '#/components/FormCreate/src/utils'; +import { generateUUID } from '#/utils'; + +export const useUploadImgRule = () => { + const label = '单图上传'; + const name = 'UploadImg'; + return { + icon: 'icon-image', + label, + name, + rule() { + return { + type: name, + field: generateUUID(), + 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, + }, + }, + { + 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-antd/src/components/FormCreate/src/config/useUploadImgsRule.ts b/apps/web-antd/src/components/FormCreate/src/config/useUploadImgsRule.ts new file mode 100644 index 000000000..c1e183062 --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/config/useUploadImgsRule.ts @@ -0,0 +1,87 @@ +import { + localeProps, + makeRequiredRule, +} from '#/components/FormCreate/src/utils'; +import { generateUUID } from '#/utils'; + +export const useUploadImgsRule = () => { + const label = '多图上传'; + const name = 'UploadImgs'; + return { + icon: 'icon-image', + label, + name, + rule() { + return { + type: name, + field: generateUUID(), + 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, + }, + }, + { + 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-antd/src/components/FormCreate/src/type/index.ts b/apps/web-antd/src/components/FormCreate/src/type/index.ts new file mode 100644 index 000000000..cc5e18aae --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/type/index.ts @@ -0,0 +1,52 @@ +import type { Rule } from '@form-create/ant-design-vue'; // 左侧拖拽按钮 +// 左侧拖拽按钮 + +// 左侧拖拽按钮 +export interface MenuItem { + label: string; + name: string; + icon: string; +} + +// 左侧拖拽按钮分类 +export interface Menu { + title: string; + name: string; + list: MenuItem[]; +} + +export type MenuList = Array; + +// 拖拽组件的规则 +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[]; +} + +// 通用下拉组件 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[]; // 事件配置 +} diff --git a/apps/web-antd/src/components/FormCreate/src/useFormCreateDesigner.ts b/apps/web-antd/src/components/FormCreate/src/useFormCreateDesigner.ts new file mode 100644 index 000000000..c522c334e --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/useFormCreateDesigner.ts @@ -0,0 +1,116 @@ +import type { Ref } from 'vue'; + +import type { Menu } from '#/components/FormCreate/src/type'; + +import { nextTick, onMounted } from 'vue'; + +import { apiSelectRule } from '#/components/FormCreate/src/config/selectRule'; + +import { + useDictSelectRule, + useEditorRule, + useSelectRule, + useUploadFileRule, + useUploadImgRule, + useUploadImgsRule, +} from './config'; + +/** + * 表单设计器增强 hook + * 新增 + * - 文件上传 + * - 单图上传 + * - 多图上传 + * - 字典选择器 + * - 用户选择器 + * - 部门选择器 + * - 富文本 + */ +export const useFormCreateDesigner = async (designer: Ref) => { + const editorRule = useEditorRule(); + const uploadFileRule = useUploadFileRule(); + const uploadImgRule = useUploadImgRule(); + const uploadImgsRule = useUploadImgsRule(); + + /** + * 构建表单组件 + */ + const buildFormComponents = () => { + // 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代 + designer.value?.removeMenuItem('upload'); + // 移除自带的富文本组件规则,使用 editorRule 替代 + designer.value?.removeMenuItem('fc-editor'); + const components = [ + editorRule, + uploadFileRule, + uploadImgRule, + uploadImgsRule, + ]; + 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-antd/src/components/FormCreate/src/utils/index.ts b/apps/web-antd/src/components/FormCreate/src/utils/index.ts new file mode 100644 index 000000000..816a0f2a3 --- /dev/null +++ b/apps/web-antd/src/components/FormCreate/src/utils/index.ts @@ -0,0 +1,65 @@ +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); + }); + } +}; diff --git a/apps/web-antd/src/locales/langs/en-US/common.json b/apps/web-antd/src/locales/langs/en-US/common.json new file mode 100644 index 000000000..c5cee3ea9 --- /dev/null +++ b/apps/web-antd/src/locales/langs/en-US/common.json @@ -0,0 +1,5 @@ +{ + "copy": "Copy", + "copySuccess": "Copy Success", + "copyError": "Copy Error" +} diff --git a/apps/web-antd/src/locales/langs/zh-CN/common.json b/apps/web-antd/src/locales/langs/zh-CN/common.json new file mode 100644 index 000000000..c2fec61f7 --- /dev/null +++ b/apps/web-antd/src/locales/langs/zh-CN/common.json @@ -0,0 +1,5 @@ +{ + "copy": "复制", + "copySuccess": "复制成功", + "copyError": "复制失败" +} diff --git a/apps/web-antd/src/plugins/formCreate/index.ts b/apps/web-antd/src/plugins/formCreate/index.ts new file mode 100644 index 000000000..ef48a9d02 --- /dev/null +++ b/apps/web-antd/src/plugins/formCreate/index.ts @@ -0,0 +1,46 @@ +import type { App } from 'vue'; + +import formCreate from '@form-create/ant-design-vue'; +// import install from '@form-create/ant-design-vue/auto-import'; +import FcDesigner from '@form-create/antd-designer'; + +// ======================= 自定义组件 ======================= +import { useApiSelect } from '#/components/FormCreate'; +// import { UploadFile, UploadImg, UploadImgs } from '#/components/UploadFile'; +// import DictSelect from '#/components/FormCreate/src/components/DictSelect.vue'; + +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 components = [ + // UploadImg, + // UploadImgs, + // UploadFile, + // DictSelect, + UserSelect, + DeptSelect, + ApiSelect, +]; + +// 参考 http://www.form-create.com/v3/ant-design-vue/auto-import.html 文档 +export const setupFormCreate = (app: App) => { + components.forEach((component) => { + app.component(component.name as string, component); + }); + // formCreate.use(install); + app.use(formCreate); + app.use(FcDesigner); +}; diff --git a/apps/web-antd/src/router/index.ts b/apps/web-antd/src/router/index.ts index 3d0d43654..5acec55ea 100644 --- a/apps/web-antd/src/router/index.ts +++ b/apps/web-antd/src/router/index.ts @@ -7,8 +7,8 @@ import { import { resetStaticRoutes } from '@vben/utils'; import { createRouterGuard } from './guard'; -import { setupBaiduTongJi } from './tongji'; import { routes } from './routes'; +import { setupBaiduTongJi } from './tongji'; /** * @zh_CN 创建vue-router实例 diff --git a/apps/web-antd/src/router/routes/modules/demos.ts b/apps/web-antd/src/router/routes/modules/demos.ts index beb3dc075..8abc34b99 100644 --- a/apps/web-antd/src/router/routes/modules/demos.ts +++ b/apps/web-antd/src/router/routes/modules/demos.ts @@ -2,6 +2,7 @@ import type { RouteRecordRaw } from 'vue-router'; import { $t } from '#/locales'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const routes: RouteRecordRaw[] = [ { meta: { @@ -25,4 +26,4 @@ const routes: RouteRecordRaw[] = [ }, ]; -// export default routes; // update by 芋艿:不展示 \ No newline at end of file +// export default routes; // update by 芋艿:不展示 diff --git a/apps/web-antd/src/router/routes/modules/vben.ts b/apps/web-antd/src/router/routes/modules/vben.ts index 36aeeab08..db15feb5b 100644 --- a/apps/web-antd/src/router/routes/modules/vben.ts +++ b/apps/web-antd/src/router/routes/modules/vben.ts @@ -11,6 +11,7 @@ import { import { IFrameView } from '#/layouts'; import { $t } from '#/locales'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const routes: RouteRecordRaw[] = [ { meta: { diff --git a/apps/web-antd/src/utils/dict.ts b/apps/web-antd/src/utils/dict.ts index adef41445..8bf64eccd 100644 --- a/apps/web-antd/src/utils/dict.ts +++ b/apps/web-antd/src/utils/dict.ts @@ -6,6 +6,19 @@ import { isObject } from '@vben/utils'; import { useDictStore } from '#/store'; +export interface DictDataType { + dictType: string; + label: string; + value: boolean | number | string; + // TODO: type + colorType: '' | 'error' | 'info' | 'success' | 'warning'; + cssClass: string; +} + +export interface NumberDictDataType extends DictDataType { + value: number; +} + const dictStore = useDictStore(); /** @@ -71,6 +84,48 @@ function getDictOptions( return dictOptions.length > 0 ? dictOptions : []; } +export const getIntDictOptions = (dictType: string): NumberDictDataType[] => { + // 获得通用的 DictDataType 列表 + const dictOptions = getDictOptions(dictType) as DictDataType[]; + // 转换成 number 类型的 NumberDictDataType 类型 + // why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警 + const dictOption: NumberDictDataType[] = []; + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: Number.parseInt(`${dict.value}`), + }); + }); + return dictOption; +}; + +export const getStrDictOptions = (dictType: string) => { + // 获得通用的 DictDataType 列表 + const dictOptions = getDictOptions(dictType) as DictDataType[]; + // 转换成 string 类型的 StringDictDataType 类型 + // why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警 + const dictOption: StringDictDataType[] = []; + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: `${dict.value}`, + }); + }); + return dictOption; +}; + +export const getBoolDictOptions = (dictType: string) => { + const dictOption: DictDataType[] = []; + const dictOptions = getDictOptions(dictType) as DictDataType[]; + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: `${dict.value}` === 'true', + }); + }); + return dictOption; +}; + enum DICT_TYPE { AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式 AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态 diff --git a/apps/web-antd/src/utils/index.ts b/apps/web-antd/src/utils/index.ts new file mode 100644 index 000000000..29af2b6a7 --- /dev/null +++ b/apps/web-antd/src/utils/index.ts @@ -0,0 +1,43 @@ +/** + * 生成 UUID + */ +export const generateUUID = () => { + if (typeof crypto === 'object') { + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + if ( + typeof crypto.getRandomValues === 'function' && + typeof Uint8Array === 'function' + ) { + const callback = (c: any) => { + const num = Number(c); + return ( + num ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4))) + ).toString(16); + }; + return '10000000-1000-4000-8000-100000000000'.replaceAll( + /[018]/g, + callback, + ); + } + } + let timestamp = Date.now(); + let performanceNow = + (typeof performance !== 'undefined' && + performance.now && + performance.now() * 1000) || + 0; + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll(/[xy]/g, (c) => { + let random = Math.random() * 16; + if (timestamp > 0) { + random = Math.trunc((timestamp + random) % 16); + timestamp = Math.floor(timestamp / 16); + } else { + random = Math.trunc((performanceNow + random) % 16); + performanceNow = Math.floor(performanceNow / 16); + } + return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16); + }); +}; diff --git a/apps/web-antd/src/utils/is.ts b/apps/web-antd/src/utils/is.ts new file mode 100644 index 000000000..54bf7fa0b --- /dev/null +++ b/apps/web-antd/src/utils/is.ts @@ -0,0 +1,125 @@ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +const toString = Object.prototype.toString; + +export const is = (val: unknown, type: string) => { + return toString.call(val) === `[object ${type}]`; +}; + +export const isDef = (val?: T): val is T => { + return val !== undefined; +}; + +export const isUnDef = (val?: T): val is T => { + return !isDef(val); +}; + +export const isObject = (val: any): val is Record => { + return val !== null && is(val, 'Object'); +}; + +export const isEmpty = (val: any): boolean => { + if (val === null || val === undefined || val === undefined) { + return true; + } + if (isArray(val) || isString(val)) { + return val.length === 0; + } + + if (val instanceof Map || val instanceof Set) { + return val.size === 0; + } + + if (isObject(val)) { + return Object.keys(val).length === 0; + } + + return false; +}; + +export const isDate = (val: unknown): val is Date => { + return is(val, 'Date'); +}; + +export const isNull = (val: unknown): val is null => { + return val === null; +}; + +export const isNullAndUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) && isNull(val); +}; + +export const isNullOrUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) || isNull(val); +}; + +export const isNumber = (val: unknown): val is number => { + return is(val, 'Number'); +}; + +export const isPromise = (val: unknown): val is Promise => { + return ( + is(val, 'Promise') && + isObject(val) && + isFunction(val.then) && + isFunction(val.catch) + ); +}; + +export const isString = (val: unknown): val is string => { + return is(val, 'String'); +}; + +export const isFunction = (val: unknown): val is Function => { + return typeof val === 'function'; +}; + +export const isBoolean = (val: unknown): val is boolean => { + return is(val, 'Boolean'); +}; + +export const isRegExp = (val: unknown): val is RegExp => { + return is(val, 'RegExp'); +}; + +export const isArray = (val: any): val is Array => { + return val && Array.isArray(val); +}; + +export const isWindow = (val: any): val is Window => { + return typeof window !== 'undefined' && is(val, 'Window'); +}; + +export const isElement = (val: unknown): val is Element => { + return isObject(val) && !!val.tagName; +}; + +export const isMap = (val: unknown): val is Map => { + return is(val, 'Map'); +}; + +export const isServer = typeof window === 'undefined'; + +export const isClient = !isServer; + +export const isUrl = (path: string): boolean => { + // fix:修复hash路由无法跳转的问题 + const reg = + // eslint-disable-next-line regexp/no-unused-capturing-group, regexp/no-useless-quantifier, regexp/no-super-linear-backtracking + /(((^https?:(?:\/\/)?)(?:[-:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%#/.\w-]*)?\??[-+=&%@.\w]*(?:#\w*)?)?)$/; + return reg.test(path); +}; + +export const isDark = (): boolean => { + return window.matchMedia('(prefers-color-scheme: dark)').matches; +}; + +// 是否是图片链接 +export const isImgPath = (path: string): boolean => { + return /(?:https?:\/\/|data:image\/).*?\.(?:png|jpg|jpeg|gif|svg|webp|ico)/i.test( + path, + ); +}; + +export const isEmptyVal = (val: any): boolean => { + return val === '' || val === null || val === undefined; +}; diff --git a/apps/web-antd/src/views/infra/build/index.vue b/apps/web-antd/src/views/infra/build/index.vue new file mode 100644 index 000000000..d5369f1c4 --- /dev/null +++ b/apps/web-antd/src/views/infra/build/index.vue @@ -0,0 +1,193 @@ + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c71501d78..48b7d6442 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,12 @@ catalogs: '@faker-js/faker': specifier: ^9.6.0 version: 9.6.0 + '@form-create/ant-design-vue': + specifier: ^3.2.22 + version: 3.2.22 + '@form-create/antd-designer': + specifier: ^3.2.11 + version: 3.2.11 '@iconify/json': specifier: ^2.2.324 version: 2.2.324 @@ -495,6 +501,9 @@ catalogs: vitest: specifier: ^2.1.9 version: 2.1.9 + vue-dompurify-html: + specifier: ^5.2.0 + version: 5.2.0 vue-eslint-parser: specifier: ^9.4.3 version: 9.4.3 @@ -662,9 +671,18 @@ importers: apps/web-antd: dependencies: + '@form-create/ant-design-vue': + specifier: 'catalog:' + version: 3.2.22(vue@3.5.13(typescript@5.8.3)) + '@form-create/antd-designer': + specifier: 'catalog:' + version: 3.2.11(vue@3.5.13(typescript@5.8.3)) '@tinymce/tinymce-vue': specifier: 'catalog:' version: 6.1.0(tinymce@7.8.0)(vue@3.5.13(typescript@5.8.3)) + '@types/lodash.clonedeep': + specifier: 'catalog:' + version: 4.5.9 '@vben/access': specifier: workspace:* version: link:../../packages/effects/access @@ -713,6 +731,9 @@ importers: ant-design-vue: specifier: 'catalog:' version: 4.2.6(vue@3.5.13(typescript@5.8.3)) + axios: + specifier: 'catalog:' + version: 1.8.4 crypto-js: specifier: 'catalog:' version: 4.2.0 @@ -722,12 +743,18 @@ importers: highlight.js: specifier: 'catalog:' version: 11.11.1 + lodash.clonedeep: + specifier: 'catalog:' + version: 4.5.0 pinia: specifier: ^2.3.1 version: 2.3.1(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)) vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.8.3) + vue-dompurify-html: + specifier: 'catalog:' + version: 5.2.0(vue@3.5.13(typescript@5.8.3)) vue-router: specifier: 'catalog:' version: 4.5.0(vue@3.5.13(typescript@5.8.3)) @@ -2621,6 +2648,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime-corejs3@7.27.0': + resolution: {integrity: sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.26.9': resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} engines: {node: '>=6.9.0'} @@ -3526,6 +3557,39 @@ packages: '@floating-ui/vue@1.1.6': resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==} + '@form-create/ant-design-vue@3.2.22': + resolution: {integrity: sha512-A848lhCnNnQCPq/aGLxxJTIhy+vL9h7+YCU2tx1rvqyxmGAYQlJgMy0ET3MpL88eK5DkuB2eyZE5Pt4i4vybeg==} + peerDependencies: + vue: ^3.5.13 + + '@form-create/antd-designer@3.2.11': + resolution: {integrity: sha512-i2MijDIAJeMWfcikqX1LVonWs85K6q5ZoQdOSXAD4MhiFGovQXBTXIenSEzRUlBlGgkwr8u1PeblMO8jCt4sXg==} + peerDependencies: + vue: ^3.5.13 + + '@form-create/component-antdv-frame@3.2.18': + resolution: {integrity: sha512-b6qGkqJnA9JlSnOvEMnkyfcPLg31oSl79i7yytJ3BLCUR8igyNO4O81dhFt4lWoaDe69LQWAtyDLEiF2Ls4OoA==} + + '@form-create/component-antdv-group@3.2.22': + resolution: {integrity: sha512-HfQw5cf7+ikcAXW++T3bLs4yocM9BJH10OgcEy5R0mo2fZfTQ49MCeUhhAUlpoHtJz/4nRijfyhQ6j+D6oAK4g==} + + '@form-create/component-antdv-upload@3.2.18': + resolution: {integrity: sha512-cobjChcblnfO0ph4MunJDUiBLyRwpzekXo6MFRsB5iq9ln73UjLnyLps4YuM2KRZ/Cn9FEoWN1kYvTFf1zKdjg==} + + '@form-create/component-subform@3.1.34': + resolution: {integrity: sha512-OJcFH/7MTHx7JLEjDK/weS27qfuFWAI+OK+gXTJ2jIt9aZkGWF/EWkjetiJLt5a0KMw4Z15wOS2XCY9pVK9vlA==} + + '@form-create/component-wangeditor@3.2.14': + resolution: {integrity: sha512-N/U/hFBdBu2OIguxoKe1Kslq5fW6XmtyhKDImLfKLn1xI6X5WUtt3r7QTaUPcVUl2vntpM9wJ/FBdG17RzF/Dg==} + + '@form-create/core@3.2.22': + resolution: {integrity: sha512-GC3b4Yrpy9TiPLqJFL9fiUFPjEv6ZBcHnOMB+GeF6iLsMV4TpZc0o/oFBPlhZqIYeljaNuxJyO2ABCStceOrZQ==} + peerDependencies: + vue: ^3.5.13 + + '@form-create/utils@3.2.18': + resolution: {integrity: sha512-C98bFPdFVMltiHQvEZqv4rVdhcqthJgvxMbWDlniL03HS5oyusnUvxUE8jf0I9zk5dZRDGmxKOUtzE3JDWP9nQ==} + '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -5502,6 +5566,10 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + codemirror@6.65.7: + resolution: {integrity: sha512-HcfnUFJwI2FvH73YWVbbMh7ObWxZiHIycEhv9ZEXy6e8ZKDjtZKbbYFUtsLN46HFXPvU5V2Uvc2d55Z//oFW5A==} + deprecated: This is an accidentally mis-tagged instance of 5.65.7 + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -5653,6 +5721,9 @@ packages: core-js-compat@3.41.0: resolution: {integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==} + core-js-pure@3.41.0: + resolution: {integrity: sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q==} + core-js@3.41.0: resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==} @@ -6079,6 +6150,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.2.5: + resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -9418,6 +9492,9 @@ packages: resolution: {integrity: sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sortablejs@1.14.0: + resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==} + sortablejs@1.15.6: resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==} @@ -10415,6 +10492,11 @@ packages: '@vue/composition-api': optional: true + vue-dompurify-html@5.2.0: + resolution: {integrity: sha512-GX+BStkKEJ8wu/+hU1EK2nu/gzXWhb4XzBu6aowpsuU/3nkvXvZ2jx4nZ9M3jtS/Vu7J7MtFXjc7x3cWQ+zbVQ==} + peerDependencies: + vue: ^3.5.13 + vue-eslint-parser@9.4.3: resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -10462,6 +10544,11 @@ packages: typescript: optional: true + vuedraggable@4.1.0: + resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} + peerDependencies: + vue: ^3.5.13 + vueuc@0.4.64: resolution: {integrity: sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA==} peerDependencies: @@ -10473,6 +10560,9 @@ packages: vxe-table@4.12.5: resolution: {integrity: sha512-VeCEmDbXeNKSvEXXfmKnB0QxUSW0FG9y7CzXhWFxnXR6Aqj4u7qauDipHV4sxcTHspcjiskPJ1MA9BkypkFtBA==} + wangeditor@4.7.15: + resolution: {integrity: sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==} + warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} @@ -11578,6 +11668,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime-corejs3@7.27.0': + dependencies: + core-js-pure: 3.41.0 + regenerator-runtime: 0.14.1 + '@babel/runtime@7.26.9': dependencies: regenerator-runtime: 0.14.1 @@ -12566,6 +12661,55 @@ snapshots: - '@vue/composition-api' - vue + '@form-create/ant-design-vue@3.2.22(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@form-create/component-antdv-frame': 3.2.18 + '@form-create/component-antdv-group': 3.2.22 + '@form-create/component-antdv-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/antd-designer@3.2.11(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@form-create/ant-design-vue': 3.2.22(vue@3.5.13(typescript@5.8.3)) + '@form-create/component-wangeditor': 3.2.14 + '@form-create/utils': 3.2.18 + ant-design-vue: 4.2.6(vue@3.5.13(typescript@5.8.3)) + codemirror: 6.65.7 + element-plus: 2.9.7(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/component-antdv-frame@3.2.18': + dependencies: + '@form-create/utils': 3.2.18 + + '@form-create/component-antdv-group@3.2.22': + dependencies: + '@form-create/utils': 3.2.18 + + '@form-create/component-antdv-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': + dependencies: + wangeditor: 4.7.15 + + '@form-create/core@3.2.22(vue@3.5.13(typescript@5.8.3))': + dependencies: + '@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': {} '@humanfs/core@0.19.1': {} @@ -14812,6 +14956,8 @@ snapshots: cluster-key-slot@1.1.2: {} + codemirror@6.65.7: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -14940,6 +15086,8 @@ snapshots: dependencies: browserslist: 4.24.4 + core-js-pure@3.41.0: {} + core-js@3.41.0: {} core-util-is@1.0.3: {} @@ -15386,6 +15534,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.2.5: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 @@ -19049,6 +19201,8 @@ snapshots: ip-address: 9.0.5 smart-buffer: 4.2.0 + sortablejs@1.14.0: {} + sortablejs@1.15.6: {} source-map-js@1.2.1: {} @@ -20214,6 +20368,11 @@ snapshots: dependencies: vue: 3.5.13(typescript@5.8.3) + vue-dompurify-html@5.2.0(vue@3.5.13(typescript@5.8.3)): + dependencies: + dompurify: 3.2.5 + vue: 3.5.13(typescript@5.8.3) + vue-eslint-parser@9.4.3(eslint@9.24.0(jiti@2.4.2)): dependencies: debug: 4.4.0 @@ -20271,6 +20430,11 @@ snapshots: optionalDependencies: typescript: 5.8.3 + vuedraggable@4.1.0(vue@3.5.13(typescript@5.8.3)): + dependencies: + sortablejs: 1.14.0 + vue: 3.5.13(typescript@5.8.3) + vueuc@0.4.64(vue@3.5.13(typescript@5.8.3)): dependencies: '@css-render/vue3-ssr': 0.15.14(vue@3.5.13(typescript@5.8.3)) @@ -20294,6 +20458,12 @@ snapshots: transitivePeerDependencies: - vue + wangeditor@4.7.15: + dependencies: + '@babel/runtime': 7.26.9 + '@babel/runtime-corejs3': 7.27.0 + tslib: 2.8.1 + warning@4.0.3: dependencies: loose-envify: 1.4.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 517b29d61..79ebbafd8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,6 +39,8 @@ catalog: '@tanstack/vue-query': ^5.72.0 '@tanstack/vue-store': ^0.7.0 '@tinymce/tinymce-vue': ^6.1.0 + '@form-create/ant-design-vue': ^3.2.22 + '@form-create/antd-designer': ^3.2.11 '@types/archiver': ^6.0.3 '@types/eslint': ^9.6.1 '@types/html-minifier-terser': ^7.0.2 @@ -181,6 +183,7 @@ catalog: vitepress-plugin-group-icons: ^1.3.8 vitest: ^2.1.9 vue: ^3.5.13 + vue-dompurify-html: ^5.2.0 vue-eslint-parser: ^9.4.3 vue-i18n: ^11.1.3 vue-json-viewer: ^3.0.4