Pārlūkot izejas kodu

feat: 修改自定义窗体

DESKTOP-USV654P\pc 9 mēneši atpakaļ
vecāks
revīzija
32eb91751b

+ 1 - 1
apps/web-baicai/.env.development

@@ -4,7 +4,7 @@ VITE_PORT=5173
 VITE_BASE=/
 
 # 接口地址
-VITE_GLOB_API_URL=https://localhost:5001/api
+VITE_GLOB_API_URL=http://localhost:8000/api
 
 # 是否开启 Nitro Mock服务,true 为开启,false 为关闭
 VITE_NITRO_MOCK=true

+ 3 - 1
apps/web-baicai/package.json

@@ -48,9 +48,11 @@
     "@vueuse/core": "catalog:",
     "ant-design-vue": "catalog:",
     "dayjs": "catalog:",
+    "monaco-editor": "^0.52.2",
     "pinia": "catalog:",
     "sm-crypto-v2": "^1.9.2",
     "vue": "catalog:",
-    "vue-router": "catalog:"
+    "vue-router": "catalog:",
+    "vuedraggable": "^4.1.0"
   }
 }

+ 9 - 3
apps/web-baicai/src/api/request.ts

@@ -75,6 +75,7 @@ function createRequestClient(baseURL: string) {
       const { data: responseData, status } = response;
 
       const { code, data } = responseData;
+
       if (status >= 200 && status < 400 && code === 200) {
         return data;
       } else if (status >= 200 && status < 400 && code !== 200) {
@@ -101,9 +102,14 @@ function createRequestClient(baseURL: string) {
     errorMessageResponseInterceptor((msg: string, _error) => {
       // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
       const responseData = _error?.data ?? {};
-      const errorMessage = responseData?.error ?? responseData?.message ?? '';
-      // 如果没有错误信息,则会根据状态码进行提示
-      message.error(errorMessage || msg);
+      if (responseData?.code === 401) {
+        message.error('登录已过期,请重新登录');
+        doReAuthenticate();
+      } else {
+        const errorMessage = responseData?.error ?? responseData?.message ?? '';
+        // 如果没有错误信息,则会根据状态码进行提示
+        message.error(errorMessage || msg);
+      }
     }),
   );
 

+ 3 - 1
apps/web-baicai/src/components/form/components/api-popup.vue

@@ -1,7 +1,9 @@
 <script setup lang="ts">
 import type { SelectValue } from 'ant-design-vue/es/select';
 
-import { computed, type PropType, ref, unref, watch } from 'vue';
+import type { PropType } from 'vue';
+
+import { computed, ref, unref, watch } from 'vue';
 
 import { useVbenModal } from '@vben/common-ui';
 import { cn } from '@vben/utils';

+ 74 - 0
apps/web-baicai/src/components/form/components/input-code.vue

@@ -0,0 +1,74 @@
+<script setup lang="ts">
+import type { PropType } from 'vue';
+
+import { reactive, watch } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { useVModel } from '@vueuse/core';
+import { Input } from 'ant-design-vue';
+
+import { Icon } from '#/components/icon';
+
+import InputCode from './input-code/input-code-modal-ignore.vue';
+
+const props = defineProps({
+  value: {
+    type: [String, Object, Array] as PropType<
+      any[] | Record<string, any> | string
+    >,
+    default: undefined,
+  },
+});
+const emit = defineEmits(['update:value']);
+const modelValue = useVModel(props, 'value', emit, {
+  defaultValue: props.value,
+  passive: true,
+});
+
+const state = reactive({
+  value: '',
+});
+
+const [InputCodeModal, inputCodeApi] = useVbenModal({
+  connectedComponent: InputCode,
+});
+
+watch(
+  () => props.value,
+  () => {
+    if (props.value) {
+      state.value = '已配置';
+    }
+  },
+  { deep: true },
+);
+
+const handleSuccess = (scriptCode: any) => {
+  if (modelValue.value !== scriptCode) {
+    emit('update:value', scriptCode);
+  }
+};
+
+const handleInput = () => {
+  inputCodeApi.setData({
+    baseData: {
+      scriptCode: modelValue.value,
+      name: '验证规则',
+      // language: 'javascript',
+    },
+  });
+  inputCodeApi.open();
+};
+</script>
+
+<template>
+  <div class="w-full">
+    <InputCodeModal :close-on-click-modal="false" @success="handleSuccess" />
+    <Input v-model:value="state.value" class="w-full">
+      <template #addonAfter>
+        <Icon icon="proicons:nodejs" @click="handleInput" />
+      </template>
+    </Input>
+  </div>
+</template>

+ 116 - 0
apps/web-baicai/src/components/form/components/input-code/input-code-modal-ignore.vue

@@ -0,0 +1,116 @@
+<script lang="ts" setup>
+import { nextTick, onMounted, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import * as monaco from 'monaco-editor';
+// import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
+// import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
+// import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
+// import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
+// import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
+
+defineOptions({
+  name: 'InputCodePreview',
+});
+const emit = defineEmits(['success']);
+// const state = reactive<{
+//   language: string;
+// }>({
+//   language: 'javascript',
+// });
+
+// globalThis.MonacoEnvironment = {
+//   getWorker: (_: string, label: string) => {
+//     if (label === 'json') {
+//       return new JsonWorker();
+//     }
+//     if (['css', 'less', 'scss'].includes(label)) {
+//       return new CssWorker();
+//     }
+//     if (['handlebars', 'html', 'razor'].includes(label)) {
+//       return new HtmlWorker();
+//     }
+//     if (['javascript', 'typescript'].includes(label)) {
+//       return new TsWorker();
+//     }
+//     return new EditorWorker();
+//   },
+// };
+const monacoEditorRef = ref();
+// 初始化monacoEditor对象
+let monacoEditor: any = null;
+
+const initMonacoEditor = () => {
+  monacoEditor = monaco.editor.create(monacoEditorRef.value, {
+    theme: 'vs-dark', // 主题 vs vs-dark hc-black
+    value: '', // 默认显示的值
+    language: 'javascript',
+    formatOnPaste: true,
+    wordWrap: 'on', // 自动换行,注意大小写
+    wrappingIndent: 'indent',
+    folding: true, // 是否折叠
+    foldingHighlight: true, // 折叠等高线
+    foldingStrategy: 'indentation', // 折叠方式  auto | indentation
+    showFoldingControls: 'always', // 是否一直显示折叠 always | mouSEOver
+    disableLayerHinting: true, // 等宽优化
+    emptySelectionClipboard: false, // 空选择剪切板
+    selectionClipboard: false, // 选择剪切板
+    automaticLayout: true, // 自动布局
+    codeLens: false, // 代码镜头
+    scrollBeyondLastLine: false, // 滚动完最后一行后再滚动一屏幕
+    colorDecorators: true, // 颜色装饰器
+    accessibilitySupport: 'auto', // 辅助功能支持  "auto" | "off" | "on"
+    lineNumbers: 'on', // 行号 取值: "on" | "off" | "relative" | "interval" | function
+    lineNumbersMinChars: 5, // 行号最小字符   number
+    // enableSplitViewResizing: false,
+    readOnly: false, // 是否只读  取值 true | false
+  });
+};
+
+const [Modal, { close, setState, getData }] = useVbenModal({
+  draggable: true,
+  fullscreen: false,
+  onConfirm: async () => {
+    try {
+      setState({ confirmLoading: true });
+      const scriptCode = monacoEditor.getValue();
+
+      close();
+      emit('success', scriptCode);
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      if (monacoEditor === null) {
+        nextTick(() => {
+          initMonacoEditor();
+        });
+      }
+      nextTick(() => {
+        const { scriptCode, name, language } = data.baseData;
+        if (scriptCode) {
+          monacoEditor.setValue(scriptCode);
+        }
+
+        if (language) {
+          monaco.editor.setModelLanguage(monacoEditor.getModel()!, language);
+        }
+        setState({ title: `${name}`, loading: false });
+      });
+    }
+  },
+  title: ' 脚本编辑',
+});
+
+onMounted(() => {});
+</script>
+<template>
+  <Modal class="h-[800px] w-[1000px]">
+    <div ref="monacoEditorRef" class="h-full w-full"></div>
+  </Modal>
+</template>

+ 42 - 0
apps/web-baicai/src/utils/utils.ts

@@ -17,6 +17,14 @@ export const toPascalCase = (str: any) => {
   // 将处理后的单词拼接成一个新的字符串
   return pascalCaseWords.join('');
 };
+
+/**
+ * 驼峰转下划线
+ * @param str
+ */
+export function toLine(str: string) {
+  return str.replaceAll(/([A-Z])/g, '_$1').toLowerCase();
+}
 export const omit = (obj: any, keysToOmit: string[]) => {
   // 如果 obj 不是对象或者 keysToOmit 不是数组,则直接返回 obj
   if (typeof obj !== 'object' || !Array.isArray(keysToOmit)) {
@@ -99,3 +107,37 @@ export function getFileNameWithoutExtension(path: string) {
   const match = path.match(/[^/]+(?=\.[^./]+$|$)/);
   return match ? match[0].replace(/\.[^/.]+$/, '') : null;
 }
+
+const hexList: string[] = [];
+for (let i = 0; i <= 15; i++) {
+  hexList[i] = i.toString(16);
+}
+export function buildUUID(): string {
+  let uuid = '';
+  for (let i = 1; i <= 36; i++) {
+    switch (i) {
+      case 9:
+      case 14:
+      case 19:
+      case 24: {
+        uuid += '-';
+
+        break;
+      }
+      case 15: {
+        uuid += 4;
+
+        break;
+      }
+      case 20: {
+        uuid += hexList[(Math.random() * 4) | 8];
+
+        break;
+      }
+      default: {
+        uuid += hexList[Math.trunc(Math.random() * 16)];
+      }
+    }
+  }
+  return uuid.replaceAll('-', '');
+}

+ 210 - 0
apps/web-baicai/src/views/form/design/components/control-config.vue

@@ -0,0 +1,210 @@
+<script lang="ts" setup>
+import { watch } from 'vue';
+
+import { useVbenForm } from '@vben/common-ui';
+
+import { useFormDesignState } from '../hooks/useFormDesignState';
+
+const { formConfig } = useFormDesignState();
+
+const handleUpdateConfig = (record: any) => {
+  formConfig.value.currentItem = { ...formConfig.value.currentItem, ...record };
+};
+
+const [Form, { setValues, resetForm }] = useVbenForm({
+  showDefaultActions: false,
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  handleValuesChange: (values: Record<string, any>) => {
+    handleUpdateConfig(values);
+  },
+  schema: [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '输入控件类型',
+        disabled: true,
+      },
+      fieldName: 'component',
+      label: '控件类型',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '输入标题',
+      },
+      fieldName: 'label',
+      label: '标题',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入占位提示',
+      },
+      fieldName: 'componentProps.placeholder',
+      label: '占位提示',
+      dependencies: {
+        triggerFields: ['label'],
+        trigger(values) {
+          if (values?.componentProps?.placeholder) {
+            values.componentProps.placeholder = `请输入${values?.label}`;
+          }
+        },
+      },
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入绑定字段',
+      },
+      fieldName: 'fieldName',
+      label: '绑定字段',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入前置标签',
+      },
+      fieldName: 'componentProps.prefix',
+      label: '前置标签',
+      dependencies: {
+        triggerFields: ['component'],
+        show(values) {
+          return ['Input'].includes(values.component);
+        },
+      },
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入后置标签',
+      },
+      fieldName: 'componentProps.suffix',
+      label: '后置标签',
+      dependencies: {
+        triggerFields: ['component'],
+        show(values) {
+          return ['Input'].includes(values.component);
+        },
+      },
+    },
+    {
+      component: 'Switch',
+      componentProps: {
+        placeholder: '请输入显示字数',
+        class: 'w-auto',
+      },
+      fieldName: 'componentProps.showCount',
+      label: '显示字数',
+      dependencies: {
+        triggerFields: ['component'],
+        show(values) {
+          return ['Input', 'Textarea'].includes(values.component);
+        },
+      },
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入最大长度',
+      },
+      fieldName: 'componentProps.maxlength',
+      label: '最大长度',
+      dependencies: {
+        triggerFields: ['component'],
+        show(values) {
+          return ['Input', 'Textarea'].includes(values.component);
+        },
+      },
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入最大值',
+      },
+      fieldName: 'componentProps.max',
+      label: '最大值',
+      dependencies: {
+        triggerFields: ['component'],
+        show(values) {
+          return ['InputNumber'].includes(values.component);
+        },
+      },
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入最小值',
+      },
+      fieldName: 'componentProps.min',
+      label: '最小值',
+      dependencies: {
+        triggerFields: ['component'],
+        show(values) {
+          return ['InputNumber'].includes(values.component);
+        },
+      },
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入步长',
+      },
+      fieldName: 'componentProps.step',
+      label: '步长',
+      dependencies: {
+        triggerFields: ['component'],
+        show(values) {
+          return ['InputNumber'].includes(values.component);
+        },
+      },
+    },
+    {
+      component: 'Switch',
+      componentProps: {
+        placeholder: '请输入隐藏标签',
+        class: 'w-auto',
+      },
+      fieldName: 'hideLabel',
+      label: '隐藏标签',
+    },
+    {
+      component: 'InputCode',
+      componentProps: {
+        placeholder: '请输入验证规则',
+      },
+      fieldName: 'rules',
+      label: '验证规则',
+    },
+  ],
+  wrapperClass: 'grid-cols-1',
+});
+
+watch(
+  () => formConfig.value,
+  () => {
+    if (formConfig.value.currentItem) {
+      resetForm();
+      setValues({ ...formConfig.value.currentItem });
+      // formConfig.value.currentItem.itemProps =
+      //   formConfig.value.currentItem.itemProps || {};
+      // formConfig.value.currentItem.itemProps.labelCol =
+      //   formConfig.value.currentItem.itemProps.labelCol || {};
+      // formConfig.value.currentItem.itemProps.wrapperCol =
+      //   formConfig.value.currentItem.itemProps.wrapperCol || {};
+    }
+  },
+  { deep: true, immediate: true },
+);
+</script>
+<template>
+  <div>
+    <div v-if="!formConfig.currentItem?.key" class="form-edit-empty">
+      未选择控件
+    </div>
+    <Form v-else />
+  </div>
+</template>

+ 118 - 0
apps/web-baicai/src/views/form/design/components/control-mark.vue

@@ -0,0 +1,118 @@
+<script lang="ts" setup>
+import type { PropType } from 'vue';
+
+import type { IVFormComponent } from '../types';
+
+import Draggable from 'vuedraggable';
+
+import { Icon } from '#/components/icon';
+
+const props = defineProps({
+  fields: {
+    type: Array as PropType<Array<string>>,
+    required: true,
+  },
+  list: {
+    type: Array as PropType<IVFormComponent[]>,
+    required: true,
+  },
+  handleListPush: {
+    type: Function,
+    default: null,
+  },
+});
+const emit = defineEmits(['copy', 'start', 'addAttrs', 'handleListPush']);
+const handleClick = (element: any) => {
+  emit('handleListPush', element);
+};
+
+const handleClone = (element: any) => {
+  return props.handleListPush(element);
+};
+
+const handleStart = (element: any, data: IVFormComponent[]) => {
+  emit('start', data[element.oldIndex]?.component);
+};
+// const handleAdd = (element: any) => {
+//   console.log('handleAdd', element);
+// };
+
+const handleDragstart = (index: number) => {
+  emit('addAttrs', props.list, index);
+};
+</script>
+<template>
+  <div>
+    <Draggable
+      tag="ul"
+      :model-value="list"
+      v-bind="{
+        group: { name: 'form-draggable', pull: 'clone', put: false },
+        sort: false,
+        clone: handleClone,
+        animation: 180,
+        ghostClass: 'ghost',
+      }"
+      item-key="type"
+      class="control-mark"
+      @start="handleStart($event, list)"
+    >
+      <template #item="{ element, index }">
+        <li
+          v-if="fields.includes(element.component)"
+          class="control-mark-item"
+          :class="{ 'no-put': element.component === 'divider' }"
+          @dragstart="handleDragstart(index)"
+          @click="handleClick(element)"
+        >
+          <a>
+            <Icon :icon="element.icon" class="svg" />
+            <span>{{ element.label }}</span>
+          </a>
+        </li>
+      </template>
+    </Draggable>
+  </div>
+</template>
+
+<style scoped lang="less">
+.control-mark {
+  width: 100%;
+
+  &-item {
+    float: left;
+    width: 44%;
+    margin-right: 6%;
+    margin-bottom: 8px;
+    padding: 8px 0;
+    text-align: center;
+    font-size: 12px;
+    border: 1px solid #e4e4e4;
+
+    & a {
+      color: #333;
+
+      & svg {
+        width: 14px !important;
+        height: 14px !important;
+        margin-right: 5px;
+      }
+    }
+
+    &:hover {
+      border: 1px dashed #eef4ff;
+      background-color: #eef4ff;
+      cursor: v-bind("props.disabled ? 'not-allowed' : 'pointer'");
+
+      & a {
+        color: #0960bd;
+        cursor: v-bind("props.disabled ? 'not-allowed' : 'pointer'");
+      }
+    }
+  }
+}
+
+.clear {
+  clear: both;
+}
+</style>

+ 71 - 0
apps/web-baicai/src/views/form/design/components/form-config.vue

@@ -0,0 +1,71 @@
+<script lang="ts" setup>
+import type { BasicOptionResult } from '#/api/model';
+
+import { onMounted, reactive } from 'vue';
+
+import { useVbenForm } from '@vben/common-ui';
+
+import { useFormDesignState } from '../hooks/useFormDesignState';
+
+const { formConfig } = useFormDesignState();
+
+const state = reactive<{ gridCols: BasicOptionResult[] }>({
+  gridCols: [],
+});
+
+const handleUpdateConfig = (record: any) => {
+  formConfig.value.config = { ...formConfig.value.config, ...record };
+};
+
+const [Form, { setValues }] = useVbenForm({
+  showDefaultActions: false,
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  handleValuesChange: (values: Record<string, any>) => {
+    handleUpdateConfig(values);
+  },
+  schema: [
+    {
+      component: 'Select',
+      componentProps: {
+        placeholder: '请选择表单栅格',
+        options: state.gridCols,
+      },
+      fieldName: 'wrapperClass',
+      label: '表单栅格',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请选输入标签宽度',
+      },
+      fieldName: 'commonConfig.labelWidth',
+      label: '标签宽度',
+    },
+    {
+      component: 'Switch',
+      componentProps: {
+        placeholder: '请输入显示操作按钮',
+        class: 'w-auto',
+      },
+      fieldName: 'showDefaultActions',
+      label: '显示操作按钮',
+    },
+  ],
+  wrapperClass: 'grid-cols-1',
+});
+
+onMounted(() => {
+  for (let i = 0; i < 12; i++) {
+    state.gridCols.push({ label: `${i + 1}列`, value: `grid-cols-${i + 1}` });
+  }
+
+  setValues({ ...formConfig.value.config });
+});
+</script>
+<template>
+  <Form />
+</template>

+ 172 - 0
apps/web-baicai/src/views/form/design/components/form-edit-item.vue

@@ -0,0 +1,172 @@
+<script lang="ts" setup>
+import type { PropType } from 'vue';
+
+import type { IVFormComponent } from '../types';
+
+import { onMounted } from 'vue';
+
+import { Form, Input, InputNumber } from 'ant-design-vue';
+
+import { Icon } from '#/components/icon';
+
+import { useFormDesignState } from '../hooks/useFormDesignState';
+import { remove } from '../utils';
+
+const props = defineProps({
+  schema: {
+    type: Object as PropType<IVFormComponent>,
+    required: true,
+  },
+  formConfigSelect: {
+    type: Object as PropType<IVFormComponent>,
+    required: true,
+  },
+});
+
+const { formConfig, formDesignMethods } = useFormDesignState();
+
+/**
+ * 删除当前项
+ */
+const handleDelete = () => {
+  const traverse = (schemas: IVFormComponent[]) => {
+    schemas.some((formItem, index) => {
+      const { key } = formItem;
+
+      if (key === props.formConfigSelect.key) {
+        let params: IVFormComponent;
+        if (schemas.length === 1) {
+          params = { component: '' } as IVFormComponent;
+        } else {
+          params =
+            schemas.length - 1 > index
+              ? (schemas[index + 1] as IVFormComponent)
+              : (schemas[index - 1] as IVFormComponent);
+        }
+
+        // const params =
+        //   schemas.length === 1
+        //     ? ({ component: '' } as IVFormComponent)
+        //     : schemas.length - 1 > index
+        //       ? (schemas[index + 1] as IVFormComponent)
+        //       : (schemas[index - 1] as IVFormComponent);
+        formDesignMethods.handleSetSelectItem(params);
+        remove(schemas, index);
+        return true;
+      }
+      return false;
+    });
+  };
+  traverse(formConfig.value!.schemas);
+};
+
+onMounted(() => {});
+</script>
+<template>
+  <div
+    class="form-edit-item"
+    @click="formDesignMethods.handleSetSelectItem(schema)"
+  >
+    <Form.Item
+      class="form-edit-item-view"
+      :key="schema.key"
+      :label="schema.hideLabel ? '' : schema.label"
+      :required="!!schema.rules"
+      :class="{
+        active: formConfigSelect.key === schema.key,
+      }"
+    >
+      <Input
+        :placeholder="schema.componentProps?.placeholder"
+        :suffix="schema.componentProps?.suffix"
+        :prefix="schema.componentProps?.prefix"
+        :show-count="schema.componentProps?.showCount"
+        :maxlength="schema.componentProps?.maxlength"
+        v-if="schema.component === 'Input'"
+      />
+      <Input.TextArea
+        :placeholder="schema.componentProps?.placeholder"
+        :show-count="schema.componentProps?.showCount"
+        :maxlength="schema.componentProps?.maxlength"
+        v-if="schema.component === 'Textarea'"
+      />
+      <Input.Password
+        :placeholder="schema.componentProps?.placeholder"
+        v-if="schema.component === 'InputPassword'"
+      />
+      <InputNumber
+        :placeholder="schema.componentProps?.placeholder"
+        :min="schema.componentProps?.min"
+        :max="schema.componentProps?.max"
+        :step="schema.componentProps?.step"
+        style="width: 100%"
+        v-if="schema.component === 'InputNumber'"
+      />
+    </Form.Item>
+    <div
+      class="form-edit-item-action"
+      v-if="formConfigSelect.key === schema.key"
+    >
+      <div class="svgicon form-edit-item-delete">
+        <Icon icon="tdesign:delete" @click="handleDelete" />
+      </div>
+    </div>
+    <div class="form-edit-item-drag" v-if="formConfigSelect.key === schema.key">
+      <div class="svgicon form-edit-item-move">
+        <Icon icon="tdesign:drag-move" class="drag-widget" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="less">
+.form-edit-item {
+  position: relative;
+  width: 100%;
+
+  &-view {
+    padding: 10px;
+    cursor: pointer;
+    margin-bottom: 0;
+  }
+
+  & > div.active {
+    background: #eef4ff;
+  }
+
+  &-action {
+    position: absolute;
+    top: -12px;
+    right: 43px;
+  }
+
+  &-drag {
+    position: absolute;
+    top: -12px;
+    right: 10px;
+  }
+
+  &-delete {
+    border: 1px solid #f64c4c !important;
+    color: #f64c4c !important;
+  }
+
+  &-move {
+    border: 1px solid #90b665 !important;
+    color: #90b665 !important;
+  }
+
+  .svgicon {
+    color: #0960bd;
+    border: 1px solid #0960bd;
+    border-radius: 50%;
+    width: 24px;
+    height: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    margin-left: 10px;
+  }
+}
+</style>

+ 132 - 0
apps/web-baicai/src/views/form/design/components/form-edit.vue

@@ -0,0 +1,132 @@
+<script lang="ts" setup>
+import type { PropType } from 'vue';
+
+import type { IVFormComponent } from '../types';
+
+import { cloneDeep } from '@vben/utils';
+
+import { Form } from 'ant-design-vue';
+import Draggable from 'vuedraggable';
+
+import { useFormDesignState } from '../hooks/useFormDesignState';
+import formEditItem from './form-edit-item.vue';
+
+defineProps({
+  formConfigSelect: {
+    type: Object as PropType<IVFormComponent>,
+    required: true,
+  },
+});
+
+const emit = defineEmits(['change']);
+
+const { formConfig } = useFormDesignState();
+
+const handleMoveAdd = ({ newIndex }: any) => {
+  formConfig.value.schemas = formConfig.value.schemas || [];
+
+  const schemas = formConfig.value.schemas;
+  schemas[newIndex] = cloneDeep(schemas[newIndex]) as IVFormComponent;
+  emit('change', schemas[newIndex]);
+};
+
+/**
+ * 拖拽开始事件
+ * @param e {Object} 事件对象
+ */
+const handleDragStart = (e: any) => {
+  emit('change', formConfig.value.schemas[e.oldIndex]);
+};
+</script>
+<template>
+  <div class="form-edit">
+    <div class="form-edit-container">
+      <div v-if="formConfig.schemas.length === 0" class="form-edit-empty">
+        表单未添加组件
+      </div>
+      <Form
+        label-align="right"
+        :label-col="{
+          style: { width: `${formConfig.config.commonConfig.labelWidth}px` },
+        }"
+      >
+        <div class="form-edit-list">
+          <Draggable
+            class="grid items-start"
+            group="form-draggable"
+            :class="formConfig.config.wrapperClass"
+            :component-data="{
+              name: 'fade',
+              tag: 'div',
+              type: 'transition-group',
+            }"
+            ghost-class="ghost"
+            :animation="180"
+            handle=".drag-widget"
+            v-model="formConfig.schemas"
+            item-key="key"
+            @add="handleMoveAdd"
+            @start="handleDragStart"
+          >
+            <template #item="{ element, index }">
+              <formEditItem
+                :key="index"
+                :schema="element"
+                :form-config-select="formConfigSelect"
+              />
+            </template>
+          </Draggable>
+        </div>
+      </Form>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="less">
+.form-edit {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  border-bottom-left-radius: 26px;
+  border-bottom-right-radius: 26px;
+
+  &-container {
+    width: 100%;
+    height: 100%;
+    background-color: #fff;
+    box-sizing: border-box;
+  }
+
+  &-list {
+    height: 100%;
+    min-height: calc(100vh - 228px);
+
+    & .ghost {
+      background: #f56c6c;
+      border: 2px solid #f56c6c;
+      outline-width: 0;
+      height: 3px;
+      box-sizing: border-box;
+      font-size: 0;
+      content: '';
+      overflow: hidden;
+      padding: 0;
+
+      &::after {
+        background: #f56c6c;
+      }
+    }
+  }
+
+  &-empty {
+    position: absolute;
+    text-align: center;
+    width: 300px;
+    font-size: 20px;
+    top: 200px;
+    left: 50%;
+    margin-left: -150px;
+    color: #ccc;
+  }
+}
+</style>

+ 57 - 0
apps/web-baicai/src/views/form/design/config/formItemProps.ts

@@ -0,0 +1,57 @@
+import type { IVFormComponent } from '../types';
+
+export const basicControls: IVFormComponent[] = [
+  {
+    label: '单行文本',
+    fieldName: '',
+    component: 'Input',
+    icon: 'proicons:terminal',
+    componentProps: {
+      placeholder: '请输入单行文本',
+      prefix: '',
+      suffix: '',
+      maxlength: 0,
+      showCount: false,
+    },
+    hideLabel: false,
+    rules: '',
+  },
+  {
+    label: '多选文本',
+    fieldName: '',
+    component: 'Textarea',
+    icon: 'proicons:screen-size',
+    componentProps: {
+      placeholder: '请输入多选文本',
+      showCount: false,
+      maxlength: 0,
+    },
+    hideLabel: false,
+    rules: '',
+  },
+  {
+    label: '密码框',
+    fieldName: '',
+    component: 'InputPassword',
+    icon: 'proicons:lock',
+    componentProps: {
+      placeholder: '请输入密码框',
+    },
+    hideLabel: false,
+    rules: '',
+  },
+  {
+    label: '数字框',
+    fieldName: '',
+    component: 'InputNumber',
+    icon: 'proicons:server',
+    componentProps: {
+      placeholder: '请输入数字框',
+      min: 0,
+      max: 100,
+      step: 1,
+    },
+    hideLabel: false,
+    rules: '',
+  },
+];

+ 51 - 0
apps/web-baicai/src/views/form/design/form-modal.vue

@@ -0,0 +1,51 @@
+<script lang="ts" setup>
+import { onMounted, ref, unref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import formDesign from './index.vue';
+
+const emit = defineEmits(['success']);
+const modelRef = ref<Record<string, any>>({});
+const isUpdate = ref(true);
+
+const [Modal, { close, setState, getData }] = useVbenModal({
+  fullscreenButton: false,
+  fullscreen: true,
+  draggable: false,
+  onCancel() {
+    close();
+  },
+  onConfirm: async () => {
+    try {
+      close();
+      emit('success');
+    } catch {
+      message.error('操作失败');
+    } finally {
+      setState({ confirmLoading: false });
+    }
+  },
+  onOpenChange: async (isOpen: boolean) => {
+    if (isOpen) {
+      setState({ loading: true });
+      const data = getData<Record<string, any>>();
+      isUpdate.value = !!data.isUpdate;
+      modelRef.value = { ...data.baseData };
+      setState({ title: unref(isUpdate) ? '表单设计' : '表单设计' });
+
+      setState({ loading: false });
+    }
+  },
+  title: '表单设计',
+});
+
+onMounted(async () => {});
+</script>
+<template>
+  <Modal>
+    <formDesign />
+  </Modal>
+</template>

+ 14 - 0
apps/web-baicai/src/views/form/design/hooks/useFormDesignState.ts

@@ -0,0 +1,14 @@
+import type { Ref } from 'vue';
+
+import type { IFormConfig, IFormDesignMethods } from '../types';
+
+import { inject } from 'vue';
+
+/**
+ * 获取formDesign状态
+ */
+export function useFormDesignState() {
+  const formConfig = inject('formConfig') as Ref<IFormConfig>;
+  const formDesignMethods = inject('formDesignMethods') as IFormDesignMethods;
+  return { formConfig, formDesignMethods };
+}

+ 223 - 0
apps/web-baicai/src/views/form/design/index.vue

@@ -0,0 +1,223 @@
+<script lang="ts" setup>
+import type { Ref } from 'vue';
+
+import type { Recordable } from '@vben/types';
+
+import type { IFormConfig, IFormDesignMethods, IVFormComponent } from './types';
+
+import { onMounted, provide, reactive, ref } from 'vue';
+
+import { cloneDeep } from '@vben/utils';
+
+import { Card, Collapse, Tabs } from 'ant-design-vue';
+
+import ControlConfig from './components/control-config.vue';
+import ControlMark from './components/control-mark.vue';
+import FormConfig from './components/form-config.vue';
+import FormEdit from './components/form-edit.vue';
+import { basicControls } from './config/formItemProps';
+import { generateKey } from './utils';
+
+defineOptions({
+  name: 'FormDesign',
+});
+
+const formConfig = ref<IFormConfig>({
+  config: {
+    wrapperClass: 'grid-cols-1',
+    showDefaultActions: false,
+    commonConfig: {
+      labelWidth: 100,
+    },
+  },
+  schemas: [],
+  currentItem: { component: '' } as IVFormComponent,
+});
+
+const state = reactive<{
+  collapseKey: string;
+  controlList: Recordable<any>[];
+}>({
+  collapseKey: '1',
+  controlList: [
+    {
+      title: '输入型组件',
+      fields: ['Input', 'Textarea', 'InputPassword', 'InputNumber'],
+      list: basicControls,
+    },
+  ],
+});
+
+/**
+ * 选中表单项
+ * @param schema 当前选中的表单项
+ */
+const handleSetSelectItem = (schema: IVFormComponent) => {
+  formConfig.value.currentItem = schema as any;
+  // handleChangePropsTabs(
+  //   schema.key ? (formConfig.value.activeKey! === 1 ? 2 : formConfig.value.activeKey!) : 1,
+  // );
+};
+
+const setGlobalConfigState = (formItem: IVFormComponent) => {
+  // formItem.colProps = formItem.colProps || {};
+  // formItem.colProps.span = globalConfigState.span;
+  formItem.aaa = '';
+  // console.log('setGlobalConfigState', formItem);
+};
+
+// 把表单配置项注入到子组件中,子组件可通过inject获取,获取到的数据为响应式
+provide<Ref<IFormConfig>>('formConfig', formConfig as any);
+
+/**
+ * 添加属性
+ * @param schemas
+ * @param index
+ */
+const handleAddAttrs = (_formItems: IVFormComponent[], _index: number) => {};
+
+const handleListPushDrag = (item: IVFormComponent) => {
+  const formItem = cloneDeep(item);
+  setGlobalConfigState(formItem);
+  generateKey(formItem);
+
+  return formItem;
+};
+
+/**
+ * 添加到表单中
+ * @param newIndex {object} 事件对象
+ * @param schemas {IVFormComponent[]} 表单项列表
+ * @param isCopy {boolean} 是否复制
+ */
+const handleBeforeColAdd = (
+  { newIndex }: any,
+  schemas: IVFormComponent[],
+  isCopy = false,
+) => {
+  const item = schemas[newIndex] as IVFormComponent;
+  isCopy && generateKey(item);
+  handleSetSelectItem(item);
+};
+/**
+ * 复制表单项,如果表单项为栅格布局,则遍历所有自表单项重新生成key
+ * @param {IVFormComponent} formItem
+ * @return {IVFormComponent}
+ */
+const copyFormItem = (formItem: IVFormComponent) => {
+  const newFormItem = cloneDeep(formItem);
+  // if (newFormItem.component === 'Grid') {
+  //   formItemsForEach([formItem], (item) => {
+  //     generateKey(item);
+  //   });
+  // }
+  return newFormItem;
+};
+/**
+ * 复制或者添加表单,isCopy为true时则复制表单
+ * @param item {IVFormComponent} 当前点击的组件
+ * @param isCopy {boolean} 是否复制
+ */
+const handleCopy = (
+  item: IVFormComponent = formConfig.value.currentItem as IVFormComponent,
+  isCopy = true,
+) => {
+  const key = formConfig.value.currentItem?.key;
+  /**
+   * 遍历当表单项配置,如果是复制,则复制一份表单项,如果不是复制,则直接添加到表单项中
+   * @param schemas
+   */
+  const traverse = (schemas: IVFormComponent[]) => {
+    // 使用some遍历,找到目标后停止遍历
+    schemas.some((formItem: IVFormComponent, index: number) => {
+      if (formItem.key === key) {
+        // 判断是不是复制
+        isCopy
+          ? schemas.splice(index, 0, copyFormItem(formItem))
+          : schemas.splice(index + 1, 0, item);
+        const event = {
+          newIndex: index + 1,
+        };
+        // 添加到表单项中
+        handleBeforeColAdd(event, schemas, isCopy);
+        return true;
+      }
+      return false;
+    });
+  };
+  if (formConfig.value.schemas) {
+    traverse(formConfig.value.schemas as any);
+  }
+};
+
+/**
+ * 单击控件时添加到面板中
+ * @param item {IVFormComponent} 当前点击的组件
+ */
+const handleListPush = (item: IVFormComponent) => {
+  // console.log('handleListPush', item);
+  const formItem = cloneDeep(item);
+  setGlobalConfigState(formItem);
+  generateKey(formItem);
+  if (!formConfig.value.currentItem?.key) {
+    handleSetSelectItem(formItem);
+    formConfig.value.schemas && formConfig.value.schemas.push(formItem as any);
+
+    return;
+  }
+  handleCopy(formItem, false);
+};
+
+// 把祖先组件的方法项注入到子组件中,子组件可通过inject获取
+provide<IFormDesignMethods>('formDesignMethods', {
+  handleAddAttrs,
+  handleSetSelectItem,
+});
+
+onMounted(async () => {});
+</script>
+<template>
+  <div class="flex h-full w-full">
+    <div class="w-[300px]">
+      <Collapse
+        :bordered="false"
+        ghost
+        expand-icon-position="end"
+        v-model:active-key="state.collapseKey"
+      >
+        <Collapse.Panel
+          v-for="(item, index) in state.controlList"
+          :key="index + 1"
+          :header="item.title"
+        >
+          <ControlMark
+            :fields="item.fields"
+            :list="item.list"
+            @add-attrs="handleAddAttrs"
+            :handle-list-push="handleListPushDrag"
+            @handle-list-push="handleListPush"
+          />
+        </Collapse.Panel>
+      </Collapse>
+    </div>
+    <div class="h-full flex-1 pl-2 pr-2">
+      <Card title="设计区域" class="h-full w-full">
+        <FormEdit
+          :form-config="formConfig"
+          :form-config-select="formConfig.currentItem"
+          @change="handleSetSelectItem"
+        />
+      </Card>
+    </div>
+    <div class="w-[300px]">
+      <Tabs>
+        <Tabs.TabPane key="1" tab="组件属性">
+          <ControlConfig />
+        </Tabs.TabPane>
+        <Tabs.TabPane key="2" tab="表单属性">
+          <FormConfig v-model:config="formConfig.config" />
+        </Tabs.TabPane>
+      </Tabs>
+    </div>
+  </div>
+</template>

+ 43 - 0
apps/web-baicai/src/views/form/design/types/index.ts

@@ -0,0 +1,43 @@
+export interface IAnyObject<T = any> {
+  [key: string]: T;
+}
+
+/**
+ * 组件属性
+ */
+export interface IVFormComponent {
+  [key: string]: any;
+  // 唯一标识
+  key?: string;
+  label: string; // 字段标签
+  fieldName?: string; // 字段名
+  component: string; // 组件类型
+  icon: string; // 图标
+  // 传给给组件的属性,默认会吧所有的props都传递给控件
+  componentProps?: IAnyObject;
+  hideLabel: boolean; // 隐藏label
+  rules?: string; // 组件校验规则
+}
+
+/**
+ * 设计配置
+ */
+export interface IFormConfig {
+  schemas: IVFormComponent[];
+  config: {
+    commonConfig?: IAnyObject;
+    showDefaultActions: boolean;
+    wrapperClass: string;
+  };
+  currentItem?: IVFormComponent;
+}
+
+/**
+ * 设计方法
+ */
+export interface IFormDesignMethods {
+  // 添加控件属性
+  handleAddAttrs(schemas: IVFormComponent[], index: number): void;
+  // 设置当前选中的控件
+  handleSetSelectItem(item: IVFormComponent): void;
+}

+ 51 - 0
apps/web-baicai/src/views/form/design/utils/index.ts

@@ -0,0 +1,51 @@
+import type { IVFormComponent } from '../types';
+
+import { isNumber } from '@vben/utils';
+
+import { buildUUID, toLine } from '#/utils';
+
+/**
+ * 生成key
+ * @param [formItem] 需要生成 key 的控件,可选,如果不传,默认返回一个唯一 key
+ * @returns {string|boolean} 返回一个唯一 id 或者 false
+ */
+export function generateKey(formItem?: IVFormComponent): boolean | string {
+  if (formItem && formItem.component) {
+    const key = `${toLine(formItem.component)}_${buildUUID()}`;
+    formItem.key = key;
+    formItem.fieldName = key;
+
+    return true;
+  }
+  return `key_${buildUUID()}`;
+}
+
+/**
+ * 移除数组中指定元素,value可以是一个数字下标,也可以是一个函数,删除函数第一个返回true的元素
+ * @param array {Array<T>} 需要移除元素的数组
+ * @param value {number | ((item: T, index: number, array: Array<T>) => boolean}
+ * @returns {T} 返回删除的数组项
+ */
+export function remove<T>(
+  array: Array<T>,
+  value: ((item: T, index: number, array: Array<T>) => boolean) | number,
+): T | undefined {
+  let removeVal: Array<T | undefined> = [];
+  if (!Array.isArray(array)) return undefined;
+  if (isNumber(value)) {
+    removeVal = array.splice(value, 1);
+  } else {
+    const predicate = value as (
+      item: T,
+      index: number,
+      array: Array<T>,
+    ) => boolean;
+    const index = array.findIndex((item, idx, arr) =>
+      predicate(item, idx, arr),
+    );
+    if (index !== -1) {
+      removeVal = array.splice(index, 1);
+    }
+  }
+  return removeVal.shift();
+}

+ 3 - 0
apps/web-baicai/src/views/form/preview/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div>preview</div>
+</template>

+ 20 - 0
apps/web-baicai/src/views/system/design/table/index.vue

@@ -7,6 +7,7 @@ import { Button, message, Modal } from 'ant-design-vue';
 import { useVbenVxeGrid } from '#/adapter/vxe-table';
 import { TableApi } from '#/api';
 import { TableAction } from '#/components/table-action';
+import FormDesign from '#/views/form/design/form-modal.vue';
 import FormMenuEdit from '#/views/system/menu/components/editMenu.vue';
 
 import FormEdit from './components/edit.vue';
@@ -32,6 +33,10 @@ const [FormPreviewModal, formPreviewApi] = useVbenModal({
   connectedComponent: FormPreview,
 });
 
+const [FormDesignModal, formDesignApi] = useVbenModal({
+  connectedComponent: FormDesign,
+});
+
 const handleDelete = (id: number) => {
   Modal.confirm({
     iconType: 'info',
@@ -78,6 +83,16 @@ const handleCreateMenu = (record: any) => {
   });
   formMenuEditApi.open();
 };
+
+const handleDesign = (id: number) => {
+  formDesignApi.setData({
+    isUpdate: false,
+    baseData: {
+      id,
+    },
+  });
+  formDesignApi.open();
+};
 </script>
 
 <template>
@@ -85,6 +100,7 @@ const handleCreateMenu = (record: any) => {
     <FormEditModal :close-on-click-modal="false" @success="handelSuccess" />
     <FormMenuEditModal :close-on-click-modal="false" @success="handelSuccess" />
     <FormPreviewModal :close-on-click-modal="false" />
+    <FormDesignModal :close-on-click-modal="false" />
     <Grid>
       <template #toolbar-tools>
         <Button
@@ -122,6 +138,10 @@ const handleCreateMenu = (record: any) => {
                   : !hasAccessByCodes(['page:edit']),
               onClick: handleCreateMenu.bind(null, row),
             },
+            {
+              label: '表单设计',
+              onClick: handleDesign.bind(null, row.id),
+            },
             {
               label: '删除',
               type: 'link',

+ 27 - 0
pnpm-lock.yaml

@@ -746,6 +746,9 @@ importers:
       dayjs:
         specifier: 'catalog:'
         version: 1.11.13
+      monaco-editor:
+        specifier: ^0.52.2
+        version: 0.52.2
       pinia:
         specifier: ^2.3.0
         version: 2.3.0(typescript@5.7.2)(vue@3.5.13(typescript@5.7.2))
@@ -758,6 +761,9 @@ importers:
       vue-router:
         specifier: 'catalog:'
         version: 4.5.0(vue@3.5.13(typescript@5.7.2))
+      vuedraggable:
+        specifier: ^4.1.0
+        version: 4.1.0(vue@3.5.13(typescript@5.7.2))
 
   apps/web-ele:
     dependencies:
@@ -7563,6 +7569,9 @@ packages:
   mlly@1.7.3:
     resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==}
 
+  monaco-editor@0.52.2:
+    resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
+
   mri@1.2.0:
     resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
     engines: {node: '>=4'}
@@ -8813,6 +8822,7 @@ packages:
   rollup-plugin-visualizer@5.13.1:
     resolution: {integrity: sha512-vMg8i6BprL8aFm9DKvL2c8AwS8324EgymYQo9o6E26wgVvwMhsJxS37aNL6ZsU7X9iAcMYwdME7gItLfG5fwJg==}
     engines: {node: '>=18'}
+    deprecated: Contains unintended breaking changes
     hasBin: true
     peerDependencies:
       rolldown: 1.x
@@ -9035,6 +9045,9 @@ packages:
     resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==}
     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==}
 
@@ -10032,6 +10045,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:
@@ -16722,6 +16740,8 @@ snapshots:
       pkg-types: 1.3.0
       ufo: 1.5.4
 
+  monaco-editor@0.52.2: {}
+
   mri@1.2.0: {}
 
   mrmime@2.0.0: {}
@@ -18319,6 +18339,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: {}
@@ -19493,6 +19515,11 @@ snapshots:
     optionalDependencies:
       typescript: 5.7.2
 
+  vuedraggable@4.1.0(vue@3.5.13(typescript@5.7.2)):
+    dependencies:
+      sortablejs: 1.14.0
+      vue: 3.5.13(typescript@5.7.2)
+
   vueuc@0.4.64(vue@3.5.13(typescript@5.7.2)):
     dependencies:
       '@css-render/vue3-ssr': 0.15.14(vue@3.5.13(typescript@5.7.2))