feat: 添加表单构建功能 formCreate

pull/79/head
dhb52 2025-04-22 23:31:13 +08:00
parent a6f25d477b
commit e7934d81a1
26 changed files with 2003 additions and 11 deletions

View File

@ -26,7 +26,11 @@
"#/*": "./src/*"
},
"dependencies": {
"@form-create/ant-design-vue": "catalog:",
"@form-create/antd-designer": "catalog:",
"@tinymce/tinymce-vue": "catalog:",
"@types/crypto-js": "catalog:",
"@types/lodash.clonedeep": "catalog:",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
@ -47,8 +51,10 @@
"crypto-js": "catalog:",
"dayjs": "catalog:",
"highlight.js": "catalog:",
"lodash.clonedeep": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-dompurify-html": "catalog:",
"vue-router": "catalog:"
},
"devDependencies": {

View File

@ -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';
@ -10,6 +11,7 @@ import '@vben/styles/antd';
import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
import { setupFormCreate } from '#/plugins/formCreate';
import { initComponentAdapter } from './adapter/component';
import App from './app.vue';
@ -39,7 +41,7 @@ async function bootstrap(namespace: string) {
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
// 配置 pinia-store
await initStores(app, { namespace });
// 安装权限指令
@ -52,6 +54,12 @@ async function bootstrap(namespace: string) {
// 配置路由及路由守卫
app.use(router);
// formCreate
setupFormCreate(app);
// vue-dompurify-html
app.use(VueDOMPurifyHTML);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);

View File

@ -0,0 +1,3 @@
export { useApiSelect } from './src/components/useApiSelect';
export { useFormCreateDesigner } from './src/useFormCreateDesigner';

View File

@ -0,0 +1,85 @@
<!-- 数据字典 Select 选择器 -->
<script lang="ts" setup>
import { computed, useAttrs } from 'vue';
import {
Checkbox,
CheckboxGroup,
Radio,
RadioGroup,
Select,
SelectOption,
} from 'ant-design-vue';
import {
getBoolDictOptions,
getIntDictOptions,
getStrDictOptions,
} from '#/utils/dict';
//
interface Props {
dictType: string; //
valueType?: 'bool' | 'int' | 'str'; //
selectType?: 'checkbox' | 'radio' | 'select'; // select checkbox radio
// eslint-disable-next-line vue/require-default-prop
formCreateInject?: any;
}
defineOptions({ name: 'DictSelect' });
const props = withDefaults(defineProps<Props>(), {
valueType: 'str',
selectType: 'select',
});
const attrs = useAttrs();
//
const getDictOptions = computed(() => {
switch (props.valueType) {
case 'bool': {
return getBoolDictOptions(props.dictType);
}
case 'int': {
return getIntDictOptions(props.dictType);
}
case 'str': {
return getStrDictOptions(props.dictType);
}
default: {
return [];
}
}
});
</script>
<template>
<Select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
<SelectOption
v-for="(dict, index) in getDictOptions"
:key="index"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
<RadioGroup v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
<Radio
v-for="(dict, index) in getDictOptions"
:key="index"
:value="dict.value"
>
{{ dict.label }}
</Radio>
</RadioGroup>
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
<Checkbox
v-for="(dict, index) in getDictOptions"
:key="index"
:value="dict.value"
>
{{ dict.label }}
</Checkbox>
</CheckboxGroup>
</template>

View File

@ -0,0 +1,290 @@
import type { ApiSelectProps } from '#/components/FormCreate/src/type';
import { defineComponent, onMounted, ref, useAttrs } from 'vue';
import { isEmpty } from '@vben/utils';
import {
Checkbox,
CheckboxGroup,
Radio,
RadioGroup,
Select,
SelectOption,
} from 'ant-design-vue';
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 (
<Select
class="w-1/1"
loading={loading.value}
mode="multiple"
{...attrs}
// TODO: remote 对等实现
// remote={props.remote}
{...(props.remote && { remoteMethod })}
>
{options.value.map(
(item: { label: any; value: any }, index: any) => (
<SelectOption key={index} value={item.value}>
{item.label}
</SelectOption>
),
)}
</Select>
);
}
return (
<Select
class="w-1/1"
loading={loading.value}
{...attrs}
// TODO: @dhb52 remote 对等实现, 还是说没作用
// remote={props.remote}
{...(props.remote && { remoteMethod })}
>
{options.value.map(
(item: { label: any; value: any }, index: any) => (
<SelectOption key={index} value={item.value}>
{item.label}
</SelectOption>
),
)}
</Select>
);
};
const buildCheckbox = () => {
if (isEmpty(options.value)) {
options.value = [
{ label: '选项1', value: '选项1' },
{ label: '选项2', value: '选项2' },
];
}
return (
<CheckboxGroup class="w-1/1" {...attrs}>
{options.value.map(
(item: { label: any; value: any }, index: any) => (
<Checkbox key={index} value={item.value}>
{item.label}
</Checkbox>
),
)}
</CheckboxGroup>
);
};
const buildRadio = () => {
if (isEmpty(options.value)) {
options.value = [
{ label: '选项1', value: '选项1' },
{ label: '选项2', value: '选项2' },
];
}
return (
<RadioGroup class="w-1/1" {...attrs}>
{options.value.map(
(item: { label: any; value: any }, index: any) => (
<Radio key={index} value={item.value}>
{item.label}
</Radio>
),
)}
</RadioGroup>
);
};
return () => (
<>
{(() => {
switch (props.selectType) {
case 'checkbox': {
return buildCheckbox();
}
case 'radio': {
return buildRadio();
}
case 'select': {
return buildSelect();
}
default: {
return buildSelect();
}
}
})()}
</>
);
},
});
};

View File

@ -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';

View File

@ -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 };

View File

@ -0,0 +1,71 @@
import { onMounted, ref } from 'vue';
import { buildUUID } from '@vben/utils';
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';
/**
* 使使 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.SystemDictType) => ({
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,
]);
},
};
};

View File

@ -0,0 +1,36 @@
import { buildUUID } from '@vben/utils';
import {
localeProps,
makeRequiredRule,
} from '#/components/FormCreate/src/utils';
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: '是否只读' },
]);
},
};
};

View File

@ -0,0 +1,47 @@
import type { SelectRuleOption } from '#/components/FormCreate/src/type';
import { buildUUID } from '@vben/utils';
import cloneDeep from 'lodash.clonedeep';
import { selectRule } from '#/components/FormCreate/src/config/selectRule';
import {
localeProps,
makeRequiredRule,
} from '#/components/FormCreate/src/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: buildUUID(),
title: label,
info: '',
$required: false,
};
},
props(_: any, { t }: any) {
if (!option.props) {
option.props = [];
}
return localeProps(t, `${name}.props`, [
makeRequiredRule(),
...option.props,
...rules,
]);
},
};
};

View File

@ -0,0 +1,84 @@
import { buildUUID } from '@vben/utils';
import {
localeProps,
makeRequiredRule,
} from '#/components/FormCreate/src/utils';
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,
},
]);
},
};
};

View File

@ -0,0 +1,93 @@
import { buildUUID } from '@vben/utils';
import {
localeProps,
makeRequiredRule,
} from '#/components/FormCreate/src/utils';
export const useUploadImgRule = () => {
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,
},
]);
},
};
};

View File

@ -0,0 +1,89 @@
import { buildUUID } from '@vben/utils';
import {
localeProps,
makeRequiredRule,
} from '#/components/FormCreate/src/utils';
export const useUploadImgsRule = () => {
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',
},
]);
},
};
};

View File

@ -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<Menu>;
// 拖拽组件的规则
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[]; // 事件配置
}

View File

@ -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();
});
};

View File

@ -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<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);
});
}
};

View File

@ -33,7 +33,7 @@ import {
type InitOptions = IPropTypes['init'];
defineOptions({ inheritAttrs: false });
defineOptions({ name: 'Tinymce', inheritAttrs: false });
const props = defineProps({
options: {
@ -157,11 +157,11 @@ const initOptions = computed((): InitOptions => {
const { httpRequest } = useUpload();
httpRequest(file)
.then((url) => {
console.log('tinymce 上传图片成功:', url);
// console.log('tinymce :', url);
resolve(url);
})
.catch((error) => {
console.error('tinymce 上传图片失败:', error);
// console.error('tinymce :', error);
reject(error.message);
});
});

View File

@ -0,0 +1,272 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosResponse } from '@vben/request';
import type { AxiosProgressEvent } from '#/api/infra/file';
import { ref, toRefs, watch } from 'vue';
import { CloudUpload } from '@vben/icons';
import { $t } from '@vben/locales';
import { isFunction, isObject, isString } from '@vben/utils';
import { message, Modal, Upload } from 'ant-design-vue';
import { checkImgType, defaultImageAccepts } from './helper';
import { UploadResultStatus } from './typing';
import { useUpload, useUploadType } from './use-upload';
type ListType = 'picture' | 'picture-card' | 'text';
defineOptions({ name: 'ImagesUpload', inheritAttrs: false });
const props = withDefaults(
defineProps<{
//
accept?: string[];
api?: (
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
disabled?: boolean;
helpText?: string;
listType?: ListType;
// Infinity
maxNumber?: number;
// MB
maxSize?: number;
//
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
//
showDescription?: boolean;
value?: string | string[];
}>(),
{
value: () => [],
disabled: false,
listType: 'picture-card',
helpText: '',
maxSize: 2,
maxNumber: 5,
accept: () => defaultImageAccepts,
multiple: true,
api: useUpload().httpRequest,
resultField: '',
showDescription: true,
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
const previewOpen = ref<boolean>(false); //
const previewImage = ref<string>(''); //
const previewTitle = ref<string>(''); //
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); //
const isActMsg = ref<boolean>(true); //
const isFirstRender = ref<boolean>(true); //
watch(
() => props.value,
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
let value: string | string[] = [];
if (v) {
if (Array.isArray(v)) {
value = v;
} else {
value.push(v);
}
fileList.value = value.map((item, i) => {
if (item && isString(item)) {
return {
uid: `${-i}`,
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: UploadResultStatus.DONE,
url: item,
};
} else if (item && isObject(item)) {
return item;
}
return null;
}) as UploadProps['fileList'];
}
if (!isFirstRender.value) {
emit('change', value);
isFirstRender.value = false;
}
},
{
immediate: true,
deep: true,
},
);
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
return new Promise<T>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => {
resolve(reader.result as T);
});
reader.addEventListener('error', (error) => reject(error));
});
}
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64<string>(file.originFileObj!);
}
previewImage.value = file.url || file.preview || '';
previewOpen.value = true;
previewTitle.value =
file.name ||
previewImage.value.slice(
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
);
};
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
emit('delete', file);
}
};
const handleCancel = () => {
previewOpen.value = false;
previewTitle.value = '';
};
const beforeUpload = async (file: File) => {
const { maxSize, accept } = props;
const isAct = checkImgType(file, accept);
if (!isAct) {
message.error($t('ui.upload.acceptUpload', [accept]));
isActMsg.value = false;
//
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
message.error($t('ui.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
//
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!api || !isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
}
try {
//
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess'));
//
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response;
}
return item?.url || item?.response?.url || item?.response;
});
// add by String
if (props.maxNumber === 1) {
return list.length > 0 ? list[0] : '';
}
return list;
}
</script>
<template>
<div>
<Upload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
:before-upload="beforeUpload"
:custom-request="customRequest"
:disabled="disabled"
:list-type="listType"
:max-count="maxNumber"
:multiple="multiple"
:progress="{ showInfo: true }"
@preview="handlePreview"
@remove="handleRemove"
>
<div
v-if="fileList && fileList.length < maxNumber"
class="flex flex-col items-center justify-center"
>
<CloudUpload />
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
</div>
</Upload>
<div
v-if="showDescription"
class="mt-2 flex flex-wrap items-center text-[14px]"
>
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
</div>
<Modal
:footer="null"
:open="previewOpen"
:title="previewTitle"
@cancel="handleCancel"
>
<img :src="previewImage" alt="" class="w-full" />
</Modal>
</div>
</template>
<style>
.ant-upload-select-picture-card {
@apply flex items-center justify-center;
}
</style>

View File

@ -1,2 +1,3 @@
export { default as FileUpload } from './file-upload.vue';
export { default as ImageUpload } from './image-upload.vue';
export { default as ImagesUpload } from './images-upload.vue';

View File

@ -0,0 +1,5 @@
{
"copy": "Copy",
"copySuccess": "Copy Success",
"copyError": "Copy Error"
}

View File

@ -0,0 +1,5 @@
{
"copy": "复制",
"copySuccess": "复制成功",
"copyError": "复制失败"
}

View File

@ -0,0 +1,49 @@
import type { App } from 'vue';
// import install from '@form-create/ant-design-vue/auto-import';
import FcDesigner from '@form-create/antd-designer';
import Antd from 'ant-design-vue';
// ======================= 自定义组件 =======================
import { useApiSelect } from '#/components/FormCreate';
import DictSelect from '#/components/FormCreate/src/components/DictSelect.vue';
import { Tinymce } from '#/components/tinymce';
import { FileUpload, ImagesUpload, 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 components = [
ImageUpload,
ImagesUpload,
FileUpload,
Tinymce,
DictSelect,
UserSelect,
DeptSelect,
ApiSelect,
];
// TODO: @dhb52 按需导入而不是app.use(Antd);
// 参考 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);
});
app.use(Antd);
app.use(FcDesigner);
app.use(FcDesigner.formCreate);
};

View File

@ -6,7 +6,28 @@ import { isObject } from '@vben/utils';
import { useDictStore } from '#/store';
const dictStore = useDictStore();
// TODO @dhb52top-level 调用 导致:"getActivePinia()" was called but there was no active Pinia
// 先临时移入到方法中
// const dictStore = useDictStore();
// TODO @dhb: antd 组件的 color 类型
type ColorType = 'error' | 'info' | 'success' | 'warning';
export interface DictDataType {
dictType: string;
label: string;
value: boolean | number | string;
colorType: ColorType;
cssClass: string;
}
export interface NumberDictDataType extends DictDataType {
value: number;
}
export interface StringDictDataType extends DictDataType {
value: string;
}
/**
*
@ -16,6 +37,7 @@ const dictStore = useDictStore();
* @returns
*/
function getDictLabel(dictType: string, value: any) {
const dictStore = useDictStore();
const dictObj = dictStore.getDictData(dictType, value);
return isObject(dictObj) ? dictObj.label : '';
}
@ -28,6 +50,7 @@ function getDictLabel(dictType: string, value: any) {
* @returns
*/
function getDictObj(dictType: string, value: any) {
const dictStore = useDictStore();
const dictObj = dictStore.getDictData(dictType, value);
return isObject(dictObj) ? dictObj : null;
}
@ -42,6 +65,7 @@ function getDictOptions(
dictType: string,
valueType: 'boolean' | 'number' | 'string' = 'string',
) {
const dictStore = useDictStore();
const dictOpts = dictStore.getDictOptions(dictType);
const dictOptions: DefaultOptionType = [];
if (dictOpts.length > 0) {
@ -71,6 +95,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 图片状态
@ -205,4 +271,4 @@ enum DICT_TYPE {
TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
USER_TYPE = 'user_type',
}
export { DICT_TYPE, getDictObj, getDictLabel, getDictOptions };
export { DICT_TYPE, getDictLabel, getDictObj, getDictOptions };

View File

@ -0,0 +1,192 @@
<!-- 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 formCreate from '@form-create/ant-design-vue';
import FcDesigner from '@form-create/antd-designer';
import { useClipboard } from '@vueuse/core';
import { Button, message } from 'ant-design-vue';
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/FormCreate';
import { $t } from '#/locales';
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 - JSON1 - Options2 -
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)) {
message.success($t('common.copySuccess'));
}
} else {
message.error($t('common.copyError'));
}
};
/**
* 代码高亮
*/
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 || '&nbsp;';
};
/** 初始化 */
onMounted(async () => {
//
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('json', json);
});
</script>
<template>
<Page auto-content-height>
<div class="m-4">
<FcDesigner ref="designer" height="100vh" :config="designerConfig">
<template #handle>
<Button size="small" type="primary" ghost @click="showJson">
生成JSON
</Button>
<Button size="small" type="primary" ghost @click="showOption">
生成Options
</Button>
<Button size="small" type="primary" ghost @click="showTemplate">
生成组件
</Button>
</template>
</FcDesigner>
</div>
<!-- 弹窗表单预览 -->
<Modal
:title="dialogTitle"
:footer="false"
:fullscreen-button="false"
max-height="600"
>
<div>
<Button style="float: right" @click="copy(formData)">
{{ $t('common.copy') }}
</Button>
<div>
<pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
</div>
</div>
</Modal>
</Page>
</template>

View File

@ -30,6 +30,12 @@ catalogs:
'@faker-js/faker':
specifier: ^9.6.0
version: 9.7.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.330
@ -498,6 +504,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
@ -665,9 +674,21 @@ 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(vue@3.5.13(typescript@5.8.3))
'@types/crypto-js':
specifier: 'catalog:'
version: 4.2.2
'@types/lodash.clonedeep':
specifier: 'catalog:'
version: 4.5.9
'@vben/access':
specifier: workspace:*
version: link:../../packages/effects/access
@ -728,19 +749,21 @@ 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))
devDependencies:
'@types/crypto-js':
specifier: 'catalog:'
version: 4.2.2
apps/web-ele:
dependencies:
@ -2631,6 +2654,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.27.0':
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
@ -3548,6 +3575,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==}
@ -5444,6 +5504,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@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@ -5616,6 +5680,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==}
@ -6084,6 +6151,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==}
@ -9671,6 +9741,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==}
@ -10704,6 +10777,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}
@ -10751,6 +10829,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:
@ -10762,6 +10845,9 @@ packages:
vxe-table@4.13.7:
resolution: {integrity: sha512-nwybQ0uPgmAQOvw0gs4oiJ7ifUVCsW0grGyfMC+FdNxFxP2WuiziuKDztjAc16EgHgSDg+KpN4oZoHUAwC55tg==}
wangeditor@4.7.15:
resolution: {integrity: sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==}
warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
@ -11889,6 +11975,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.27.0':
dependencies:
regenerator-runtime: 0.14.1
@ -12892,6 +12983,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.8(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': {}
@ -15137,6 +15277,8 @@ snapshots:
cluster-key-slot@1.1.2: {}
codemirror@6.65.7: {}
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@ -15290,6 +15432,8 @@ snapshots:
dependencies:
browserslist: 4.24.4
core-js-pure@3.41.0: {}
core-js@3.41.0: {}
core-util-is@1.0.3: {}
@ -15793,6 +15937,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
@ -19703,6 +19851,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: {}
@ -20919,6 +21069,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.25.1(jiti@2.4.2)):
dependencies:
debug: 4.4.0
@ -20976,6 +21131,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))
@ -20999,6 +21159,12 @@ snapshots:
transitivePeerDependencies:
- vue
wangeditor@4.7.15:
dependencies:
'@babel/runtime': 7.27.0
'@babel/runtime-corejs3': 7.27.0
tslib: 2.8.1
warning@4.0.3:
dependencies:
loose-envify: 1.4.0

View File

@ -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
@ -182,6 +184,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