feat: 添加表单构建功能 formCreate
parent
a6f25d477b
commit
e7934d81a1
|
@ -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": {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export { useApiSelect } from './src/components/useApiSelect';
|
||||
|
||||
export { useFormCreateDesigner } from './src/useFormCreateDesigner';
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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';
|
|
@ -0,0 +1,182 @@
|
|||
/* eslint-disable no-template-curly-in-string */
|
||||
const selectRule = [
|
||||
{
|
||||
type: 'select',
|
||||
field: 'selectType',
|
||||
title: '选择器类型',
|
||||
value: 'select',
|
||||
options: [
|
||||
{ label: '下拉框', value: 'select' },
|
||||
{ label: '单选框', value: 'radio' },
|
||||
{ label: '多选框', value: 'checkbox' },
|
||||
],
|
||||
// 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性
|
||||
control: [
|
||||
{
|
||||
value: 'select',
|
||||
condition: '==',
|
||||
method: 'hidden',
|
||||
rule: [
|
||||
'multiple',
|
||||
'clearable',
|
||||
'collapseTags',
|
||||
'multipleLimit',
|
||||
'allowCreate',
|
||||
'filterable',
|
||||
'noMatchText',
|
||||
'remote',
|
||||
'remoteMethod',
|
||||
'reserveKeyword',
|
||||
'defaultFirstOption',
|
||||
'automaticDropdown',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'filterable',
|
||||
title: '是否可搜索',
|
||||
},
|
||||
{ type: 'switch', field: 'multiple', title: '是否多选' },
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否禁用',
|
||||
},
|
||||
{ type: 'switch', field: 'clearable', title: '是否可以清空选项' },
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'collapseTags',
|
||||
title: '多选时是否将选中值按文字的形式展示',
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'multipleLimit',
|
||||
title: '多选时用户最多可以选择的项目数,为 0 则不限制',
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'autocomplete',
|
||||
title: 'autocomplete 属性',
|
||||
},
|
||||
{ type: 'input', field: 'placeholder', title: '占位符' },
|
||||
{ type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
|
||||
{
|
||||
type: 'input',
|
||||
field: 'noMatchText',
|
||||
title: '搜索条件无匹配时显示的文字',
|
||||
},
|
||||
{ type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'reserveKeyword',
|
||||
title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'defaultFirstOption',
|
||||
title: '在输入框按下回车,选择第一个匹配项',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'popperAppendToBody',
|
||||
title: '是否将弹出框插入至 body 元素',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'automaticDropdown',
|
||||
title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单',
|
||||
},
|
||||
];
|
||||
|
||||
const apiSelectRule = [
|
||||
{
|
||||
type: 'input',
|
||||
field: 'url',
|
||||
title: 'url 地址',
|
||||
props: {
|
||||
placeholder: '/system/user/simple-list',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'method',
|
||||
title: '请求类型',
|
||||
value: 'GET',
|
||||
options: [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
],
|
||||
control: [
|
||||
{
|
||||
value: 'GET',
|
||||
condition: '!=',
|
||||
method: 'hidden',
|
||||
rule: [
|
||||
{
|
||||
type: 'input',
|
||||
field: 'data',
|
||||
title: '请求参数 JSON 格式',
|
||||
props: {
|
||||
autosize: true,
|
||||
type: 'textarea',
|
||||
placeholder: '{"type": 1}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'labelField',
|
||||
title: 'label 属性',
|
||||
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
|
||||
props: {
|
||||
placeholder: 'nickname',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'valueField',
|
||||
title: 'value 属性',
|
||||
info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}',
|
||||
props: {
|
||||
placeholder: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'parseFunc',
|
||||
title: '选项解析函数',
|
||||
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
|
||||
(data: any)=>{ label: string; value: any }[]`,
|
||||
props: {
|
||||
autosize: true,
|
||||
rows: { minRows: 2, maxRows: 6 },
|
||||
type: 'textarea',
|
||||
placeholder: `
|
||||
function (data) {
|
||||
console.log(data)
|
||||
return data.list.map(item=> ({label: item.nickname,value: item.id}))
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'remote',
|
||||
info: '是否可搜索',
|
||||
title: '其中的选项是否从服务器远程加载',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'remoteField',
|
||||
title: '请求参数',
|
||||
info: '远程请求时请求携带的参数名称,如:name',
|
||||
},
|
||||
];
|
||||
|
||||
export { apiSelectRule, selectRule };
|
|
@ -0,0 +1,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,
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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: '是否只读' },
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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[]; // 事件配置
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"copy": "Copy",
|
||||
"copySuccess": "Copy Success",
|
||||
"copyError": "Copy Error"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"copy": "复制",
|
||||
"copySuccess": "复制成功",
|
||||
"copyError": "复制失败"
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -6,7 +6,28 @@ import { isObject } from '@vben/utils';
|
|||
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
const dictStore = useDictStore();
|
||||
// TODO @dhb52:top-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 };
|
||||
|
|
|
@ -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 - 生成 JSON;1 - 生成 Options;2 - 生成组件
|
||||
const formData = ref(''); // 表单数据
|
||||
useFormCreateDesigner(designer); // 表单设计器增强
|
||||
|
||||
/** 打开弹窗 */
|
||||
const openModel = (title: string) => {
|
||||
dialogVisible.value = true;
|
||||
dialogTitle.value = title;
|
||||
modalApi.open();
|
||||
};
|
||||
|
||||
/** 生成 JSON */
|
||||
const showJson = () => {
|
||||
openModel('生成 JSON');
|
||||
formType.value = 0;
|
||||
formData.value = designer.value.getRule();
|
||||
};
|
||||
|
||||
/** 生成 Options */
|
||||
const showOption = () => {
|
||||
openModel('生成 Options');
|
||||
formType.value = 1;
|
||||
formData.value = designer.value.getOption();
|
||||
};
|
||||
|
||||
/** 生成组件 */
|
||||
const showTemplate = () => {
|
||||
openModel('生成组件');
|
||||
formType.value = 2;
|
||||
formData.value = makeTemplate();
|
||||
};
|
||||
|
||||
const makeTemplate = () => {
|
||||
const rule = designer.value.getRule();
|
||||
const opt = designer.value.getOption();
|
||||
return `<template>
|
||||
<form-create
|
||||
v-model:api="fApi"
|
||||
:rule="rule"
|
||||
:option="option"
|
||||
@submit="onSubmit"
|
||||
></form-create>
|
||||
</template>
|
||||
<script setup lang=ts>
|
||||
const faps = ref(null)
|
||||
const rule = ref('')
|
||||
const option = ref('')
|
||||
const init = () => {
|
||||
rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}')
|
||||
option.value = formCreate.parseJson('${JSON.stringify(opt, null, 2)}')
|
||||
}
|
||||
const onSubmit = (formData) => {
|
||||
//todo 提交表单
|
||||
}
|
||||
init()
|
||||
<\/script>`;
|
||||
};
|
||||
|
||||
/** 复制 */
|
||||
const copy = async (text: string) => {
|
||||
const textToCopy = JSON.stringify(text, null, 2);
|
||||
const { copy, copied, isSupported } = useClipboard({ source: textToCopy });
|
||||
if (isSupported) {
|
||||
await copy();
|
||||
if (unref(copied)) {
|
||||
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 || ' ';
|
||||
};
|
||||
|
||||
/** 初始化 */
|
||||
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>
|
174
pnpm-lock.yaml
174
pnpm-lock.yaml
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue