签入版本

This commit is contained in:
2026-09-11 11:39:07 +08:00
commit c381201858
928 changed files with 116443 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
export { default as BasicForm } from './src/BasicForm.vue';
export { useForm } from './src/hooks/useForm';
export * from './src/types/form';
export * from './src/types/index';
+392
View File
@@ -0,0 +1,392 @@
<template>
<el-form v-bind="getBindValue" :model="formModel" ref="formElRef">
<el-row v-bind="getRow">
<el-col
v-bind="getCol"
v-for="schema in getSchema"
:key="schema.field"
:span="getHidden(schema) ? 0 : getCol.span"
>
<el-form-item
v-bind="schema"
:label="schema.label"
:prop="schema.field"
:showFeedback="schema.showFeedback"
v-if="!getHidden(schema)"
>
<!--标签名右侧温馨提示-->
<template #label v-if="schema.labelMessage">
<div class="flex items-center">
{{ schema.label }}
<el-tooltip
trigger="hover"
:style="schema.labelMessageStyle"
:content="schema.labelMessage"
>
<el-icon size="18" class="ml-1 text-gray-400 cursor-pointer el-input__icon">
<QuestionCircleOutlined />
</el-icon>
</el-tooltip>
</div>
</template>
<!--判断插槽-->
<template v-if="schema.slot">
<slot
:name="schema.slot"
:model="formModel"
:field="schema.field"
:value="formModel[schema.field]"
></slot>
</template>
<!--NCheckbox-->
<template v-else-if="schema.component === 'Checkbox'">
<el-checkbox-group
v-bind="getSpecComponentProps(schema)"
v-model="formModel[schema.field]"
:class="{ isFull: schema.isFull != false && getProps.isFull }"
>
<el-space>
<el-checkbox
v-for="item in schema.componentProps?.options"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</el-checkbox>
</el-space>
</el-checkbox-group>
</template>
<!--elRadioGroup-->
<template v-else-if="schema.component === 'RadioGroup'">
<el-radio-group
v-bind="getSpecComponentProps(schema)"
v-model="formModel[schema.field]"
:class="{ isFull: schema.isFull != false && getProps.isFull }"
>
<el-space>
<el-radio
v-for="item in schema.componentProps?.options"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</el-radio>
</el-space>
</el-radio-group>
</template>
<!--elSelect-->
<template v-else-if="schema.component === 'Select'">
<el-select
v-bind="getSpecComponentProps(schema)"
v-model="formModel[schema.field]"
:class="{ isFull: schema.isFull != false && getProps.isFull }"
>
<el-option
v-for="item in schema.componentProps?.options"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-select>
</template>
<!-- 时间段选择器 -->
<template v-else-if="schema.component === 'Daterange'">
<el-date-picker
v-bind="getSpecComponentProps(schema)"
value-format="YYYY-MM-DD"
v-model="formModel[schema.field]"
style="width: 220px"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</template>
<!--BasicSelect-->
<template v-else-if="schema.component === 'BasicSelect'">
<BasicSelect v-bind="getSpecComponentProps(schema)" v-model="formModel[schema.field]" />
</template>
<!--动态渲染表单组件-->
<component
v-else
v-bind="getComponentProps(schema)"
:is="`el-${schema.component}`"
v-model="formModel[schema.field]"
:type="schema.type"
/>
<!--组件后面的内容-->
<template v-if="schema.suffix">
<slot
:name="schema.suffix"
:model="formModel"
:field="schema.field"
:value="formModel[schema.field]"
></slot>
</template>
</el-form-item>
</el-col>
<!--提交 重置 展开 收起 按钮-->
<el-col :span="6" v-if="getProps.showActionButtonGroup">
<el-form-item :label-width="isHorizontal ? '20px' : getProps.labelWidth">
<el-space>
<el-button
v-if="getProps.showSubmitButton"
v-bind="getSubmitBtnOptions"
@click="handleSubmit"
icon="Search"
:loading="loadingSub"
>{{ getProps.submitButtonText }}</el-button
>
<el-button
v-if="getProps.showResetButton"
v-bind="getResetBtnOptions"
@click="resetFields"
>{{ getProps.resetButtonText }}</el-button
>
<!-- <el-button
type="primary"
text
icon-placement="right"
v-if="isHorizontal && getProps.showAdvancedButton"
@click="unfoldToggle"
>
<template #icon>
<el-icon size="14" class="el-input__icon unfold-icon" v-if="overflow">
<DownOutlined />
</el-icon>
<el-icon size="14" class="el-input__icon unfold-icon" v-else>
<UpOutlined />
</el-icon>
</template>
{{ overflow ? '展开' : '收起' }}
</el-button> -->
</el-space>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import { reactive, ref, useAttrs, computed, unref, onMounted, watch } from 'vue';
import { createPlaceholderMessage } from './helper';
import { useFormEvents } from './hooks/useFormEvents';
import { useFormValues } from './hooks/useFormValues';
import { BasicSelect } from '@/components/Select';
import { basicProps } from './props';
import { QuestionCircleOutlined } from '@vicons/antd';
import type { Ref } from 'vue';
import type { FormSchema, FormProps, FormActionType } from './types/form';
import { isArray, isBoolean, isFunction } from '@/utils/is/index';
import { deepMerge } from '@/utils';
const props = defineProps({ ...basicProps });
const emit = defineEmits(['reset', 'submit', 'register', 'advanced']);
const attrs = useAttrs();
const defaultFormModel = ref<Recordable>({});
const formModel = reactive<Recordable>({});
const propsRef = ref<Partial<FormProps>>({});
const schemaRef = ref<Nullable<FormSchema[]>>(null);
const formElRef = ref<Nullable<FormActionType>>(null);
// const gridCollapsed = ref(!props.collapsed);
const loadingSub = ref(false);
const isUpdateDefaultRef = ref(false);
const getSubmitBtnOptions = computed(() => {
return Object.assign(
{
size: props.size,
type: 'primary',
},
props.submitButtonOptions,
);
});
const getResetBtnOptions = computed(() => {
return Object.assign(
{
size: props.size,
type: 'default',
},
props.resetButtonOptions,
);
});
function getComponentProps(schema) {
const compProps = schema.componentProps ?? {};
const component = schema.component;
return {
class: schema.isFull != false && getProps.value.isFull ? 'w-full' : '',
clearable: true,
placeholder: createPlaceholderMessage(unref(component)),
...compProps,
};
}
function getHidden(schema): boolean {
const hidden = schema.hidden;
const field = schema.field;
if (isBoolean(hidden)) return hidden;
if (isFunction(hidden)) {
const values = getFieldsValue();
const status = hidden({ schema, values, model: formModel, field });
return status;
}
return false;
}
function getSpecComponentProps(schema) {
const compProps = schema.componentProps ?? {};
return {
...compProps,
};
}
const getProps = computed((): FormProps => {
const formProps = { ...props, ...unref(propsRef) } as FormProps;
const rulesObj: any = {
rules: {},
};
const schemas: any = formProps.schemas || [];
schemas.forEach((item) => {
if (item.rules && isArray(item.rules)) {
rulesObj.rules[item.field] = item.rules;
}
});
return { ...formProps, ...unref(rulesObj) };
});
const isHorizontal = computed(() => {
const { layout } = unref(getProps);
return layout === 'horizontal';
});
const getRow = computed(() => {
const { rowProps } = unref(getProps);
return {
...rowProps,
// collapsed: isHorizontal .value ? gridCollapsed.value : false,
// responsive: 'screen',
};
});
const getCol = computed(() => {
const { colProps } = unref(getProps);
return {
...colProps,
// collapsed: isHorizontal .value ? gridCollapsed.value : false,
// responsive: 'screen',
};
});
const getBindValue = computed(() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable));
const getSchema = computed((): FormSchema[] => {
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
for (const schema of schemas) {
const { defaultValue } = schema;
// handle date type
// dateItemType.includes(component as string)
if (defaultValue) {
schema.defaultValue = defaultValue;
}
}
return schemas as FormSchema[];
});
const { handleFormValues, initDefault } = useFormValues({
defaultFormModel,
getSchema,
formModel,
});
const { handleSubmit, validate, resetFields, getFieldsValue, clearValidate, setFieldsValue } =
useFormEvents({
emit,
getProps,
formModel,
getSchema,
formElRef: formElRef as Ref<FormActionType>,
defaultFormModel,
loadingSub,
handleFormValues,
});
// function unfoldToggle() {
// gridCollapsed.value = !gridCollapsed.value;
// emit('advanced', gridCollapsed.value);
// }
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
async function setSchema(schemaProps: FormSchema[]): Promise<void> {
schemaRef.value = schemaProps;
}
//更新loading状态
function setLoadingSub(status: boolean): void {
loadingSub.value = status;
}
const formActionType: Partial<FormActionType> = {
getFieldsValue,
setFieldsValue,
resetFields,
validate,
clearValidate,
setProps,
setSchema,
setLoadingSub,
submit: handleSubmit,
};
watch(
() => getSchema.value,
(schema) => {
if (unref(isUpdateDefaultRef)) {
return;
}
if (schema?.length) {
initDefault();
isUpdateDefaultRef.value = true;
}
},
);
onMounted(() => {
initDefault();
emit('register', formActionType);
});
defineExpose({
...formActionType,
});
</script>
<style lang="scss" scoped>
.isFull {
width: 100%;
justify-content: flex-start;
}
.unfold-icon {
display: flex;
align-items: center;
height: 100%;
margin-left: -3px;
}
</style>
+36
View File
@@ -0,0 +1,36 @@
import { ComponentType } from './types/index';
/**
* @description: 生成 placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component === 'Input') return '请输入';
if (
['Picker', 'Select', 'Checkbox', 'Radio', 'Switch', 'DatePicker', 'TimePicker'].includes(
component,
)
)
return '请选择';
return '';
}
const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'];
function genType() {
return [...DATE_TYPE, 'RangePicker'];
}
/**
* 时间字段
*/
export const dateItemType = genType();
export function defaultType(component) {
if (component === 'Input') return '';
if (component === 'InputNumber') return null;
return ['Picker', 'Select', 'Checkbox', 'Radio', 'Switch', 'DatePicker', 'TimePicker'].includes(
component,
)
? ''
: undefined;
}
@@ -0,0 +1,96 @@
import type { FormProps, FormActionType, UseFormReturnType } from '../types/form';
import type { DynamicProps } from '/#/utils';
import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
import { isProdMode } from '@/utils/env';
import { getDynamicProps } from '@/utils';
type Props = Partial<DynamicProps<FormProps>>;
export function useForm(props?: Props): UseFormReturnType {
const formRef = ref<Nullable<FormActionType>>(null);
const loadedRef = ref<Nullable<boolean>>(false);
async function getForm() {
const form = unref(formRef);
if (!form) {
console.error(
'The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!',
);
}
await nextTick();
return form as FormActionType;
}
function register(instance: FormActionType) {
isProdMode() &&
onUnmounted(() => {
formRef.value = null;
loadedRef.value = null;
});
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
formRef.value = instance;
loadedRef.value = true;
watch(
() => props,
() => {
props && instance.setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
},
);
}
const methods: FormActionType = {
setProps: async (formProps: Partial<FormProps>) => {
const form = await getForm();
await form.setProps(formProps);
},
resetFields: async () => {
getForm().then(async (form) => {
await form.resetFields();
});
},
clearValidate: async (name?: string | string[]) => {
const form = await getForm();
await form.clearValidate(name);
},
getFieldsValue: <T>() => {
return unref(formRef)?.getFieldsValue() as T;
},
setFieldsValue: async <T>(values: T) => {
const form = await getForm();
await form.setFieldsValue(values as Recordable<any>);
},
submit: async (): Promise<any> => {
const form = await getForm();
return form.submit();
},
validate: async (nameList?: any[]): Promise<Recordable> => {
const form = await getForm();
return form.validate(nameList);
},
setLoadingSub: (value: boolean) => {
loadedRef.value = value;
},
setSchema: async (values) => {
const form = await getForm();
form.setSchema(values);
},
};
return [register, methods];
}
@@ -0,0 +1,11 @@
import { provide, inject } from 'vue';
const key = Symbol('formElRef');
export function createFormContext(instance) {
provide(key, instance);
}
export function useFormContext() {
return inject(key);
}
@@ -0,0 +1,130 @@
import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema, FormActionType } from '../types/form';
import { unref, toRaw } from 'vue';
import { isDef, isFunction } from '@/utils/is';
declare type EmitType = (event: string, ...args: any[]) => void;
interface UseFormActionContext {
emit: EmitType;
getProps: ComputedRef<FormProps>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
formElRef: Ref<FormActionType>;
defaultFormModel: Recordable;
loadingSub: Ref<boolean>;
handleFormValues: Function;
}
export function useFormEvents({
emit,
getProps,
formModel,
getSchema,
formElRef,
defaultFormModel,
loadingSub,
handleFormValues,
}: UseFormActionContext) {
// 验证
async function validate() {
return unref(formElRef)?.validate();
}
// 提交
async function handleSubmit(e?: Event): Promise<object | boolean> {
e && e.preventDefault();
loadingSub.value = true;
const { submitFunc } = unref(getProps);
if (submitFunc && isFunction(submitFunc)) {
await submitFunc();
loadingSub.value = false;
return false;
}
const formEl = unref(formElRef);
if (!formEl) return false;
try {
await validate();
const values = getFieldsValue();
loadingSub.value = false;
emit('submit', values);
return values;
} catch (error: any) {
emit('submit', false);
loadingSub.value = false;
console.error(error);
return false;
}
}
//清空校验
async function clearValidate() {
await unref(formElRef)?.resetFields();
}
//重置
async function resetFields(): Promise<void> {
const { resetFunc, submitOnReset } = unref(getProps);
resetFunc && isFunction(resetFunc) && (await resetFunc());
const formEl = unref(formElRef);
if (!formEl) return;
Object.keys(formModel).forEach((key) => {
formModel[key] = unref(defaultFormModel)[key] || null;
});
await clearValidate();
const fromValues = handleFormValues(toRaw(unref(formModel)));
emit('reset', fromValues);
submitOnReset && (await handleSubmit());
}
//获取表单值
function getFieldsValue(): Recordable {
const formEl = unref(formElRef);
if (!formEl) return {};
return handleFormValues(toRaw(unref(formModel)));
}
//设置表单字段值
function setFieldsValue(values: Recordable): void {
const fields = unref(getSchema)
.map((item) => item.field)
.filter(Boolean);
// 兼容 a.b.c 写法
const decimal = '.';
const decimalKeyList = fields.filter((item) => item.indexOf(decimal) >= 0);
Object.keys(values).forEach((key) => {
const value = values[key];
const isKey = Reflect.has(values, key);
if (isKey && fields.includes(key)) {
formModel[key] = value;
} else {
decimalKeyList.forEach((itemKey: string) => {
try {
const value = eval('values' + decimal + itemKey);
if (isDef(value)) {
formModel[itemKey] = value;
}
} catch (e) {
console.error(e);
}
});
}
});
}
function setLoadingSub(value: boolean): void {
loadingSub.value = value;
}
return {
handleSubmit,
validate,
resetFields,
getFieldsValue,
clearValidate,
setFieldsValue,
setLoadingSub,
};
}
@@ -0,0 +1,84 @@
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '@/utils/is';
import { unref } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import type { FormSchema } from '../types/form';
import { set } from 'lodash-es';
interface UseFormValuesContext {
defaultFormModel: Ref<any>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
}
/**
* @desription 变异目标对象
*/
function strShiftObject(key: string, value: any, target: Recordable) {
const pattern = /^\{(.+)\}$/;
if (pattern.test(key)) {
const match = key.match(pattern);
if (match && match[1]) {
const keys = match[1].split(',');
value = isObject(value) ? value : {};
keys.forEach((k) => {
set(target, k.trim(), value[k.trim()]);
});
return true;
}
}
}
export function useFormValues({ defaultFormModel, getSchema, formModel }: UseFormValuesContext) {
// 加工 form values
function handleFormValues(values: Recordable) {
if (!isObject(values)) {
return {};
}
if (values) {
for (let key in values) {
if (values[key]) {
if (values[key] instanceof Array) {
values[key] = values[key].join(',');
}
}
}
}
const res: Recordable = {};
for (const item of Object.entries(values)) {
let [, value] = item;
const [key] = item;
if (
!key ||
(isArray(value) && value.length === 0) ||
isFunction(value) ||
isNullOrUnDef(value)
) {
continue;
}
// 删除空格
if (isString(value)) {
value = value.trim();
}
if (!strShiftObject(key, value, res)) {
set(res, key, value);
}
}
return res;
}
//初始化默认值
function initDefault() {
const schemas = unref(getSchema);
const obj: Recordable = {};
schemas.forEach((item) => {
const { defaultValue } = item;
if (!isNullOrUnDef(defaultValue)) {
obj[item.field] = defaultValue;
formModel[item.field] = defaultValue;
}
});
defaultFormModel.value = obj;
}
return { handleFormValues, initDefault };
}
+129
View File
@@ -0,0 +1,129 @@
import type { CSSProperties, PropType } from 'vue';
import { FormSchema } from './types/form';
import type { ColProps } from 'element-plus/es/components/col/src/col';
import type { ButtonProps } from 'element-plus/es/components/button/src/button';
import { propTypes } from '@/utils/propTypes';
import { FormRules } from 'element-plus';
export const basicProps = {
//表单数据对象
model: {
type: Object,
},
//表单验证规则
rules: {
type: Object as PropType<FormRules>,
},
//是否展示为行内表单
inline: {
type: Boolean,
default: false,
},
//表单域标签的位置
labelPosition: {
type: String,
default: 'right',
},
// 标签宽度 固定宽度
labelWidth: {
type: [Number, String] as PropType<number | string>,
default: 80,
},
//表单域标签的后缀
labelSuffix: {
type: String,
},
//是否显示必填字段的标签旁边的红色星号
hideRequiredAsterisk: {
type: Boolean,
default: false,
},
//是否显示校验错误信息
showMessage: {
type: Boolean,
default: true,
},
//是否以行内形式展示校验信息
inlineMessage: {
type: Boolean,
default: false,
},
//是否在输入框中显示校验结果反馈图标
statusIcon: {
type: Boolean,
default: false,
},
//是否在 rules 属性改变后立即触发一次验证
validateOnRuleChange: {
type: Boolean,
default: true,
},
//大小
size: {
type: String,
default: 'default',
},
//是否禁用该表单内的所有组件。 若设置为 true,则表单内组件上的 disabled 属性不再生效
disabled: {
type: Boolean,
default: false,
},
/* =============================== 自定义开始 ================================= */
// 表单配置规则
schemas: {
type: [Array] as PropType<FormSchema[]>,
default: () => [],
},
//布局方式
layout: {
type: String,
default: 'vertical',
},
//组件是否width 100%
isFull: {
type: Boolean,
default: true,
},
//是否显示操作按钮(查询/重置)
showActionButtonGroup: propTypes.bool.def(true),
// 显示重置按钮
showResetButton: propTypes.bool.def(true),
//重置按钮配置
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
// 显示确认按钮
showSubmitButton: propTypes.bool.def(true),
// 确认按钮配置
submitButtonOptions: Object as PropType<Partial<ButtonProps>>,
//展开收起按钮
showAdvancedButton: propTypes.bool.def(true),
// 确认按钮文字
submitButtonText: {
type: String,
default: '查询',
},
//重置按钮文字
resetButtonText: {
type: String,
default: '重置',
},
//row 配置
rowProps: Object as PropType<ColProps>,
//col配置
colProps: Object as PropType<ColProps>,
//grid 样式
baseGridStyle: {
type: Object as PropType<CSSProperties>,
},
//是否折叠
collapsed: {
type: Boolean,
default: true,
},
//默认展示的行数
collapsedRows: {
type: Number,
default: 1,
},
};
+85
View File
@@ -0,0 +1,85 @@
import { ComponentType } from './index';
import type { CSSProperties } from 'vue';
// import type { ElCol, ElButton } from 'element-plus';
import type { ColProps } from 'element-plus/es/components/col/src/col';
import type { ButtonProps } from 'element-plus/es/components/button/src/button';
export interface RenderReturnParams {
schema: FormSchema;
values: Recordable;
model: Recordable;
field: string;
}
export interface componentProps {
options?: any[];
placeholder?: string;
showButton?: boolean;
onInput?: () => Recordable;
onChange?: () => Recordable;
}
export interface FormSchema {
field: string;
label?: string;
type?:string;
labelMessage?: string;
labelMessageStyle?: CSSProperties;
defaultValue?: any;
component?: ComponentType;
componentProps?: any;
slot?: string;
rules?: object | object[];
rowProps?: ColProps;
colProps?: ColProps;
isFull?: boolean;
suffix?: string;
showFeedback?: boolean;
showLabel?: boolean;
requireMarkPlacement?: string;
hidden?: boolean | ((renderCallbackParams: RenderReturnParams) => boolean);
}
export interface FormProps {
model?: Recordable;
labelWidth?: number | string;
schemas?: FormSchema[];
inline: boolean;
layout?: string;
size: string;
labelPlacement: string;
isFull: boolean;
showActionButtonGroup?: boolean;
showResetButton?: boolean;
resetButtonOptions?: Partial<ButtonProps>;
showSubmitButton?: boolean;
showAdvancedButton?: boolean;
submitButtonOptions?: Partial<ButtonProps>;
submitButtonText?: string;
resetButtonText?: string;
rowProps?: ColProps;
colProps?: ColProps;
resetFunc?: () => Promise<void>;
submitFunc?: () => Promise<void>;
submitOnReset?: boolean;
baseGridStyle?: CSSProperties;
draggable?: boolean;
collapsed?: boolean;
collapsedRows?: number;
}
export interface FormActionType {
submit: () => Promise<any>;
setProps: (formProps: Partial<FormProps>) => Promise<void>;
setSchema: (schemaProps: Partial<FormSchema[]>) => Promise<void>;
setFieldsValue: (values: Recordable) => void;
clearValidate: (name?: string | string[]) => Promise<void>;
getFieldsValue: () => Recordable;
resetFields: () => Promise<void>;
validate: (nameList?: any[]) => Promise<any>;
setLoadingSub: (status: boolean) => void;
}
export type RegisterFn = (formInstance: FormActionType) => void;
export type UseFormReturnType = [RegisterFn, FormActionType];
+18
View File
@@ -0,0 +1,18 @@
export type ComponentType =
| 'Input'
| 'InputNumber'
| 'Select'
| 'Radio'
| 'RadioGroup'
| 'Checkbox'
| 'ColorPicker'
| 'AutoComplete'
| 'Cascader'
| 'DatePicker'
| 'TimePicker'
| 'TimeSelect'
| 'Switch'
| 'Upload'
| 'Slider'
| 'Rate'
| 'BasicSelect';