🔱: [client] sync upgrade with 7 commits [trident-sync]

chore:
Merge branch 'vben'

# Conflicts:
#	package.json
perf: antdv示例改成使用vben框架
chore: vben
chore: vben
chore: vben
This commit is contained in:
GitHub Actions Bot
2025-03-03 19:24:51 +00:00
parent de26ee9383
commit 335d175d57
649 changed files with 36984 additions and 826 deletions
@@ -0,0 +1,126 @@
<script setup lang="ts">
import { computed, toRaw, unref, watch } from "vue";
import { useSimpleLocale } from "../../composables";
import { VbenExpandableArrow } from "../../shadcn-ui";
import { cn, isFunction, triggerWindowResize } from "../../shared/utils";
import { COMPONENT_MAP } from "../config";
import { injectFormProps } from "../use-form-context";
const { $t } = useSimpleLocale();
const [rootProps, form] = injectFormProps();
const collapsed = defineModel({ default: false });
const resetButtonOptions = computed(() => {
return {
content: `${$t.value("reset")}`,
show: true,
...unref(rootProps).resetButtonOptions
};
});
const submitButtonOptions = computed(() => {
return {
content: `${$t.value("submit")}`,
show: true,
...unref(rootProps).submitButtonOptions
};
});
// const isQueryForm = computed(() => {
// return !!unref(rootProps).showCollapseButton;
// });
const queryFormStyle = computed(() => {
if (!unref(rootProps).actionWrapperClass) {
return {
"grid-column": `-2 / -1`,
marginLeft: "auto"
};
}
return {};
});
async function handleSubmit(e: Event) {
e?.preventDefault();
e?.stopPropagation();
const { valid } = await form.validate();
if (!valid) {
return;
}
const values = toRaw(await unref(rootProps).formApi?.getValues());
await unref(rootProps).handleSubmit?.(values);
}
async function handleReset(e: Event) {
e?.preventDefault();
e?.stopPropagation();
const props = unref(rootProps);
const values = toRaw(props.formApi?.getValues());
if (isFunction(props.handleReset)) {
await props.handleReset?.(values);
} else {
form.resetForm();
}
}
watch(
() => collapsed.value,
() => {
const props = unref(rootProps);
if (props.collapseTriggerResize) {
triggerWindowResize();
}
}
);
defineExpose({
handleReset,
handleSubmit
});
</script>
<template>
<div :class="cn('col-span-full w-full text-right', rootProps.compact ? 'pb-2' : 'pb-6', rootProps.actionWrapperClass)" :style="queryFormStyle">
<template v-if="rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<component :is="COMPONENT_MAP.PrimaryButton" v-if="submitButtonOptions.show" class="ml-3" type="button" v-bind="submitButtonOptions" @click="handleSubmit">
{{ submitButtonOptions.content }}
</component>
</template>
<!-- 重置按钮前 -->
<slot name="reset-before"></slot>
<component :is="COMPONENT_MAP.DefaultButton" v-if="resetButtonOptions.show" class="ml-3" type="button" v-bind="resetButtonOptions" @click="handleReset">
{{ resetButtonOptions.content }}
</component>
<template v-if="!rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<component :is="COMPONENT_MAP.PrimaryButton" v-if="submitButtonOptions.show" class="ml-3" type="button" v-bind="submitButtonOptions" @click="handleSubmit">
{{ submitButtonOptions.content }}
</component>
</template>
<!-- 展开按钮前 -->
<slot name="expand-before"></slot>
<VbenExpandableArrow v-if="rootProps.showCollapseButton" v-model:model-value="collapsed" class="ml-2">
<span>{{ collapsed ? $t("expand") : $t("collapse") }}</span>
</VbenExpandableArrow>
<!-- 展开按钮后 -->
<slot name="expand-after"></slot>
</div>
</template>
@@ -0,0 +1,65 @@
import type { Component } from "vue";
import type { BaseFormComponentType, FormCommonConfig, VbenFormAdapterOptions } from "./types";
import { h } from "vue";
import { VbenButton, VbenCheckbox, Input as VbenInput, VbenInputPassword, VbenPinInput, VbenSelect } from "../shadcn-ui";
import { globalShareState } from "../shared/global-state";
import { defineRule } from "vee-validate";
const DEFAULT_MODEL_PROP_NAME = "modelValue";
export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {};
export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
DefaultButton: h(VbenButton, { size: "sm", variant: "outline" }),
PrimaryButton: h(VbenButton, { size: "sm", variant: "default" }),
VbenCheckbox,
VbenInput,
VbenInputPassword,
VbenPinInput,
VbenSelect
};
export const COMPONENT_BIND_EVENT_MAP: Partial<Record<BaseFormComponentType, string>> = {
VbenCheckbox: "checked"
};
export function setupVbenForm<T extends BaseFormComponentType = BaseFormComponentType>(options: VbenFormAdapterOptions<T>) {
const { config, defineRules } = options;
const { disabledOnChangeListener = true, disabledOnInputListener = true, emptyStateValue = undefined } = (config || {}) as FormCommonConfig;
Object.assign(DEFAULT_FORM_COMMON_CONFIG, {
disabledOnChangeListener,
disabledOnInputListener,
emptyStateValue
});
if (defineRules) {
for (const key of Object.keys(defineRules)) {
defineRule(key, defineRules[key as never]);
}
}
const baseModelPropName = config?.baseModelPropName ?? DEFAULT_MODEL_PROP_NAME;
const modelPropNameMap = config?.modelPropNameMap as Record<BaseFormComponentType, string> | undefined;
const components = globalShareState.getComponents();
for (const component of Object.keys(components)) {
const key = component as BaseFormComponentType;
COMPONENT_MAP[key] = components[component as never];
if (baseModelPropName !== DEFAULT_MODEL_PROP_NAME) {
COMPONENT_BIND_EVENT_MAP[key] = baseModelPropName;
}
// 覆盖特殊组件的modelPropName
if (modelPropNameMap && modelPropNameMap[key]) {
COMPONENT_BIND_EVENT_MAP[key] = modelPropNameMap[key];
}
}
}
@@ -0,0 +1,361 @@
import type { FormState, GenericObject, ResetFormOpts, ValidationOptions } from "vee-validate";
import type { Recordable } from "/@/vben/typings";
import type { FormActions, FormSchema, VbenFormProps } from "./types";
import { toRaw } from "vue";
import { Store } from "/@/vben/shared/store";
import { bindMethods, createMerge, formatDate, isDate, isDayjsObject, isFunction, isObject, mergeWithArrayOverride, StateHandler } from "/@/vben/shared/utils";
function getDefaultState(): VbenFormProps {
return {
actionWrapperClass: "",
collapsed: false,
collapsedRows: 1,
collapseTriggerResize: false,
commonConfig: {},
handleReset: undefined,
handleSubmit: undefined,
handleValuesChange: undefined,
layout: "horizontal",
resetButtonOptions: {},
schema: [],
showCollapseButton: false,
showDefaultActions: true,
submitButtonOptions: {},
submitOnChange: false,
submitOnEnter: false,
wrapperClass: "grid-cols-1"
};
}
export class FormApi {
// private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;
public form = {} as FormActions;
isMounted = false;
public state: null | VbenFormProps = null;
stateHandler: StateHandler;
public store: Store<VbenFormProps>;
// 最后一次点击提交时的表单值
private latestSubmissionValues: null | Recordable<any> = null;
private prevState: null | VbenFormProps = null;
constructor(options: VbenFormProps = {}) {
const { ...storeState } = options;
const defaultState = getDefaultState();
this.store = new Store<VbenFormProps>(
{
...defaultState,
...storeState
},
{
onUpdate: () => {
this.prevState = this.state;
this.state = this.store.state;
this.updateState();
}
}
);
this.state = this.store.state;
this.stateHandler = new StateHandler();
bindMethods(this);
}
getLatestSubmissionValues() {
return this.latestSubmissionValues || {};
}
getState() {
return this.state;
}
async getValues<T = Recordable<any>>() {
const form = await this.getForm();
return (form.values ? this.handleRangeTimeValue(form.values) : {}) as T;
}
async isFieldValid(fieldName: string) {
const form = await this.getForm();
return form.isFieldValid(fieldName);
}
merge(formApi: FormApi) {
const chain = [this, formApi];
const proxy = new Proxy(formApi, {
get(target: any, prop: any) {
if (prop === "merge") {
return (nextFormApi: FormApi) => {
chain.push(nextFormApi);
return proxy;
};
}
if (prop === "submitAllForm") {
return async (needMerge: boolean = true) => {
try {
const results = await Promise.all(
chain.map(async (api) => {
const validateResult = await api.validate();
if (!validateResult.valid) {
return;
}
const rawValues = toRaw((await api.getValues()) || {});
return rawValues;
})
);
if (needMerge) {
const mergedResults = Object.assign({}, ...results);
return mergedResults;
}
return results;
} catch (error) {
console.error("Validation error:", error);
}
};
}
return target[prop];
}
});
return proxy;
}
mount(formActions: FormActions) {
if (!this.isMounted) {
Object.assign(this.form, formActions);
this.stateHandler.setConditionTrue();
this.setLatestSubmissionValues({
...toRaw(this.handleRangeTimeValue(this.form.values))
});
this.isMounted = true;
}
}
/**
* 根据字段名移除表单项
* @param fields
*/
async removeSchemaByFields(fields: string[]) {
const fieldSet = new Set(fields);
const schema = this.state?.schema ?? [];
const filterSchema = schema.filter((item) => !fieldSet.has(item.fieldName));
this.setState({
schema: filterSchema
});
}
/**
* 重置表单
*/
async resetForm(state?: Partial<FormState<GenericObject>> | undefined, opts?: Partial<ResetFormOpts>) {
const form = await this.getForm();
return form.resetForm(state, opts);
}
async resetValidate() {
const form = await this.getForm();
const fields = Object.keys(form.errors.value);
fields.forEach((field) => {
form.setFieldError(field, undefined);
});
}
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
const form = await this.getForm();
form.setFieldValue(field, value, shouldValidate);
}
setLatestSubmissionValues(values: null | Recordable<any>) {
this.latestSubmissionValues = { ...toRaw(values) };
}
setState(stateOrFn: ((prev: VbenFormProps) => Partial<VbenFormProps>) | Partial<VbenFormProps>) {
if (isFunction(stateOrFn)) {
this.store.setState((prev) => {
return mergeWithArrayOverride(stateOrFn(prev), prev);
});
} else {
this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev));
}
}
/**
* 设置表单值
* @param fields record
* @param filterFields 过滤不在schema中定义的字段 默认为true
* @param shouldValidate
*/
async setValues(fields: Record<string, any>, filterFields: boolean = true, shouldValidate: boolean = false) {
const form = await this.getForm();
if (!filterFields) {
form.setValues(fields, shouldValidate);
return;
}
/**
* 合并算法有待改进,目前的算法不支持object类型的值。
* antd的日期时间相关组件的值类型为dayjs对象
* element-plus的日期时间相关组件的值类型可能为Date对象
* 以上两种类型需要排除深度合并
*/
const fieldMergeFn = createMerge((obj, key, value) => {
if (key in obj) {
obj[key] = !Array.isArray(obj[key]) && isObject(obj[key]) && !isDayjsObject(obj[key]) && !isDate(obj[key]) ? fieldMergeFn(obj[key], value) : value;
}
return true;
});
const filteredFields = fieldMergeFn(fields, form.values);
form.setValues(filteredFields, shouldValidate);
}
async submitForm(e?: Event) {
e?.preventDefault();
e?.stopPropagation();
const form = await this.getForm();
await form.submitForm();
const rawValues = toRaw(await this.getValues());
await this.state?.handleSubmit?.(rawValues);
return rawValues;
}
unmount() {
this.form?.resetForm?.();
// this.state = null;
this.latestSubmissionValues = null;
this.isMounted = false;
this.stateHandler.reset();
}
updateSchema(schema: Partial<FormSchema>[]) {
const updated: Partial<FormSchema>[] = [...schema];
const hasField = updated.every((item) => Reflect.has(item, "fieldName") && item.fieldName);
if (!hasField) {
console.error("All items in the schema array must have a valid `fieldName` property to be updated");
return;
}
const currentSchema = [...(this.state?.schema ?? [])];
const updatedMap: Record<string, any> = {};
updated.forEach((item) => {
if (item.fieldName) {
updatedMap[item.fieldName] = item;
}
});
currentSchema.forEach((schema, index) => {
const updatedData = updatedMap[schema.fieldName];
if (updatedData) {
currentSchema[index] = mergeWithArrayOverride(updatedData, schema) as FormSchema;
}
});
this.setState({ schema: currentSchema });
}
async validate(opts?: Partial<ValidationOptions>) {
const form = await this.getForm();
const validateResult = await form.validate(opts);
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
console.error("validate error", validateResult?.errors);
}
return validateResult;
}
async validateAndSubmitForm() {
const form = await this.getForm();
const { valid } = await form.validate();
if (!valid) {
return;
}
return await this.submitForm();
}
async validateField(fieldName: string, opts?: Partial<ValidationOptions>) {
const form = await this.getForm();
const validateResult = await form.validateField(fieldName, opts);
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
console.error("validate error", validateResult?.errors);
}
return validateResult;
}
private async getForm() {
if (!this.isMounted) {
// 等待form挂载
await this.stateHandler.waitForCondition();
}
if (!this.form?.meta) {
throw new Error("<VbenForm /> is not mounted");
}
return this.form;
}
private handleRangeTimeValue = (originValues: Record<string, any>) => {
const values = { ...originValues };
const fieldMappingTime = this.state?.fieldMappingTime;
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
return values;
}
fieldMappingTime.forEach(([field, [startTimeKey, endTimeKey], format = "YYYY-MM-DD"]) => {
if (startTimeKey && endTimeKey && values[field] === null) {
Reflect.deleteProperty(values, startTimeKey);
Reflect.deleteProperty(values, endTimeKey);
// delete values[startTimeKey];
// delete values[endTimeKey];
}
if (!values[field]) {
Reflect.deleteProperty(values, field);
// delete values[field];
return;
}
const [startTime, endTime] = values[field];
if (format === null) {
values[startTimeKey] = startTime;
values[endTimeKey] = endTime;
} else if (isFunction(format)) {
values[startTimeKey] = format(startTime, startTimeKey);
values[endTimeKey] = format(endTime, endTimeKey);
} else {
const [startTimeFormat, endTimeFormat] = Array.isArray(format) ? format : [format, format];
values[startTimeKey] = startTime ? formatDate(startTime, startTimeFormat) : undefined;
values[endTimeKey] = endTime ? formatDate(endTime, endTimeFormat) : undefined;
}
// delete values[field];
Reflect.deleteProperty(values, field);
});
return values;
};
private updateState() {
const currentSchema = this.state?.schema ?? [];
const prevSchema = this.prevState?.schema ?? [];
// 进行了删除schema操作
if (currentSchema.length < prevSchema.length) {
const currentFields = new Set(currentSchema.map((item) => item.fieldName));
const deletedSchema = prevSchema.filter((item) => !currentFields.has(item.fieldName));
for (const schema of deletedSchema) {
this.form?.setFieldValue?.(schema.fieldName, undefined);
}
}
}
}
@@ -0,0 +1,21 @@
import type { FormRenderProps } from "../types";
import { computed } from "vue";
import { createContext } from "/@/vben/shadcn-ui";
export const [injectRenderFormProps, provideFormRenderProps] = createContext<FormRenderProps>("FormRenderProps");
export const useFormContext = () => {
const formRenderProps = injectRenderFormProps();
const isVertical = computed(() => formRenderProps.layout === "vertical");
const componentMap = computed(() => formRenderProps.componentMap);
const componentBindEventMap = computed(() => formRenderProps.componentBindEventMap);
return {
componentBindEventMap,
componentMap,
isVertical
};
};
@@ -0,0 +1,110 @@
import type { FormItemDependencies, FormSchemaRuleType, MaybeComponentProps } from "../types";
import { computed, ref, watch } from "vue";
import { isBoolean, isFunction } from "/@/vben/shared/utils";
import { useFormValues } from "vee-validate";
import { injectRenderFormProps } from "./context";
export default function useDependencies(getDependencies: () => FormItemDependencies | undefined) {
const values = useFormValues();
const formRenderProps = injectRenderFormProps();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const formApi = formRenderProps.form!;
if (!values) {
throw new Error("useDependencies should be used within <VbenForm>");
}
const isIf = ref(true);
const isDisabled = ref(false);
const isShow = ref(true);
const isRequired = ref(false);
const dynamicComponentProps = ref<MaybeComponentProps>({});
const dynamicRules = ref<FormSchemaRuleType>();
const triggerFieldValues = computed(() => {
// 该字段可能会被多个字段触发
const triggerFields = getDependencies()?.triggerFields ?? [];
return triggerFields.map((dep) => {
return values.value[dep];
});
});
const resetConditionState = () => {
isDisabled.value = false;
isIf.value = true;
isShow.value = true;
isRequired.value = false;
dynamicRules.value = undefined;
dynamicComponentProps.value = {};
};
watch(
[triggerFieldValues, getDependencies],
async ([_values, dependencies]) => {
if (!dependencies || !dependencies?.triggerFields?.length) {
return;
}
resetConditionState();
const { componentProps, disabled, if: whenIf, required, rules, show, trigger } = dependencies;
// 1. 优先判断if,如果if为false,则不渲染dom,后续判断也不再执行
const formValues = values.value;
if (isFunction(whenIf)) {
isIf.value = !!(await whenIf(formValues, formApi));
// 不渲染
if (!isIf.value) return;
} else if (isBoolean(whenIf)) {
isIf.value = whenIf;
if (!isIf.value) return;
}
// 2. 判断show,如果show为false,则隐藏
if (isFunction(show)) {
isShow.value = !!(await show(formValues, formApi));
if (!isShow.value) return;
} else if (isBoolean(show)) {
isShow.value = show;
if (!isShow.value) return;
}
if (isFunction(componentProps)) {
dynamicComponentProps.value = await componentProps(formValues, formApi);
}
if (isFunction(rules)) {
dynamicRules.value = await rules(formValues, formApi);
}
if (isFunction(disabled)) {
isDisabled.value = !!(await disabled(formValues, formApi));
} else if (isBoolean(disabled)) {
isDisabled.value = disabled;
}
if (isFunction(required)) {
isRequired.value = !!(await required(formValues, formApi));
}
if (isFunction(trigger)) {
await trigger(formValues, formApi);
}
},
{ deep: true, immediate: true }
);
return {
dynamicComponentProps,
dynamicRules,
isDisabled,
isIf,
isRequired,
isShow
};
}
@@ -0,0 +1,90 @@
import type { FormRenderProps } from "../types";
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from "vue";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
/**
* 动态计算行数
*/
export function useExpandable(props: FormRenderProps) {
const wrapperRef = useTemplateRef<HTMLElement>("wrapperRef");
const rowMapping = ref<Record<number, number>>({});
// 是否已经计算过一次
const isCalculated = ref(false);
const breakpoints: any = useBreakpoints(breakpointsTailwind);
const keepFormItemIndex = computed(() => {
const rows = props.collapsedRows ?? 1;
const mapping = rowMapping.value;
let maxItem = 0;
for (let index = 1; index <= rows; index++) {
maxItem += mapping?.[index] ?? 0;
}
// 保持一行
return maxItem - 1 || 1;
});
watch([() => props.showCollapseButton, () => breakpoints.active().value, () => props.schema?.length], async ([val]) => {
if (val) {
await nextTick();
rowMapping.value = {};
isCalculated.value = false;
await calculateRowMapping();
}
});
async function calculateRowMapping() {
if (!props.showCollapseButton) {
return;
}
await nextTick();
if (!wrapperRef.value) {
return;
}
// 小屏幕不计算
// if (breakpoints.smaller('sm').value) {
// // 保持一行
// rowMapping.value = { 1: 2 };
// return;
// }
const formItems = [...wrapperRef.value.children];
const container = wrapperRef.value;
const containerStyles = window.getComputedStyle(container);
const rowHeights = containerStyles.getPropertyValue("grid-template-rows").split(" ");
const containerRect = container?.getBoundingClientRect();
formItems.forEach((el) => {
const itemRect = el.getBoundingClientRect();
// 计算元素在第几行
const itemTop = itemRect.top - containerRect.top;
let rowStart = 0;
let cumulativeHeight = 0;
for (const [i, rowHeight] of rowHeights.entries()) {
cumulativeHeight += Number.parseFloat(rowHeight);
if (itemTop < cumulativeHeight) {
rowStart = i + 1;
break;
}
}
if (rowStart > (props?.collapsedRows ?? 1)) {
return;
}
rowMapping.value[rowStart] = (rowMapping.value[rowStart] ?? 0) + 1;
isCalculated.value = true;
});
}
onMounted(() => {
calculateRowMapping();
});
return { isCalculated, keepFormItemIndex, wrapperRef };
}
@@ -0,0 +1,366 @@
<script setup lang="ts">
import type { ZodType } from 'zod';
import type { FormSchema, MaybeComponentProps } from '../types';
import { computed, nextTick, useTemplateRef, watch } from 'vue';
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
VbenRenderContent,
} from '/@/vben/shadcn-ui';
import { cn, isFunction, isObject, isString } from '/@/vben/shared/utils';
import { toTypedSchema } from '@vee-validate/zod';
import { useFieldError, useFormValues } from 'vee-validate';
import { injectRenderFormProps, useFormContext } from './context';
import useDependencies from './dependencies';
import FormLabel from './form-label.vue';
import { isEventObjectLike } from './helper';
interface Props extends FormSchema {}
const {
colon,
commonComponentProps,
component,
componentProps,
dependencies,
description,
disabled,
disabledOnChangeListener,
disabledOnInputListener,
emptyStateValue,
fieldName,
formFieldProps,
label,
labelClass,
labelWidth,
modelPropName,
renderComponentContent,
rules,
} = defineProps<
Props & {
commonComponentProps: MaybeComponentProps;
}
>();
const { componentBindEventMap, componentMap, isVertical } = useFormContext();
const formRenderProps = injectRenderFormProps();
const values = useFormValues();
const errors = useFieldError(fieldName);
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
const formApi = formRenderProps.form;
const compact = formRenderProps.compact;
const isInValid = computed(() => errors.value?.length > 0);
const FieldComponent = computed(() => {
const finalComponent = isString(component)
? componentMap.value[component]
: component;
if (!finalComponent) {
// 组件未注册
console.warn(`Component ${component} is not registered`);
}
return finalComponent;
});
const {
dynamicComponentProps,
dynamicRules,
isDisabled,
isIf,
isRequired,
isShow,
} = useDependencies(() => dependencies);
const labelStyle = computed(() => {
return labelClass?.includes('w-') || isVertical.value
? {}
: {
width: `${labelWidth}px`,
};
});
const currentRules = computed(() => {
return dynamicRules.value || rules;
});
const visible = computed(() => {
return isIf.value && isShow.value;
});
const shouldRequired = computed(() => {
if (!visible.value) {
return false;
}
if (!currentRules.value) {
return isRequired.value;
}
if (isRequired.value) {
return true;
}
if (isString(currentRules.value)) {
return ['required', 'selectRequired'].includes(currentRules.value);
}
let isOptional = currentRules?.value?.isOptional?.();
// 如果有设置默认值,则不是必填,需要特殊处理
const typeName = currentRules?.value?._def?.typeName;
if (typeName === 'ZodDefault') {
const innerType = currentRules?.value?._def.innerType;
if (innerType) {
isOptional = innerType.isOptional?.();
}
}
return !isOptional;
});
const fieldRules = computed(() => {
if (!visible.value) {
return null;
}
let rules = currentRules.value;
if (!rules) {
return isRequired.value ? 'required' : null;
}
if (isString(rules)) {
return rules;
}
const isOptional = !shouldRequired.value;
if (!isOptional) {
const unwrappedRules = (rules as any)?.unwrap?.();
if (unwrappedRules) {
rules = unwrappedRules;
}
}
return toTypedSchema(rules as ZodType);
});
const computedProps = computed(() => {
const finalComponentProps = isFunction(componentProps)
? componentProps(values.value, formApi!)
: componentProps;
return {
...commonComponentProps,
...finalComponentProps,
...dynamicComponentProps.value,
};
});
watch(
() => computedProps.value?.autofocus,
(value) => {
if (value === true) {
nextTick(() => {
autofocus();
});
}
},
{ immediate: true },
);
const shouldDisabled = computed(() => {
return isDisabled.value || disabled || computedProps.value?.disabled;
});
const customContentRender = computed(() => {
if (!isFunction(renderComponentContent)) {
return {};
}
return renderComponentContent(values.value, formApi!);
});
const renderContentKey = computed(() => {
return Object.keys(customContentRender.value);
});
const fieldProps = computed(() => {
const rules = fieldRules.value;
return {
keepValue: true,
label: isString(label) ? label : '',
...(rules ? { rules } : {}),
...(formFieldProps as Record<string, any>),
};
});
function fieldBindEvent(slotProps: Record<string, any>) {
const modelValue = slotProps.componentField.modelValue;
const handler = slotProps.componentField['onUpdate:modelValue'];
const bindEventField =
modelPropName ||
(isString(component) ? componentBindEventMap.value?.[component] : null);
let value = modelValue;
// antd design 的一些组件会传递一个 event 对象
if (modelValue && isObject(modelValue) && bindEventField) {
value = isEventObjectLike(modelValue)
? modelValue?.target?.[bindEventField]
: (modelValue?.[bindEventField] ?? modelValue);
}
if (bindEventField) {
return {
[`onUpdate:${bindEventField}`]: handler,
[bindEventField]: value === undefined ? emptyStateValue : value,
onChange: disabledOnChangeListener
? undefined
: (e: Record<string, any>) => {
const shouldUnwrap = isEventObjectLike(e);
const onChange = slotProps?.componentField?.onChange;
if (!shouldUnwrap) {
return onChange?.(e);
}
return onChange?.(e?.target?.[bindEventField] ?? e);
},
...(disabledOnInputListener ? { onInput: undefined } : {}),
};
}
return {
...(disabledOnInputListener ? { onInput: undefined } : {}),
...(disabledOnChangeListener ? { onChange: undefined } : {}),
};
}
function createComponentProps(slotProps: Record<string, any>) {
const bindEvents = fieldBindEvent(slotProps);
const binds = {
...slotProps.componentField,
...computedProps.value,
...bindEvents,
...(Reflect.has(computedProps.value, 'onChange')
? { onChange: computedProps.value.onChange }
: {}),
...(Reflect.has(computedProps.value, 'onInput')
? { onInput: computedProps.value.onInput }
: {}),
};
return binds;
}
function autofocus() {
if (
fieldComponentRef.value &&
isFunction(fieldComponentRef.value.focus) &&
// 检查当前是否有元素被聚焦
document.activeElement !== fieldComponentRef.value
) {
fieldComponentRef.value?.focus?.();
}
}
</script>
<template>
<FormField
v-if="isIf"
v-bind="fieldProps"
v-slot="slotProps"
:name="fieldName"
>
<FormItem
v-show="isShow"
:class="{
'form-valid-error': isInValid,
'flex-col': isVertical,
'flex-row items-center': !isVertical,
'pb-6': !compact,
'pb-2': compact,
}"
class="relative flex"
v-bind="$attrs"
>
<FormLabel
v-if="!hideLabel"
:class="
cn(
'flex leading-6',
{
'mr-2 flex-shrink-0 justify-end': !isVertical,
'mb-1 flex-row': isVertical,
},
labelClass,
)
"
:help="help"
:colon="colon"
:label="label"
:required="shouldRequired && !hideRequiredMark"
:style="labelStyle"
>
<template v-if="label">
<VbenRenderContent :content="label" />
</template>
</FormLabel>
<div class="w-full overflow-hidden">
<div :class="cn('relative flex w-full items-center', wrapperClass)">
<div class="flex-auto overflow-hidden p-[2px]">
<FormControl :class="cn(controlClass)">
<slot
v-bind="{
...slotProps,
...createComponentProps(slotProps),
disabled: shouldDisabled,
isInValid,
}"
>
<component
:is="FieldComponent"
ref="fieldComponentRef"
:class="{
'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
isInValid,
}"
v-bind="createComponentProps(slotProps)"
:disabled="shouldDisabled"
>
<template
v-for="name in renderContentKey"
:key="name"
#[name]="renderSlotProps"
>
<VbenRenderContent
:content="customContentRender[name]"
v-bind="{ ...renderSlotProps, formContext: slotProps }"
/>
</template>
<!-- <slot></slot> -->
</component>
</slot>
</FormControl>
</div>
<!-- 自定义后缀 -->
<div v-if="suffix" class="ml-1">
<VbenRenderContent :content="suffix" />
</div>
<FormDescription v-if="description" class="ml-1">
<VbenRenderContent :content="description" />
</FormDescription>
</div>
<Transition name="slide-up">
<FormMessage class="absolute bottom-1" />
</Transition>
</div>
</FormItem>
</FormField>
</template>
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { CustomRenderType } from '../types';
import {
FormLabel,
VbenHelpTooltip,
VbenRenderContent,
} from '/@/vben/shadcn-ui';
import { cn } from '/@/vben/shared/utils';
interface Props {
class?: string;
colon?: boolean;
help?: CustomRenderType;
label?: CustomRenderType;
required?: boolean;
}
const props = defineProps<Props>();
</script>
<template>
<FormLabel :class="cn('flex items-center', props.class)">
<span v-if="required" class="text-destructive mr-[2px]">*</span>
<slot></slot>
<VbenHelpTooltip v-if="help" trigger-class="size-3.5 ml-1">
<VbenRenderContent :content="help" />
</VbenHelpTooltip>
<span v-if="colon && label" class="ml-[2px]">:</span>
</FormLabel>
</template>
@@ -0,0 +1,146 @@
<script setup lang="ts">
import type { GenericObject } from "vee-validate";
import type { ZodTypeAny } from "zod";
import type { FormCommonConfig, FormRenderProps, FormSchema, FormShape } from "../types";
import { computed } from "vue";
import { Form } from "../../shadcn-ui";
import { cn, isString, mergeWithArrayOverride } from "../../shared/utils";
import { provideFormRenderProps } from "./context";
import { useExpandable } from "./expandable";
import FormField from "./form-field.vue";
import { getBaseRules, getDefaultValueInZodStack } from "./helper";
interface Props extends FormRenderProps {}
const props = withDefaults(defineProps<Props & { globalCommonConfig?: FormCommonConfig }>(), {
collapsedRows: 1,
commonConfig: () => ({}),
globalCommonConfig: () => ({}),
showCollapseButton: false,
wrapperClass: "grid-cols-1 sm:grid-cols-2 md:grid-cols-3"
});
const emits = defineEmits<{
submit: [event: any];
}>();
provideFormRenderProps(props);
const { isCalculated, keepFormItemIndex, wrapperRef } = useExpandable(props);
const shapes = computed(() => {
const resultShapes: FormShape[] = [];
props.schema?.forEach((schema) => {
const { fieldName } = schema;
const rules = schema.rules as ZodTypeAny;
let typeName = "";
if (rules && !isString(rules)) {
typeName = rules._def.typeName;
}
const baseRules = getBaseRules(rules) as ZodTypeAny;
resultShapes.push({
default: getDefaultValueInZodStack(rules),
fieldName,
required: !["ZodNullable", "ZodOptional"].includes(typeName),
rules: baseRules
});
});
return resultShapes;
});
const formComponent = computed(() => (props.form ? "form" : Form));
const formComponentProps = computed(() => {
return props.form
? {
onSubmit: props.form.handleSubmit((val: any) => emits("submit", val))
}
: {
onSubmit: (val: GenericObject) => emits("submit", val)
};
});
const formCollapsed = computed(() => {
return props.collapsed && isCalculated.value;
});
const computedSchema = computed(
(): (Omit<FormSchema, "formFieldProps"> & {
commonComponentProps: Record<string, any>;
formFieldProps: Record<string, any>;
})[] => {
const {
colon = false,
componentProps = {},
controlClass = "",
disabled,
disabledOnChangeListener = true,
disabledOnInputListener = true,
emptyStateValue = undefined,
formFieldProps = {},
formItemClass = "",
hideLabel = false,
hideRequiredMark = false,
labelClass = "",
labelWidth = 100,
modelPropName = "",
wrapperClass = ""
} = mergeWithArrayOverride(props.commonConfig, props.globalCommonConfig);
return (props.schema || []).map((schema, index) => {
const keepIndex = keepFormItemIndex.value;
const hidden =
// 折叠状态 & 显示折叠按钮 & 当前索引大于保留索引
props.showCollapseButton && !!formCollapsed.value && keepIndex ? keepIndex <= index : false;
return {
colon,
disabled,
disabledOnChangeListener,
disabledOnInputListener,
emptyStateValue,
hideLabel,
hideRequiredMark,
labelWidth,
modelPropName,
wrapperClass,
...schema,
commonComponentProps: componentProps,
componentProps: schema.componentProps,
controlClass: cn(controlClass, schema.controlClass),
formFieldProps: {
...formFieldProps,
...schema.formFieldProps
},
formItemClass: cn("flex-shrink-0", { hidden }, formItemClass, schema.formItemClass),
labelClass: cn(labelClass, schema.labelClass)
};
});
}
);
</script>
<template>
<component :is="formComponent" v-bind="formComponentProps">
<div ref="wrapperRef" :class="wrapperClass" class="grid">
<template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot>
</div> -->
<FormField v-bind="cSchema" :class="cSchema.formItemClass" :rules="cSchema.rules">
<template #default="slotProps">
<slot v-bind="slotProps" :name="cSchema.fieldName"> </slot>
</template>
</FormField>
</template>
<slot :shapes="shapes"></slot>
</div>
</component>
</template>
@@ -0,0 +1,60 @@
import type {
AnyZodObject,
ZodDefault,
ZodEffects,
ZodNumber,
ZodString,
ZodTypeAny,
} from 'zod';
import { isObject, isString } from '/@/vben/shared/utils';
/**
* Get the lowest level Zod type.
* This will unpack optionals, refinements, etc.
*/
export function getBaseRules<
ChildType extends AnyZodObject | ZodTypeAny = ZodTypeAny,
>(schema: ChildType | ZodEffects<ChildType>): ChildType | null {
if (!schema || isString(schema)) return null;
if ('innerType' in schema._def)
return getBaseRules(schema._def.innerType as ChildType);
if ('schema' in schema._def)
return getBaseRules(schema._def.schema as ChildType);
return schema as ChildType;
}
/**
* Search for a "ZodDefault" in the Zod stack and return its value.
*/
export function getDefaultValueInZodStack(schema: ZodTypeAny): any {
if (!schema || isString(schema)) {
return;
}
const typedSchema = schema as unknown as ZodDefault<ZodNumber | ZodString>;
if (typedSchema._def.typeName === 'ZodDefault')
return typedSchema._def.defaultValue();
if ('innerType' in typedSchema._def) {
return getDefaultValueInZodStack(
typedSchema._def.innerType as unknown as ZodTypeAny,
);
}
if ('schema' in typedSchema._def) {
return getDefaultValueInZodStack(
(typedSchema._def as any).schema as ZodTypeAny,
);
}
return undefined;
}
export function isEventObjectLike(obj: any) {
if (!obj || !isObject(obj)) {
return false;
}
return Reflect.has(obj, 'target') && Reflect.has(obj, 'stopPropagation');
}
@@ -0,0 +1,3 @@
export { default as FormField } from './form-field.vue';
export { default as FormLabel } from './form-label.vue';
export { default as Form } from './form.vue';
@@ -0,0 +1,7 @@
export { setupVbenForm } from "./config";
export type { BaseFormComponentType, ExtendedFormApi, VbenFormProps, FormSchema as VbenFormSchema } from "./types";
export * from "./use-vben-form";
// export { default as VbenForm } from './vben-form.vue';
export * as z from "zod";
@@ -0,0 +1,343 @@
import type { FieldOptions, FormContext, GenericObject } from "vee-validate";
import type { ZodTypeAny } from "zod";
import type { Component, HtmlHTMLAttributes, Ref } from "vue";
import type { VbenButtonProps } from "/@/vben/shadcn-ui";
import type { ClassType, MaybeComputedRef } from "/@/vben/typings";
import type { FormApi } from "./form-api";
export type FormLayout = "horizontal" | "vertical";
export type BaseFormComponentType = "DefaultButton" | "PrimaryButton" | "VbenCheckbox" | "VbenInput" | "VbenInputPassword" | "VbenPinInput" | "VbenSelect" | (Record<never, never> & string);
type Breakpoints = "2xl:" | "3xl:" | "" | "lg:" | "md:" | "sm:" | "xl:";
type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
export type WrapperClassType = `${Breakpoints}grid-cols-${GridCols}` | (Record<never, never> & string);
export type FormItemClassType =
| `${Breakpoints}cols-end-${"auto" | GridCols}`
| `${Breakpoints}cols-span-${"auto" | "full" | GridCols}`
| `${Breakpoints}cols-start-${"auto" | GridCols}`
| (Record<never, never> & string)
| WrapperClassType;
export type FormFieldOptions = Partial<
FieldOptions & {
validateOnBlur?: boolean;
validateOnChange?: boolean;
validateOnInput?: boolean;
validateOnModelUpdate?: boolean;
}
>;
export interface FormShape {
/** 默认值 */
default?: any;
/** 字段名 */
fieldName: string;
/** 是否必填 */
required?: boolean;
rules?: ZodTypeAny;
}
export type MaybeComponentPropKey = "options" | "placeholder" | "title" | keyof HtmlHTMLAttributes | (Record<never, never> & string);
export type MaybeComponentProps = { [K in MaybeComponentPropKey]?: any };
export type FormActions = FormContext<GenericObject>;
export type CustomRenderType = (() => Component | string) | string;
export type FormSchemaRuleType = "required" | "selectRequired" | null | (Record<never, never> & string) | ZodTypeAny;
type FormItemDependenciesCondition<T = boolean | PromiseLike<boolean>> = (value: Partial<Record<string, any>>, actions: FormActions) => T;
type FormItemDependenciesConditionWithRules = (value: Partial<Record<string, any>>, actions: FormActions) => FormSchemaRuleType | PromiseLike<FormSchemaRuleType>;
type FormItemDependenciesConditionWithProps = (value: Partial<Record<string, any>>, actions: FormActions) => MaybeComponentProps | PromiseLike<MaybeComponentProps>;
export interface FormItemDependencies {
/**
* 组件参数
* @returns 组件参数
*/
componentProps?: FormItemDependenciesConditionWithProps;
/**
* 是否禁用
* @returns 是否禁用
*/
disabled?: boolean | FormItemDependenciesCondition;
/**
* 是否渲染(删除dom
* @returns 是否渲染
*/
if?: boolean | FormItemDependenciesCondition;
/**
* 是否必填
* @returns 是否必填
*/
required?: FormItemDependenciesCondition;
/**
* 字段规则
*/
rules?: FormItemDependenciesConditionWithRules;
/**
* 是否隐藏(Css)
* @returns 是否隐藏
*/
show?: boolean | FormItemDependenciesCondition;
/**
* 任意触发都会执行
*/
trigger?: FormItemDependenciesCondition<void>;
/**
* 触发字段
*/
triggerFields: string[];
}
type ComponentProps = ((value: Partial<Record<string, any>>, actions: FormActions) => MaybeComponentProps) | MaybeComponentProps;
export interface FormCommonConfig {
/**
* 在Label后显示一个冒号
*/
colon?: boolean;
/**
* 所有表单项的props
*/
componentProps?: ComponentProps;
/**
* 所有表单项的控件样式
*/
controlClass?: string;
/**
* 所有表单项的禁用状态
* @default false
*/
disabled?: boolean;
/**
* 是否禁用所有表单项的change事件监听
* @default true
*/
disabledOnChangeListener?: boolean;
/**
* 是否禁用所有表单项的input事件监听
* @default true
*/
disabledOnInputListener?: boolean;
/**
* 所有表单项的空状态值,默认都是undefinednaive-ui的空状态值是null
*/
emptyStateValue?: null | undefined;
/**
* 所有表单项的控件样式
* @default {}
*/
formFieldProps?: FormFieldOptions;
/**
* 所有表单项的栅格布局
* @default ""
*/
formItemClass?: string;
/**
* 隐藏所有表单项label
* @default false
*/
hideLabel?: boolean;
/**
* 是否隐藏必填标记
* @default false
*/
hideRequiredMark?: boolean;
/**
* 所有表单项的label样式
* @default ""
*/
labelClass?: string;
/**
* 所有表单项的label宽度
*/
labelWidth?: number;
/**
* 所有表单项的model属性名
* @default "modelValue"
*/
modelPropName?: string;
/**
* 所有表单项的wrapper样式
*/
wrapperClass?: string;
}
type RenderComponentContentType = (value: Partial<Record<string, any>>, api: FormActions) => Record<string, any>;
export type HandleSubmitFn = (values: Record<string, any>) => Promise<void> | void;
export type HandleResetFn = (values: Record<string, any>) => Promise<void> | void;
export type FieldMappingTime = [string, [string, string], (((value: any, fieldName: string) => any) | [string, string] | null | string)?][];
export interface FormSchema<T extends BaseFormComponentType = BaseFormComponentType> extends FormCommonConfig {
/** 组件 */
component: Component | T;
/** 组件参数 */
componentProps?: ComponentProps;
/** 默认值 */
defaultValue?: any;
/** 依赖 */
dependencies?: FormItemDependencies;
/** 描述 */
description?: CustomRenderType;
/** 字段名 */
fieldName: string;
/** 帮助信息 */
help?: CustomRenderType;
/** 表单项 */
label?: CustomRenderType;
// 自定义组件内部渲染
renderComponentContent?: RenderComponentContentType;
/** 字段规则 */
rules?: FormSchemaRuleType;
/** 后缀 */
suffix?: CustomRenderType;
}
export interface FormFieldProps extends FormSchema {
required?: boolean;
}
export interface FormRenderProps<T extends BaseFormComponentType = BaseFormComponentType> {
/**
* 是否展开,在showCollapseButton=true下生效
*/
collapsed?: boolean;
/**
* 折叠时保持行数
* @default 1
*/
collapsedRows?: number;
/**
* 是否触发resize事件
* @default false
*/
collapseTriggerResize?: boolean;
/**
* 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
*/
commonConfig?: FormCommonConfig;
/**
* 紧凑模式(移除表单每一项底部为校验信息预留的空间)
*/
compact?: boolean;
/**
* 组件v-model事件绑定
*/
componentBindEventMap?: Partial<Record<BaseFormComponentType, string>>;
/**
* 组件集合
*/
componentMap: Record<BaseFormComponentType, Component>;
/**
* 表单实例
*/
form?: FormContext<GenericObject>;
/**
* 表单项布局
*/
layout?: FormLayout;
/**
* 表单定义
*/
schema?: FormSchema<T>[];
/**
* 是否显示展开/折叠
*/
showCollapseButton?: boolean;
/**
* 表单栅格布局
* @default "grid-cols-1"
*/
wrapperClass?: WrapperClassType;
}
export interface ActionButtonOptions extends VbenButtonProps {
[key: string]: any;
content?: MaybeComputedRef<string>;
show?: boolean;
}
export interface VbenFormProps<T extends BaseFormComponentType = BaseFormComponentType> extends Omit<FormRenderProps<T>, "componentBindEventMap" | "componentMap" | "form"> {
/**
* 操作按钮是否反转(提交按钮前置)
*/
actionButtonsReverse?: boolean;
/**
* 表单操作区域class
*/
actionWrapperClass?: ClassType;
/**
* 表单字段映射
*/
fieldMappingTime?: FieldMappingTime;
/**
* 表单重置回调
*/
handleReset?: HandleResetFn;
/**
* 表单提交回调
*/
handleSubmit?: HandleSubmitFn;
/**
* 表单值变化回调
*/
handleValuesChange?: (values: Record<string, any>) => void;
/**
* 重置按钮参数
*/
resetButtonOptions?: ActionButtonOptions;
/**
* 是否显示默认操作按钮
* @default true
*/
showDefaultActions?: boolean;
/**
* 提交按钮参数
*/
submitButtonOptions?: ActionButtonOptions;
/**
* 是否在字段值改变时提交表单
* @default false
*/
submitOnChange?: boolean;
/**
* 是否在回车时提交表单
* @default false
*/
submitOnEnter?: boolean;
}
export type ExtendedFormApi = FormApi & {
useStore: <T = NoInfer<VbenFormProps>>(selector?: (state: NoInfer<VbenFormProps>) => T) => Readonly<Ref<T>>;
};
export interface VbenFormAdapterOptions<T extends BaseFormComponentType = BaseFormComponentType> {
config?: {
baseModelPropName?: string;
disabledOnChangeListener?: boolean;
disabledOnInputListener?: boolean;
emptyStateValue?: null | undefined;
modelPropNameMap?: Partial<Record<T, string>>;
};
defineRules?: {
required?: (value: any, params: any, ctx: Record<string, any>) => boolean | string;
selectRequired?: (value: any, params: any, ctx: Record<string, any>) => boolean | string;
};
}
@@ -0,0 +1,60 @@
import type { ZodRawShape } from "zod";
import type { ComputedRef } from "vue";
import type { ExtendedFormApi, FormActions, VbenFormProps } from "./types";
import { computed, unref, useSlots } from "vue";
import { createContext } from "/@/vben/shadcn-ui";
import { isString } from "/@/vben/shared/utils";
import { useForm } from "vee-validate";
import { object } from "zod";
import { getDefaultsForSchema } from "zod-defaults";
type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
export const [injectFormProps, provideFormProps] = createContext<[ComputedRef<ExtendFormProps> | ExtendFormProps, FormActions]>("VbenFormProps");
export function useFormInitial(props: ComputedRef<VbenFormProps> | VbenFormProps) {
const slots = useSlots();
const initialValues = generateInitialValues();
const form = useForm({
...(Object.keys(initialValues)?.length ? { initialValues } : {})
});
const delegatedSlots = computed(() => {
const resultSlots: string[] = [];
for (const key of Object.keys(slots)) {
if (key !== "default") {
resultSlots.push(key);
}
}
return resultSlots;
});
function generateInitialValues() {
const initialValues: Record<string, any> = {};
const zodObject: ZodRawShape = {};
(unref(props).schema || []).forEach((item) => {
if (Reflect.has(item, "defaultValue")) {
initialValues[item.fieldName] = item.defaultValue;
} else if (item.rules && !isString(item.rules)) {
zodObject[item.fieldName] = item.rules;
}
});
const schemaInitialValues = getDefaultsForSchema(object(zodObject));
return { ...initialValues, ...schemaInitialValues };
}
return {
delegatedSlots,
form
};
}
@@ -0,0 +1,43 @@
import type { BaseFormComponentType, ExtendedFormApi, VbenFormProps } from "./types";
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from "vue";
import { useStore } from "/@/vben/shared/store";
import { FormApi } from "./form-api";
import VbenUseForm from "./vben-use-form.vue";
export function useVbenForm<T extends BaseFormComponentType = BaseFormComponentType>(options: VbenFormProps<T>) {
const IS_REACTIVE = isReactive(options);
const api = new FormApi(options);
const extendedApi: ExtendedFormApi = api as never;
extendedApi.useStore = (selector) => {
return useStore(api.store, selector);
};
const Form = defineComponent(
(props: VbenFormProps, { attrs, slots }) => {
onBeforeUnmount(() => {
api.unmount();
});
api.setState({ ...props, ...attrs });
return () => h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
},
{
name: "VbenUseForm",
inheritAttrs: false
}
);
// Add reactivity support
if (IS_REACTIVE) {
watch(
() => options.schema,
() => {
api.setState({ schema: options.schema });
},
{ immediate: true }
);
}
return [Form, extendedApi] as const;
}
@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { VbenFormProps } from "./types";
import { ref, watchEffect } from "vue";
import { useForwardPropsEmits } from "../composables";
import FormActions from "./components/form-actions.vue";
import { COMPONENT_BIND_EVENT_MAP, COMPONENT_MAP, DEFAULT_FORM_COMMON_CONFIG } from "./config";
import { Form } from "./form-render";
import { provideFormProps, useFormInitial } from "./use-form-context";
// 通过 extends 会导致热更新卡死
interface Props extends VbenFormProps {}
const props = withDefaults(defineProps<Props>(), {
actionWrapperClass: "",
collapsed: false,
collapsedRows: 1,
commonConfig: () => ({}),
handleReset: undefined,
handleSubmit: undefined,
layout: "horizontal",
resetButtonOptions: () => ({}),
showCollapseButton: false,
showDefaultActions: true,
submitButtonOptions: () => ({}),
wrapperClass: "grid-cols-1"
});
const forward = useForwardPropsEmits(props);
const currentCollapsed = ref(false);
const { delegatedSlots, form } = useFormInitial(props);
provideFormProps([props, form]);
const handleUpdateCollapsed = (value: boolean) => {
currentCollapsed.value = !!value;
};
watchEffect(() => {
currentCollapsed.value = props.collapsed;
});
</script>
<template>
<Form v-bind="forward" :collapsed="currentCollapsed" :component-bind-event-map="COMPONENT_BIND_EVENT_MAP" :component-map="COMPONENT_MAP" :form="form" :global-common-config="DEFAULT_FORM_COMMON_CONFIG">
<template v-for="slotName in delegatedSlots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
<template #default="slotProps">
<slot v-bind="slotProps">
<FormActions v-if="showDefaultActions" :model-value="currentCollapsed" @update:model-value="handleUpdateCollapsed" />
</slot>
</template>
</Form>
</template>
@@ -0,0 +1,96 @@
<script setup lang="ts">
import type { ExtendedFormApi, VbenFormProps } from "./types";
// import { toRaw, watch } from 'vue';
import { nextTick, onMounted, watch } from "vue";
// import { isFunction } from '/@/vben/shared/utils';
import { useForwardPriorityValues } from "../composables";
import { cloneDeep } from "../shared/utils";
import { useDebounceFn } from "@vueuse/core";
import FormActions from "./components/form-actions.vue";
import { COMPONENT_BIND_EVENT_MAP, COMPONENT_MAP, DEFAULT_FORM_COMMON_CONFIG } from "./config";
import { Form } from "./form-render";
import { provideFormProps, useFormInitial } from "./use-form-context";
// 通过 extends 会导致热更新卡死,所以重复写了一遍
interface Props extends VbenFormProps {
formApi: ExtendedFormApi;
}
const props = defineProps<Props>();
const state = props.formApi?.useStore?.();
const forward = useForwardPriorityValues(props, state);
const { delegatedSlots, form } = useFormInitial(forward);
provideFormProps([forward, form]);
props.formApi?.mount?.(form);
const handleUpdateCollapsed = (value: boolean) => {
props.formApi?.setState({ collapsed: !!value });
};
function handleKeyDownEnter(event: KeyboardEvent) {
if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {
return;
}
// 如果是 textarea 不阻止默认行为,否则会导致无法换行。
// 跳过 textarea 的回车提交处理
if (event.target instanceof HTMLTextAreaElement) {
return;
}
event.preventDefault();
forward.value.formApi.validateAndSubmitForm();
}
const handleValuesChangeDebounced = useDebounceFn(async () => {
forward.value.handleValuesChange?.(cloneDeep(await forward.value.formApi.getValues()));
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
}, 300);
onMounted(async () => {
// 只在挂载后开始监听,form.values会有一个初始化的过程
await nextTick();
watch(() => form.values, handleValuesChangeDebounced, { deep: true });
});
</script>
<template>
<Form
v-bind="forward"
:collapsed="state.collapsed"
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
:component-map="COMPONENT_MAP"
:form="form"
:global-common-config="DEFAULT_FORM_COMMON_CONFIG"
@keydown.enter="handleKeyDownEnter"
>
<template v-for="slotName in delegatedSlots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
<template #default="slotProps">
<slot v-bind="slotProps">
<FormActions v-if="forward.showDefaultActions" :model-value="state.collapsed" @update:model-value="handleUpdateCollapsed">
<template #reset-before="resetSlotProps">
<slot name="reset-before" v-bind="resetSlotProps"></slot>
</template>
<template #submit-before="submitSlotProps">
<slot name="submit-before" v-bind="submitSlotProps"></slot>
</template>
<template #expand-before="expandBeforeSlotProps">
<slot name="expand-before" v-bind="expandBeforeSlotProps"></slot>
</template>
<template #expand-after="expandAfterSlotProps">
<slot name="expand-after" v-bind="expandAfterSlotProps"></slot>
</template>
</FormActions>
</slot>
</template>
</Form>
</template>