This commit is contained in:
dap 2025-03-10 19:17:57 +08:00
commit 7f1548b343
14 changed files with 253 additions and 73 deletions

View File

@ -3,11 +3,12 @@
* vben-formvben-modalvben-drawer 使,
*/
import type { Component, SetupContext } from 'vue';
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { h } from 'vue';
import { defineComponent, getCurrentInstance, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
@ -44,10 +45,30 @@ const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
return defineComponent({
inheritAttrs: false,
name: component.name,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
return () =>
h(component, { ...props, ...attrs, placeholder, ref: innerRef }, slots);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明

View File

@ -279,22 +279,24 @@ const [Form, formApi] = useVbenForm({
useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。
| 方法名 | 描述 | 类型 |
| --- | --- | --- |
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` |
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` |
| resetForm | 重置表单 | `()=>Promise<void>` |
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
| validate | 表单校验 | `()=>Promise<void>` |
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
| setState | 设置组件状态props | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` |
| getState | 获取组件状态props | `()=>Promise<VbenFormProps>` |
| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - |
| 方法名 | 描述 | 类型 | 版本号 |
| --- | --- | --- | --- |
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
| resetForm | 重置表单 | `()=>Promise<void>` | - |
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` | - |
| validate | 表单校验 | `()=>Promise<void>` | - |
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` | - |
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` | - |
| resetValidate | 重置表单校验 | `()=>Promise<void>` | - |
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - |
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` | - |
| setState | 设置组件状态props | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` | - |
| getState | 获取组件状态props | `()=>Promise<VbenFormProps>` | - |
| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - |
| getFieldComponentRef | 获取指定字段的组件实例 | `<T=unknown>(fieldName: string)=>T` | >5.5.3 |
| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 |
## Props

View File

@ -165,7 +165,7 @@ vxeUI.renderer.add('CellLink', {
**表单搜索** 部分采用了`Vben Form 表单`,参考 [Vben Form 表单文档](/components/common-ui/vben-form)。
当启用了表单搜索时可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。
当启用了表单搜索时可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。表格的所有以`form-`开头的命名插槽都会被传递给搜索表单。
<DemoPreview dir="demos/vben-vxe-table/form" />
@ -250,3 +250,9 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表
| toolbar-actions | 工具栏左侧部分(表格标题附近) |
| toolbar-tools | 工具栏右侧部分vxeTable原生工具按钮的左侧 |
| table-title | 表格标题插槽 |
::: info 搜索表单的插槽
对于使用了搜索表单的表格来说,所有以`form-`开头的命名插槽都会传递给表单。
:::

View File

@ -95,7 +95,7 @@
"node": ">=20.10.0",
"pnpm": ">=9.12.0"
},
"packageManager": "pnpm@9.15.6",
"packageManager": "pnpm@9.15.7",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {

View File

@ -5,6 +5,8 @@ import type {
ValidationOptions,
} from 'vee-validate';
import type { ComponentPublicInstance } from 'vue';
import type { Recordable } from '@vben-core/typings';
import type { FormActions, FormSchema, VbenFormProps } from './types';
@ -56,6 +58,11 @@ export class FormApi {
public store: Store<VbenFormProps>;
/**
*
*/
private componentRefMap: Map<string, unknown> = new Map();
// 最后一次点击提交时的表单值
private latestSubmissionValues: null | Recordable<any> = null;
@ -85,6 +92,46 @@ export class FormApi {
bindMethods(this);
}
/**
*
* @param fieldName
* @returns
*/
getFieldComponentRef<T = ComponentPublicInstance>(
fieldName: string,
): T | undefined {
return this.componentRefMap.has(fieldName)
? (this.componentRefMap.get(fieldName) as T)
: undefined;
}
/**
* undefined
*/
getFocusedField() {
for (const fieldName of this.componentRefMap.keys()) {
const ref = this.getFieldComponentRef(fieldName);
if (ref) {
let el: HTMLElement | null = null;
if (ref instanceof HTMLElement) {
el = ref;
} else if (ref.$el instanceof HTMLElement) {
el = ref.$el;
}
if (!el) {
continue;
}
if (
el === document.activeElement ||
el.contains(document.activeElement)
) {
return fieldName;
}
}
}
return undefined;
}
getLatestSubmissionValues() {
return this.latestSubmissionValues || {};
}
@ -143,13 +190,14 @@ export class FormApi {
return proxy;
}
mount(formActions: FormActions) {
mount(formActions: FormActions, componentRefMap: Map<string, unknown>) {
if (!this.isMounted) {
Object.assign(this.form, formActions);
this.stateHandler.setConditionTrue();
this.setLatestSubmissionValues({
...toRaw(this.handleRangeTimeValue(this.form.values)),
});
this.componentRefMap = componentRefMap;
this.isMounted = true;
}
}

View File

@ -3,7 +3,7 @@ import type { ZodType } from 'zod';
import type { FormSchema, MaybeComponentProps } from '../types';
import { computed, nextTick, useTemplateRef, watch } from 'vue';
import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
import {
FormControl,
@ -18,6 +18,7 @@ import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
import { toTypedSchema } from '@vee-validate/zod';
import { useFieldError, useFormValues } from 'vee-validate';
import { injectComponentRefMap } from '../use-form-context';
import { injectRenderFormProps, useFormContext } from './context';
import useDependencies from './dependencies';
import FormLabel from './form-label.vue';
@ -267,6 +268,15 @@ function autofocus() {
fieldComponentRef.value?.focus?.();
}
}
const componentRefMap = injectComponentRefMap();
watch(fieldComponentRef, (componentRef) => {
componentRefMap?.set(fieldName, componentRef);
});
onUnmounted(() => {
if (componentRefMap?.has(fieldName)) {
componentRefMap.delete(fieldName);
}
});
</script>
<template>

View File

@ -20,6 +20,9 @@ export const [injectFormProps, provideFormProps] =
'VbenFormProps',
);
export const [injectComponentRefMap, provideComponentRefMap] =
createContext<Map<string, unknown>>('ComponentRefMap');
export function useFormInitial(
props: ComputedRef<VbenFormProps> | VbenFormProps,
) {

View File

@ -17,7 +17,11 @@ import {
DEFAULT_FORM_COMMON_CONFIG,
} from './config';
import { Form } from './form-render';
import { provideFormProps, useFormInitial } from './use-form-context';
import {
provideComponentRefMap,
provideFormProps,
useFormInitial,
} from './use-form-context';
// extends
interface Props extends VbenFormProps {
formApi: ExtendedFormApi;
@ -29,11 +33,14 @@ const state = props.formApi?.useStore?.();
const forward = useForwardPriorityValues(props, state);
const componentRefMap = new Map<string, unknown>();
const { delegatedSlots, form } = useFormInitial(forward);
provideFormProps([forward, form]);
provideComponentRefMap(componentRefMap);
props.formApi?.mount?.(form);
props.formApi?.mount?.(form, componentRefMap);
const handleUpdateCollapsed = (value: boolean) => {
props.formApi?.setState({ collapsed: !!value });

View File

@ -3,6 +3,8 @@ import type { CSSProperties } from 'vue';
import type { VbenLayoutProps } from './vben-layout';
import { computed, ref, watch } from 'vue';
import {
SCROLL_FIXED_CLASS,
useLayoutFooterStyle,
@ -11,8 +13,8 @@ import {
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
import { computed, ref, watch } from 'vue';
import {
LayoutContent,
@ -60,10 +62,16 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{ sideMouseLeave: []; toggleSidebar: [] }>();
const sidebarCollapse = defineModel<boolean>('sidebarCollapse');
const sidebarCollapse = defineModel<boolean>('sidebarCollapse', {
default: false,
});
const sidebarExtraVisible = defineModel<boolean>('sidebarExtraVisible');
const sidebarExtraCollapse = defineModel<boolean>('sidebarExtraCollapse');
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover');
const sidebarExtraCollapse = defineModel<boolean>('sidebarExtraCollapse', {
default: false,
});
const sidebarExpandOnHover = defineModel<boolean>('sidebarExpandOnHover', {
default: false,
});
const sidebarEnable = defineModel<boolean>('sidebarEnable', { default: true });
// sidehover

View File

@ -1,14 +1,20 @@
import type { Component, DefineComponent } from 'vue';
import type {
AccessModeType,
GenerateMenuAndRoutesOptions,
RouteRecordRaw,
} from '@vben/types';
import { defineComponent, h } from 'vue';
import {
cloneDeep,
generateMenus,
generateRoutesByBackend,
generateRoutesByFrontend,
isFunction,
isString,
mapTree,
setObjToUrlParams,
} from '@vben/utils';
@ -89,8 +95,31 @@ async function generateRoutes(
/**
*
* 1. redirect的路由添加redirect
* 2. keep-alive的话
*/
resultRoutes = mapTree(resultRoutes, (route) => {
// 重新包装component使用与路由名称相同的name以支持keep-alive的条件缓存。
if (
route.meta?.keepAlive &&
isFunction(route.component) &&
route.name &&
isString(route.name)
) {
const originalComponent = route.component as () => Promise<{
default: Component | DefineComponent;
}>;
route.component = async () => {
const component = await originalComponent();
if (!component.default) return component;
return defineComponent({
name: route.name as string,
setup(props, { attrs, slots }) {
return () => h(component.default, { ...props, ...attrs }, slots);
},
});
};
}
// 如果有redirect或者没有子路由则直接返回
if (route.redirect || !route.children || route.children.length === 0) {
return route;

View File

@ -3,11 +3,12 @@
* vben-formvben-modalvben-drawer 使,
*/
import type { Component, SetupContext } from 'vue';
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { h } from 'vue';
import { defineComponent, getCurrentInstance, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
@ -41,10 +42,30 @@ const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
return defineComponent({
inheritAttrs: false,
name: component.name,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
return () =>
h(component, { ...props, ...attrs, placeholder, ref: innerRef }, slots);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明

View File

@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import type { BasicOption, Recordable } from '@vben/types';
import { computed, markRaw } from 'vue';
import { computed, markRaw, useTemplateRef } from 'vue';
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
@ -104,12 +104,28 @@ const formSchema = computed((): VbenFormSchema[] => {
},
];
});
const loginRef =
useTemplateRef<InstanceType<typeof AuthenticationLogin>>('loginRef');
async function onSubmit(params: Recordable<any>) {
authStore.authLogin(params).catch(() => {
//
// 使APIresume
loginRef.value
?.getFormApi()
?.getFieldComponentRef<InstanceType<typeof SliderCaptcha>>('captcha')
?.resume();
});
}
</script>
<template>
<AuthenticationLogin
ref="loginRef"
:form-schema="formSchema"
:loading="authStore.loginLoading"
@submit="authStore.authLogin"
@submit="onSubmit"
/>
</template>

View File

@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { RefSelectProps } from 'ant-design-vue/es/select';
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
@ -82,6 +84,7 @@ function handleClick(
action:
| 'batchAddSchema'
| 'batchDeleteSchema'
| 'componentRef'
| 'disabled'
| 'hiddenAction'
| 'hiddenResetButton'
@ -129,6 +132,11 @@ function handleClick(
});
break;
}
case 'componentRef': {
// focus
formApi.getFieldComponentRef<RefSelectProps>('fieldOptions')?.focus();
break;
}
case 'disabled': {
formApi.setState({ commonConfig: { disabled: true } });
break;
@ -182,6 +190,7 @@ function handleClick(
formApi.setState({ submitButtonOptions: { show: true } });
break;
}
case 'updateActionAlign': {
formApi.setState({
// class
@ -189,7 +198,6 @@ function handleClick(
});
break;
}
case 'updateResetButton': {
formApi.setState({
resetButtonOptions: { disabled: true },
@ -257,6 +265,7 @@ function handleClick(
<Button @click="handleClick('batchDeleteSchema')">
批量删除表单项
</Button>
<Button @click="handleClick('componentRef')">下拉组件获取焦点</Button>
</Space>
<Card title="操作示例">
<BaseForm />

View File

@ -18,25 +18,25 @@ catalog:
'@changesets/cli': ^2.28.1
'@changesets/git': ^3.0.2
'@clack/prompts': ^0.9.1
'@commitlint/cli': ^19.7.1
'@commitlint/config-conventional': ^19.7.1
'@commitlint/cli': ^19.8.0
'@commitlint/config-conventional': ^19.8.0
'@ctrl/tinycolor': ^4.1.0
'@eslint/js': ^9.21.0
'@faker-js/faker': ^9.5.1
'@iconify/json': ^2.2.311
'@eslint/js': ^9.22.0
'@faker-js/faker': ^9.6.0
'@iconify/json': ^2.2.314
'@iconify/tailwind': ^1.2.0
'@iconify/vue': ^4.3.0
'@intlify/core-base': ^11.1.1
'@intlify/core-base': ^11.1.2
'@intlify/unplugin-vue-i18n': ^6.0.3
'@jspm/generator': ^2.5.1
'@manypkg/get-packages': ^2.2.2
'@nolebase/vitepress-plugin-git-changelog': ^2.15.0
'@playwright/test': ^1.50.1
'@pnpm/workspace.read-manifest': ^1000.1.0
'@playwright/test': ^1.51.0
'@pnpm/workspace.read-manifest': ^1000.1.1
'@stylistic/stylelint-plugin': ^3.1.2
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e
'@tailwindcss/typography': ^0.5.16
'@tanstack/vue-query': ^5.66.9
'@tanstack/vue-query': ^5.67.2
'@tanstack/vue-store': ^0.7.0
'@types/archiver': ^6.0.3
'@types/eslint': ^9.6.1
@ -45,13 +45,13 @@ catalog:
'@types/lodash.clonedeep': ^4.5.9
'@types/lodash.get': ^4.4.9
'@types/lodash.isequal': ^4.5.8
'@types/node': ^22.13.5
'@types/node': ^22.13.10
'@types/nprogress': ^0.2.3
'@types/postcss-import': ^14.0.3
'@types/qrcode': ^1.5.5
'@types/sortablejs': ^1.15.8
'@typescript-eslint/eslint-plugin': ^8.25.0
'@typescript-eslint/parser': ^8.25.0
'@typescript-eslint/eslint-plugin': ^8.26.0
'@typescript-eslint/parser': ^8.26.0
'@vee-validate/zod': ^4.15.0
'@vite-pwa/vitepress': ^0.5.3
'@vitejs/plugin-vue': ^5.2.1
@ -59,13 +59,13 @@ catalog:
'@vue/reactivity': ^3.5.13
'@vue/shared': ^3.5.13
'@vue/test-utils': ^2.4.6
'@vueuse/core': ^12.7.0
'@vueuse/core': ^12.8.2
'@vueuse/motion': ^2.2.6
'@vueuse/integrations': ^12.7.0
'@vueuse/integrations': ^12.8.2
ant-design-vue: ^4.2.6
archiver: ^7.0.1
autoprefixer: ^10.4.20
axios: ^1.8.1
axios: ^1.8.2
axios-mock-adapter: ^2.1.0
cac: ^6.7.14
chalk: ^5.4.1
@ -78,30 +78,30 @@ catalog:
cross-env: ^7.0.3
cspell: 8.17.2
cssnano: ^7.0.6
cz-git: ^1.11.0
czg: ^1.11.0
cz-git: ^1.11.1
czg: ^1.11.1
dayjs: ^1.11.13
defu: ^6.1.4
depcheck: ^1.4.7
dotenv: ^16.4.7
echarts: ^5.6.0
element-plus: ^2.9.5
eslint: ^9.21.0
element-plus: ^2.9.6
eslint: ^9.22.0
eslint-config-turbo: ^2.4.4
eslint-plugin-command: ^0.2.7
eslint-plugin-eslint-comments: ^3.2.0
eslint-plugin-import-x: ^4.6.1
eslint-plugin-jsdoc: ^50.6.3
eslint-plugin-jsonc: ^2.19.1
eslint-plugin-n: ^17.15.1
eslint-plugin-n: ^17.16.2
eslint-plugin-no-only-tests: ^3.3.0
eslint-plugin-perfectionist: ^4.9.0
eslint-plugin-perfectionist: ^4.10.0
eslint-plugin-prettier: ^5.2.3
eslint-plugin-regexp: ^2.7.0
eslint-plugin-unicorn: ^56.0.1
eslint-plugin-unused-imports: ^4.1.4
eslint-plugin-vitest: ^0.5.4
eslint-plugin-vue: ^9.32.0
eslint-plugin-vue: ^9.33.0
execa: ^9.5.2
find-up: ^7.0.0
get-port: ^7.1.0
@ -120,31 +120,31 @@ catalog:
lucide-vue-next: ^0.469.0
medium-zoom: ^1.1.0
naive-ui: ^2.41.0
nitropack: ^2.10.4
nitropack: ^2.11.6
nprogress: ^0.2.0
ora: ^8.2.0
pinia: ^2.3.1
pinia-plugin-persistedstate: ^4.2.0
pkg-types: ^1.3.1
playwright: ^1.50.1
playwright: ^1.51.0
postcss: ^8.5.3
postcss-antd-fixes: ^0.2.0
postcss-html: ^1.8.0
postcss-import: ^16.1.0
postcss-preset-env: ^10.1.5
postcss-scss: ^4.0.9
prettier: ^3.5.2
prettier: ^3.5.3
prettier-plugin-tailwindcss: ^0.6.11
publint: ^0.2.12
qrcode: ^1.5.4
radix-vue: ^1.9.17
resolve.exports: ^2.0.3
rimraf: ^6.0.1
rollup: ^4.34.8
rollup: ^4.35.0
rollup-plugin-visualizer: ^5.14.0
sass: ^1.85.1
sortablejs: ^1.15.6
stylelint: ^16.14.1
stylelint: ^16.15.0
stylelint-config-recess-order: ^5.1.1
stylelint-config-recommended: ^14.0.1
stylelint-config-recommended-scss: ^14.1.0
@ -163,9 +163,9 @@ catalog:
unbuild: ^3.5.0
unplugin-element-plus: ^0.9.1
vee-validate: ^4.15.0
vite: ^6.2.0
vite: ^6.2.1
vite-plugin-compression: ^0.5.1
vite-plugin-dts: ^4.5.1
vite-plugin-dts: ^4.5.3
vite-plugin-html: ^3.2.2
vite-plugin-lazy-import: ^1.0.7
vite-plugin-pwa: ^0.21.1
@ -175,12 +175,12 @@ catalog:
vitest: ^2.1.9
vue: ^3.5.13
vue-eslint-parser: ^9.4.3
vue-i18n: ^11.1.1
vue-i18n: ^11.1.2
vue-json-viewer: ^3.0.4
vue-router: ^4.5.0
vue-tippy: ^6.6.0
vue-tsc: 2.1.10
vxe-pc-ui: ^4.3.99
vxe-pc-ui: ^4.4.8
vxe-table: 4.10.0
watermark-js-plus: ^1.5.8
zod: ^3.24.2