feat: 增加 FormCreate 初始功能

pull/76/head
dhb52 2025-04-20 20:41:09 +08:00
parent c3358652fd
commit 1e07c9ee12
29 changed files with 1880 additions and 3 deletions

View File

@ -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:"
}
}

View File

@ -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 = {

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

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,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<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}
// 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 () => (
<>
{props.selectType === 'select'
? buildSelect()
: props.selectType === 'radio'
? buildRadio()
: // eslint-disable-next-line unicorn/no-nested-ternary
props.selectType === 'checkbox'
? buildCheckbox()
: 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,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,
]);
},
};
};

View File

@ -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: '是否只读' },
]);
},
};
};

View File

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

View File

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

View File

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

View File

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

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

@ -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,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<Element>) => {
components.forEach((component) => {
app.component(component.name as string, component);
});
// formCreate.use(install);
app.use(formCreate);
app.use(FcDesigner);
};

View File

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

View File

@ -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 芋艿:不展示
// export default routes; // update by 芋艿:不展示

View File

@ -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: {

View File

@ -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 图片状态

View File

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

View File

@ -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 = <T = unknown>(val?: T): val is T => {
return val !== undefined;
};
export const isUnDef = <T = unknown>(val?: T): val is T => {
return !isDef(val);
};
export const isObject = (val: any): val is Record<any, any> => {
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 = <T = any>(val: unknown): val is Promise<T> => {
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<any> => {
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<any, any> => {
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;
};

View File

@ -0,0 +1,193 @@
<!-- eslint-disable no-useless-escape -->
<script setup lang="ts">
import { onMounted, ref, unref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
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 { isString } from '#/utils/is';
import 'highlight.js/styles/github.css';
defineOptions({ name: 'InfraBuild' });
const [Modal, modalApi] = useVbenModal();
const designer = ref(); //
useFormCreateDesigner(designer);
//
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.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

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