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

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 });
// side是否处于hover状态展开菜单中

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;