diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index eb7b797f..d85b3c51 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -1,17 +1,17 @@ -import BasicForm from './src/BasicForm.vue' +import BasicForm from './src/BasicForm.vue'; -export * from './src/types/form' -export * from './src/types/formItem' +export * from './src/types/form'; +export * from './src/types/formItem'; -export { useComponentRegister } from './src/hooks/useComponentRegister' -export { useForm } from './src/hooks/useForm' +export { useComponentRegister } from './src/hooks/useComponentRegister'; +export { useForm } from './src/hooks/useForm'; -export { default as ApiSelect } from './src/components/ApiSelect.vue' -export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue' -export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue' -export { default as ApiTree } from './src/components/ApiTree.vue' -export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue' -export { default as ApiCascader } from './src/components/ApiCascader.vue' -export { default as ApiTransfer } from './src/components/ApiTransfer.vue' +export { default as ApiSelect } from './src/components/ApiSelect.vue'; +export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue'; +export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue'; +export { default as ApiTree } from './src/components/ApiTree.vue'; +export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue'; +export { default as ApiCascader } from './src/components/ApiCascader.vue'; +export { default as ApiTransfer } from './src/components/ApiTransfer.vue'; -export { BasicForm } +export { BasicForm }; diff --git a/src/components/Form/src/BasicForm.vue b/src/components/Form/src/BasicForm.vue index 05acef7a..d5bc4768 100644 --- a/src/components/Form/src/BasicForm.vue +++ b/src/components/Form/src/BasicForm.vue @@ -10,6 +10,7 @@ diff --git a/src/components/Form/src/components/ApiSelect.vue b/src/components/Form/src/components/ApiSelect.vue index 66718639..ea5b1a19 100644 --- a/src/components/Form/src/components/ApiSelect.vue +++ b/src/components/Form/src/components/ApiSelect.vue @@ -59,7 +59,7 @@ immediate: propTypes.bool.def(true), alwaysLoad: propTypes.bool.def(false), }, - emits: ['options-change', 'change'], + emits: ['options-change', 'change', 'update:value'], setup(props, { emit }) { const options = ref([]) const loading = ref(false) @@ -91,6 +91,13 @@ props.immediate && !props.alwaysLoad && fetch() }) + watch( + () => state.value, + (v) => { + emit('update:value', v) + }, + ) + watch( () => props.params, () => { diff --git a/src/components/Form/src/components/ApiTransfer.vue b/src/components/Form/src/components/ApiTransfer.vue index 2cf1f46c..16dfee09 100644 --- a/src/components/Form/src/components/ApiTransfer.vue +++ b/src/components/Form/src/components/ApiTransfer.vue @@ -12,13 +12,13 @@ diff --git a/src/components/Form/src/components/ApiTree.vue b/src/components/Form/src/components/ApiTree.vue index ea80c2d1..0ec6917d 100644 --- a/src/components/Form/src/components/ApiTree.vue +++ b/src/components/Form/src/components/ApiTree.vue @@ -10,12 +10,12 @@ diff --git a/src/components/Form/src/components/FormItem.vue b/src/components/Form/src/components/FormItem.vue index 2aa3b4ec..75de163e 100644 --- a/src/components/Form/src/components/FormItem.vue +++ b/src/components/Form/src/components/FormItem.vue @@ -44,6 +44,9 @@ formActionType: { type: Object as PropType, }, + isAdvanced: { + type: Boolean, + }, }, setup(props, { slots }) { const { t } = useI18n() @@ -103,8 +106,8 @@ const { show, ifShow } = props.schema const { showAdvancedButton } = props.formProps const itemIsAdvanced = showAdvancedButton - ? isBoolean(props.schema.isAdvanced) - ? props.schema.isAdvanced + ? isBoolean(props.isAdvanced) + ? props.isAdvanced : true : true @@ -136,7 +139,6 @@ dynamicRules, required, } = props.schema - if (isFunction(dynamicRules)) { return dynamicRules(unref(getValues)) as ValidationRule[] } @@ -282,7 +284,6 @@ ...on, ...bindValue, } - if (!renderComponentContent) { return } @@ -291,6 +292,7 @@ : { default: () => renderComponentContent, } + return {compSlot} } diff --git a/src/components/Form/src/helper.ts b/src/components/Form/src/helper.ts index f357da58..d0727f38 100644 --- a/src/components/Form/src/helper.ts +++ b/src/components/Form/src/helper.ts @@ -1,20 +1,20 @@ -import type { ValidationRule } from 'ant-design-vue/lib/form/Form' -import type { ComponentType } from './types/index' -import { useI18n } from '/@/hooks/web/useI18n' -import { dateUtil } from '/@/utils/dateUtil' -import { isNumber, isObject } from '/@/utils/is' +import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; +import type { ComponentType } from './types/index'; +import { useI18n } from '/@/hooks/web/useI18n'; +import { dateUtil } from '/@/utils/dateUtil'; +import { isNumber, isObject } from '/@/utils/is'; -const { t } = useI18n() +const { t } = useI18n(); /** * @description: 生成placeholder */ export function createPlaceholderMessage(component: ComponentType) { if (component.includes('Input') || component.includes('Complete')) { - return t('common.inputText') + return t('common.inputText'); } if (component.includes('Picker')) { - return t('common.chooseText') + return t('common.chooseText'); } if ( component.includes('Select') || @@ -24,15 +24,15 @@ export function createPlaceholderMessage(component: ComponentType) { component.includes('Switch') ) { // return `请选择${label}`; - return t('common.chooseText') + return t('common.chooseText'); } - return '' + return ''; } -const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'] +const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker']; function genType() { - return [...DATE_TYPE, 'RangePicker'] + return [...DATE_TYPE, 'RangePicker']; } export function setComponentRuleType( @@ -41,34 +41,34 @@ export function setComponentRuleType( valueFormat: string, ) { if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) { - rule.type = valueFormat ? 'string' : 'object' + rule.type = valueFormat ? 'string' : 'object'; } else if (['RangePicker', 'Upload', 'CheckboxGroup', 'TimePicker'].includes(component)) { - rule.type = 'array' + rule.type = 'array'; } else if (['InputNumber'].includes(component)) { - rule.type = 'number' + rule.type = 'number'; } } export function processDateValue(attr: Recordable, component: string) { - const { valueFormat, value } = attr + const { valueFormat, value } = attr; if (valueFormat) { - attr.value = isObject(value) ? dateUtil(value).format(valueFormat) : value + attr.value = isObject(value) ? dateUtil(value).format(valueFormat) : value; } else if (DATE_TYPE.includes(component) && value) { - attr.value = dateUtil(attr.value) + attr.value = dateUtil(attr.value); } } export function handleInputNumberValue(component?: ComponentType, val?: any) { - if (!component) return val + if (!component) return val; if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) { - return val && isNumber(val) ? `${val}` : val + return val && isNumber(val) ? `${val}` : val; } - return val + return val; } /** * 时间字段 */ -export const dateItemType = genType() +export const dateItemType = genType(); -export const defaultValueComponents = ['Input', 'InputPassword', 'InputSearch', 'InputTextArea'] +export const defaultValueComponents = ['Input', 'InputPassword', 'InputSearch', 'InputTextArea']; diff --git a/src/components/Form/src/hooks/useAdvanced.ts b/src/components/Form/src/hooks/useAdvanced.ts index ca532f5c..9bd2d9de 100644 --- a/src/components/Form/src/hooks/useAdvanced.ts +++ b/src/components/Form/src/hooks/useAdvanced.ts @@ -1,21 +1,21 @@ -import type { ColEx } from '../types' -import type { AdvanceState } from '../types/hooks' -import { ComputedRef, getCurrentInstance, Ref } from 'vue' -import type { FormProps, FormSchema } from '../types/form' -import { computed, unref, watch } from 'vue' -import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is' -import { useBreakpoint } from '/@/hooks/event/useBreakpoint' -import { useDebounceFn } from '@vueuse/core' +import type { ColEx } from '../types'; +import type { AdvanceState } from '../types/hooks'; +import { ComputedRef, getCurrentInstance, Ref, shallowReactive } from 'vue'; +import type { FormProps, FormSchema } from '../types/form'; +import { computed, unref, watch } from 'vue'; +import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is'; +import { useBreakpoint } from '/@/hooks/event/useBreakpoint'; +import { useDebounceFn } from '@vueuse/core'; -const BASIC_COL_LEN = 24 +const BASIC_COL_LEN = 24; interface UseAdvancedContext { - advanceState: AdvanceState - emit: EmitType - getProps: ComputedRef - getSchema: ComputedRef - formModel: Recordable - defaultValueRef: Ref + advanceState: AdvanceState; + emit: EmitType; + getProps: ComputedRef; + getSchema: ComputedRef; + formModel: Recordable; + defaultValueRef: Ref; } export default function ({ @@ -26,104 +26,106 @@ export default function ({ formModel, defaultValueRef, }: UseAdvancedContext) { - const vm = getCurrentInstance() + const vm = getCurrentInstance(); - const { realWidthRef, screenEnum, screenRef } = useBreakpoint() + const { realWidthRef, screenEnum, screenRef } = useBreakpoint(); const getEmptySpan = computed((): number => { if (!advanceState.isAdvanced) { - return 0 + return 0; } // For some special cases, you need to manually specify additional blank lines - const emptySpan = unref(getProps).emptySpan || 0 + const emptySpan = unref(getProps).emptySpan || 0; if (isNumber(emptySpan)) { - return emptySpan + return emptySpan; } if (isObject(emptySpan)) { - const { span = 0 } = emptySpan - const screen = unref(screenRef) as string + const { span = 0 } = emptySpan; + const screen = unref(screenRef) as string; - const screenSpan = (emptySpan as any)[screen.toLowerCase()] - return screenSpan || span || 0 + const screenSpan = (emptySpan as any)[screen.toLowerCase()]; + return screenSpan || span || 0; } - return 0 - }) + return 0; + }); - const debounceUpdateAdvanced = useDebounceFn(updateAdvanced, 30) + const debounceUpdateAdvanced = useDebounceFn(updateAdvanced, 30); watch( [() => unref(getSchema), () => advanceState.isAdvanced, () => unref(realWidthRef)], () => { - const { showAdvancedButton } = unref(getProps) + const { showAdvancedButton } = unref(getProps); if (showAdvancedButton) { - debounceUpdateAdvanced() + debounceUpdateAdvanced(); } }, { immediate: true }, - ) + ); function getAdvanced(itemCol: Partial, itemColSum = 0, isLastAction = false) { - const width = unref(realWidthRef) + const width = unref(realWidthRef); const mdWidth = parseInt(itemCol.md as string) || parseInt(itemCol.xs as string) || parseInt(itemCol.sm as string) || (itemCol.span as number) || - BASIC_COL_LEN + BASIC_COL_LEN; - const lgWidth = parseInt(itemCol.lg as string) || mdWidth - const xlWidth = parseInt(itemCol.xl as string) || lgWidth - const xxlWidth = parseInt(itemCol.xxl as string) || xlWidth + const lgWidth = parseInt(itemCol.lg as string) || mdWidth; + const xlWidth = parseInt(itemCol.xl as string) || lgWidth; + const xxlWidth = parseInt(itemCol.xxl as string) || xlWidth; if (width <= screenEnum.LG) { - itemColSum += mdWidth + itemColSum += mdWidth; } else if (width < screenEnum.XL) { - itemColSum += lgWidth + itemColSum += lgWidth; } else if (width < screenEnum.XXL) { - itemColSum += xlWidth + itemColSum += xlWidth; } else { - itemColSum += xxlWidth + itemColSum += xxlWidth; } if (isLastAction) { - advanceState.hideAdvanceBtn = false + advanceState.hideAdvanceBtn = false; if (itemColSum <= BASIC_COL_LEN * 2) { // When less than or equal to 2 lines, the collapse and expand buttons are not displayed - advanceState.hideAdvanceBtn = true - advanceState.isAdvanced = true + advanceState.hideAdvanceBtn = true; + advanceState.isAdvanced = true; } else if ( itemColSum > BASIC_COL_LEN * 2 && itemColSum <= BASIC_COL_LEN * (unref(getProps).autoAdvancedLine || 3) ) { - advanceState.hideAdvanceBtn = false + advanceState.hideAdvanceBtn = false; // More than 3 lines collapsed by default } else if (!advanceState.isLoad) { - advanceState.isLoad = true - advanceState.isAdvanced = !advanceState.isAdvanced + advanceState.isLoad = true; + advanceState.isAdvanced = !advanceState.isAdvanced; } - return { isAdvanced: advanceState.isAdvanced, itemColSum } + return { isAdvanced: advanceState.isAdvanced, itemColSum }; } if (itemColSum > BASIC_COL_LEN * (unref(getProps).alwaysShowLines || 1)) { - return { isAdvanced: advanceState.isAdvanced, itemColSum } + return { isAdvanced: advanceState.isAdvanced, itemColSum }; } else { // The first line is always displayed - return { isAdvanced: true, itemColSum } + return { isAdvanced: true, itemColSum }; } } + const fieldsIsAdvancedMap = shallowReactive({}); + function updateAdvanced() { - let itemColSum = 0 - let realItemColSum = 0 - const { baseColProps = {} } = unref(getProps) + let itemColSum = 0; + let realItemColSum = 0; + const { baseColProps = {} } = unref(getProps); for (const schema of unref(getSchema)) { - const { show, colProps } = schema - let isShow = true + const { show, colProps } = schema; + let isShow = true; if (isBoolean(show)) { - isShow = show + isShow = show; } if (isFunction(show)) { @@ -135,36 +137,36 @@ export default function ({ ...unref(defaultValueRef), ...formModel, }, - }) + }); } if (isShow && (colProps || baseColProps)) { const { itemColSum: sum, isAdvanced } = getAdvanced( { ...baseColProps, ...colProps }, itemColSum, - ) + ); - itemColSum = sum || 0 + itemColSum = sum || 0; if (isAdvanced) { - realItemColSum = itemColSum + realItemColSum = itemColSum; } - schema.isAdvanced = isAdvanced + fieldsIsAdvancedMap[schema.field] = isAdvanced; } } // 确保页面发送更新 - vm?.proxy?.$forceUpdate() + vm?.proxy?.$forceUpdate(); - advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan) + advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan); - getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true) + getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true); - emit('advanced-change') + emit('advanced-change'); } function handleToggleAdvanced() { - advanceState.isAdvanced = !advanceState.isAdvanced + advanceState.isAdvanced = !advanceState.isAdvanced; } - return { handleToggleAdvanced } + return { handleToggleAdvanced, fieldsIsAdvancedMap }; } diff --git a/src/components/Form/src/hooks/useForm.ts b/src/components/Form/src/hooks/useForm.ts index ed76e134..ab80409a 100644 --- a/src/components/Form/src/hooks/useForm.ts +++ b/src/components/Form/src/hooks/useForm.ts @@ -1,96 +1,96 @@ -import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form' -import type { NamePath } from 'ant-design-vue/lib/form/interface' -import type { DynamicProps } from '/#/utils' -import { ref, onUnmounted, unref, nextTick, watch } from 'vue' -import { isProdMode } from '/@/utils/env' -import { error } from '/@/utils/log' -import { getDynamicProps } from '/@/utils' +import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form'; +import type { NamePath } from 'ant-design-vue/lib/form/interface'; +import type { DynamicProps } from '/#/utils'; +import { ref, onUnmounted, unref, nextTick, watch } from 'vue'; +import { isProdMode } from '/@/utils/env'; +import { error } from '/@/utils/log'; +import { getDynamicProps } from '/@/utils'; -export declare type ValidateFields = (nameList?: NamePath[]) => Promise +export declare type ValidateFields = (nameList?: NamePath[]) => Promise; -type Props = Partial> +type Props = Partial>; export function useForm(props?: Props): UseFormReturnType { - const formRef = ref>(null) - const loadedRef = ref>(false) + const formRef = ref>(null); + const loadedRef = ref>(false); async function getForm() { - const form = unref(formRef) + const form = unref(formRef); if (!form) { 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 + 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 = null; + loadedRef.value = null; + }); + if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return; - formRef.value = instance - loadedRef.value = true + formRef.value = instance; + loadedRef.value = true; watch( () => props, () => { - props && instance.setProps(getDynamicProps(props)) + props && instance.setProps(getDynamicProps(props)); }, { immediate: true, deep: true, }, - ) + ); } const methods: FormActionType = { scrollToField: async (name: NamePath, options?: ScrollOptions | undefined) => { - const form = await getForm() - form.scrollToField(name, options) + const form = await getForm(); + form.scrollToField(name, options); }, setProps: async (formProps: Partial) => { - const form = await getForm() - form.setProps(formProps) + const form = await getForm(); + form.setProps(formProps); }, updateSchema: async (data: Partial | Partial[]) => { - const form = await getForm() - form.updateSchema(data) + const form = await getForm(); + form.updateSchema(data); }, resetSchema: async (data: Partial | Partial[]) => { - const form = await getForm() - form.resetSchema(data) + const form = await getForm(); + form.resetSchema(data); }, clearValidate: async (name?: string | string[]) => { - const form = await getForm() - form.clearValidate(name) + const form = await getForm(); + form.clearValidate(name); }, resetFields: async () => { getForm().then(async (form) => { - await form.resetFields() - }) + await form.resetFields(); + }); }, removeSchemaByField: async (field: string | string[]) => { - unref(formRef)?.removeSchemaByField(field) + unref(formRef)?.removeSchemaByField(field); }, // TODO promisify getFieldsValue: () => { - return unref(formRef)?.getFieldsValue() as T + return unref(formRef)?.getFieldsValue() as T; }, setFieldsValue: async (values: T) => { - const form = await getForm() - form.setFieldsValue(values) + const form = await getForm(); + form.setFieldsValue(values); }, appendSchemaByField: async ( @@ -98,25 +98,25 @@ export function useForm(props?: Props): UseFormReturnType { prefixField: string | undefined, first: boolean, ) => { - const form = await getForm() - form.appendSchemaByField(schema, prefixField, first) + const form = await getForm(); + form.appendSchemaByField(schema, prefixField, first); }, submit: async (): Promise => { - const form = await getForm() - return form.submit() + const form = await getForm(); + return form.submit(); }, validate: async (nameList?: NamePath[]): Promise => { - const form = await getForm() - return form.validate(nameList) + const form = await getForm(); + return form.validate(nameList); }, validateFields: async (nameList?: NamePath[]): Promise => { - const form = await getForm() - return form.validateFields(nameList) + const form = await getForm(); + return form.validateFields(nameList); }, - } + }; - return [register, methods] + return [register, methods]; } diff --git a/src/components/Form/src/hooks/useFormEvents.ts b/src/components/Form/src/hooks/useFormEvents.ts index 6bc29696..09e55e2a 100644 --- a/src/components/Form/src/hooks/useFormEvents.ts +++ b/src/components/Form/src/hooks/useFormEvents.ts @@ -1,23 +1,23 @@ -import type { ComputedRef, Ref } from 'vue' -import type { FormProps, FormSchema, FormActionType } from '../types/form' -import type { NamePath } from 'ant-design-vue/lib/form/interface' -import { unref, toRaw, nextTick } from 'vue' -import { isArray, isFunction, isObject, isString, isDef, isNullOrUnDef } from '/@/utils/is' -import { deepMerge } from '/@/utils' -import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper' -import { dateUtil } from '/@/utils/dateUtil' -import { cloneDeep, uniqBy } from 'lodash-es' -import { error } from '/@/utils/log' +import type { ComputedRef, Ref } from 'vue'; +import type { FormProps, FormSchema, FormActionType } from '../types/form'; +import type { NamePath } from 'ant-design-vue/lib/form/interface'; +import { unref, toRaw, nextTick } from 'vue'; +import { isArray, isFunction, isObject, isString, isDef, isNullOrUnDef } from '/@/utils/is'; +import { deepMerge } from '/@/utils'; +import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper'; +import { dateUtil } from '/@/utils/dateUtil'; +import { cloneDeep, uniqBy } from 'lodash-es'; +import { error } from '/@/utils/log'; interface UseFormActionContext { - emit: EmitType - getProps: ComputedRef - getSchema: ComputedRef - formModel: Recordable - defaultValueRef: Ref - formElRef: Ref - schemaRef: Ref - handleFormValues: Fn + emit: EmitType; + getProps: ComputedRef; + getSchema: ComputedRef; + formModel: Recordable; + defaultValueRef: Ref; + formElRef: Ref; + schemaRef: Ref; + handleFormValues: Fn; } export function useFormEvents({ emit, @@ -30,22 +30,22 @@ export function useFormEvents({ handleFormValues, }: UseFormActionContext) { async function resetFields(): Promise { - const { resetFunc, submitOnReset } = unref(getProps) - resetFunc && isFunction(resetFunc) && (await resetFunc()) + const { resetFunc, submitOnReset } = unref(getProps); + resetFunc && isFunction(resetFunc) && (await resetFunc()); - const formEl = unref(formElRef) - if (!formEl) return + const formEl = unref(formElRef); + if (!formEl) return; Object.keys(formModel).forEach((key) => { - const schema = unref(getSchema).find((item) => item.field === key) - const isInput = schema?.component && defaultValueComponents.includes(schema.component) - const defaultValue = cloneDeep(defaultValueRef.value[key]) - formModel[key] = isInput ? defaultValue || '' : defaultValue - }) - nextTick(() => clearValidate()) + const schema = unref(getSchema).find((item) => item.field === key); + const isInput = schema?.component && defaultValueComponents.includes(schema.component); + const defaultValue = cloneDeep(defaultValueRef.value[key]); + formModel[key] = isInput ? defaultValue || '' : defaultValue; + }); + nextTick(() => clearValidate()); - emit('reset', toRaw(formModel)) - submitOnReset && handleSubmit() + emit('reset', toRaw(formModel)); + submitOnReset && handleSubmit(); } /** @@ -54,78 +54,78 @@ export function useFormEvents({ async function setFieldsValue(values: Recordable): Promise { const fields = unref(getSchema) .map((item) => item.field) - .filter(Boolean) + .filter(Boolean); // key 支持 a.b.c 的嵌套写法 - const delimiter = '.' - const nestKeyArray = fields.filter((item) => item.indexOf(delimiter) >= 0) + const delimiter = '.'; + const nestKeyArray = fields.filter((item) => item.indexOf(delimiter) >= 0); - const validKeys: string[] = [] + const validKeys: string[] = []; Object.keys(values).forEach((key) => { - const schema = unref(getSchema).find((item) => item.field === key) - let value = values[key] + const schema = unref(getSchema).find((item) => item.field === key); + let value = values[key]; - const hasKey = Reflect.has(values, key) + const hasKey = Reflect.has(values, key); - value = handleInputNumberValue(schema?.component, value) + value = handleInputNumberValue(schema?.component, value); // 0| '' is allow if (hasKey && fields.includes(key)) { // time type if (itemIsDateType(key)) { if (Array.isArray(value)) { - const arr: any[] = [] + const arr: any[] = []; for (const ele of value) { - arr.push(ele ? dateUtil(ele) : null) + arr.push(ele ? dateUtil(ele) : null); } - formModel[key] = arr + formModel[key] = arr; } else { - const { componentProps } = schema || {} - let _props = componentProps as any + const { componentProps } = schema || {}; + let _props = componentProps as any; if (typeof componentProps === 'function') { - _props = _props({ formModel }) + _props = _props({ formModel }); } - formModel[key] = value ? (_props?.valueFormat ? value : dateUtil(value)) : null + formModel[key] = value ? (_props?.valueFormat ? value : dateUtil(value)) : null; } } else { - formModel[key] = value + formModel[key] = value; } - validKeys.push(key) + validKeys.push(key); } else { nestKeyArray.forEach((nestKey: string) => { try { - const value = eval('values' + delimiter + nestKey) + const value = eval('values' + delimiter + nestKey); if (isDef(value)) { - formModel[nestKey] = value - validKeys.push(nestKey) + formModel[nestKey] = value; + validKeys.push(nestKey); } } catch (e) { // key not exist if (isDef(defaultValueRef.value[nestKey])) { - formModel[nestKey] = cloneDeep(defaultValueRef.value[nestKey]) + formModel[nestKey] = cloneDeep(defaultValueRef.value[nestKey]); } } - }) + }); } - }) - validateFields(validKeys).catch((_) => {}) + }); + validateFields(validKeys).catch((_) => {}); } /** * @description: Delete based on field name */ async function removeSchemaByField(fields: string | string[]): Promise { - const schemaList: FormSchema[] = cloneDeep(unref(getSchema)) + const schemaList: FormSchema[] = cloneDeep(unref(getSchema)); if (!fields) { - return + return; } - let fieldList: string[] = isString(fields) ? [fields] : fields + let fieldList: string[] = isString(fields) ? [fields] : fields; if (isString(fields)) { - fieldList = [fields] + fieldList = [fields]; } for (const field of fieldList) { - _removeSchemaByFeild(field, schemaList) + _removeSchemaByFeild(field, schemaList); } - schemaRef.value = schemaList + schemaRef.value = schemaList; } /** @@ -133,10 +133,10 @@ export function useFormEvents({ */ function _removeSchemaByFeild(field: string, schemaList: FormSchema[]): void { if (isString(field)) { - const index = schemaList.findIndex((schema) => schema.field === field) + const index = schemaList.findIndex((schema) => schema.field === field); if (index !== -1) { - delete formModel[field] - schemaList.splice(index, 1) + delete formModel[field]; + schemaList.splice(index, 1); } } } @@ -145,92 +145,92 @@ export function useFormEvents({ * @description: Insert after a certain field, if not insert the last */ async function appendSchemaByField(schema: FormSchema, prefixField?: string, first = false) { - const schemaList: FormSchema[] = cloneDeep(unref(getSchema)) + const schemaList: FormSchema[] = cloneDeep(unref(getSchema)); - const index = schemaList.findIndex((schema) => schema.field === prefixField) + const index = schemaList.findIndex((schema) => schema.field === prefixField); if (!prefixField || index === -1 || first) { - first ? schemaList.unshift(schema) : schemaList.push(schema) - schemaRef.value = schemaList - _setDefaultValue(schema) - return + first ? schemaList.unshift(schema) : schemaList.push(schema); + schemaRef.value = schemaList; + _setDefaultValue(schema); + return; } if (index !== -1) { - schemaList.splice(index + 1, 0, schema) + schemaList.splice(index + 1, 0, schema); } - _setDefaultValue(schema) + _setDefaultValue(schema); - schemaRef.value = schemaList + schemaRef.value = schemaList; } async function resetSchema(data: Partial | Partial[]) { - let updateData: Partial[] = [] + let updateData: Partial[] = []; if (isObject(data)) { - updateData.push(data as FormSchema) + updateData.push(data as FormSchema); } if (isArray(data)) { - updateData = [...data] + updateData = [...data]; } const hasField = updateData.every( (item) => item.component === 'Divider' || (Reflect.has(item, 'field') && item.field), - ) + ); if (!hasField) { error( 'All children of the form Schema array that need to be updated must contain the `field` field', - ) - return + ); + return; } - schemaRef.value = updateData as FormSchema[] + schemaRef.value = updateData as FormSchema[]; } async function updateSchema(data: Partial | Partial[]) { - let updateData: Partial[] = [] + let updateData: Partial[] = []; if (isObject(data)) { - updateData.push(data as FormSchema) + updateData.push(data as FormSchema); } if (isArray(data)) { - updateData = [...data] + updateData = [...data]; } const hasField = updateData.every( (item) => item.component === 'Divider' || (Reflect.has(item, 'field') && item.field), - ) + ); if (!hasField) { error( 'All children of the form Schema array that need to be updated must contain the `field` field', - ) - return + ); + return; } - const schema: FormSchema[] = [] + const schema: FormSchema[] = []; updateData.forEach((item) => { unref(getSchema).forEach((val) => { if (val.field === item.field) { - const newSchema = deepMerge(val, item) - schema.push(newSchema as FormSchema) + const newSchema = deepMerge(val, item); + schema.push(newSchema as FormSchema); } else { - schema.push(val) + schema.push(val); } - }) - }) - _setDefaultValue(schema) + }); + }); + _setDefaultValue(schema); - schemaRef.value = uniqBy(schema, 'field') + schemaRef.value = uniqBy(schema, 'field'); } function _setDefaultValue(data: FormSchema | FormSchema[]) { - let schemas: FormSchema[] = [] + let schemas: FormSchema[] = []; if (isObject(data)) { - schemas.push(data as FormSchema) + schemas.push(data as FormSchema); } if (isArray(data)) { - schemas = [...data] + schemas = [...data]; } - const obj: Recordable = {} - const currentFieldsValue = getFieldsValue() + const obj: Recordable = {}; + const currentFieldsValue = getFieldsValue(); schemas.forEach((item) => { if ( item.component != 'Divider' && @@ -239,16 +239,16 @@ export function useFormEvents({ !isNullOrUnDef(item.defaultValue) && !(item.field in currentFieldsValue) ) { - obj[item.field] = item.defaultValue + obj[item.field] = item.defaultValue; } - }) - setFieldsValue(obj) + }); + setFieldsValue(obj); } function getFieldsValue(): Recordable { - const formEl = unref(formElRef) - if (!formEl) return {} - return handleFormValues(toRaw(unref(formModel))) + const formEl = unref(formElRef); + if (!formEl) return {}; + return handleFormValues(toRaw(unref(formModel))); } /** @@ -256,44 +256,44 @@ export function useFormEvents({ */ function itemIsDateType(key: string) { return unref(getSchema).some((item) => { - return item.field === key ? dateItemType.includes(item.component) : false - }) + return item.field === key ? dateItemType.includes(item.component) : false; + }); } async function validateFields(nameList?: NamePath[] | undefined) { - return unref(formElRef)?.validateFields(nameList) + return unref(formElRef)?.validateFields(nameList); } async function validate(nameList?: NamePath[] | undefined) { - return await unref(formElRef)?.validate(nameList) + return await unref(formElRef)?.validate(nameList); } async function clearValidate(name?: string | string[]) { - await unref(formElRef)?.clearValidate(name) + await unref(formElRef)?.clearValidate(name); } async function scrollToField(name: NamePath, options?: ScrollOptions | undefined) { - await unref(formElRef)?.scrollToField(name, options) + await unref(formElRef)?.scrollToField(name, options); } /** * @description: Form submission */ async function handleSubmit(e?: Event): Promise { - e && e.preventDefault() - const { submitFunc } = unref(getProps) + e && e.preventDefault(); + const { submitFunc } = unref(getProps); if (submitFunc && isFunction(submitFunc)) { - await submitFunc() - return + await submitFunc(); + return; } - const formEl = unref(formElRef) - if (!formEl) return + const formEl = unref(formElRef); + if (!formEl) return; try { - const values = await validate() - const res = handleFormValues(values) - emit('submit', res) + const values = await validate(); + const res = handleFormValues(values); + emit('submit', res); } catch (error: any) { - throw new Error(error) + throw new Error(error); } } @@ -310,5 +310,5 @@ export function useFormEvents({ resetFields, setFieldsValue, scrollToField, - } + }; } diff --git a/src/components/Form/src/hooks/useFormValues.ts b/src/components/Form/src/hooks/useFormValues.ts index 5c917686..a4865993 100644 --- a/src/components/Form/src/hooks/useFormValues.ts +++ b/src/components/Form/src/hooks/useFormValues.ts @@ -1,31 +1,31 @@ -import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is' -import { dateUtil } from '/@/utils/dateUtil' -import { unref } from 'vue' -import type { Ref, ComputedRef } from 'vue' -import type { FormProps, FormSchema } from '../types/form' -import { cloneDeep, set } from 'lodash-es' +import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is'; +import { dateUtil } from '/@/utils/dateUtil'; +import { unref } from 'vue'; +import type { Ref, ComputedRef } from 'vue'; +import type { FormProps, FormSchema } from '../types/form'; +import { cloneDeep, set } from 'lodash-es'; interface UseFormValuesContext { - defaultValueRef: Ref - getSchema: ComputedRef - getProps: ComputedRef - formModel: Recordable + defaultValueRef: Ref; + getSchema: ComputedRef; + getProps: ComputedRef; + formModel: Recordable; } /** * @desription deconstruct array-link key. This method will mutate the target. */ function tryDeconstructArray(key: string, value: any, target: Recordable) { - const pattern = /^\[(.+)\]$/ + const pattern = /^\[(.+)\]$/; if (pattern.test(key)) { - const match = key.match(pattern) + const match = key.match(pattern); if (match && match[1]) { - const keys = match[1].split(',') - value = Array.isArray(value) ? value : [value] + const keys = match[1].split(','); + value = Array.isArray(value) ? value : [value]; keys.forEach((k, index) => { - set(target, k.trim(), value[index]) - }) - return true + set(target, k.trim(), value[index]); + }); + return true; } } } @@ -34,16 +34,16 @@ function tryDeconstructArray(key: string, value: any, target: Recordable) { * @desription deconstruct object-link key. This method will mutate the target. */ function tryDeconstructObject(key: string, value: any, target: Recordable) { - const pattern = /^\{(.+)\}$/ + const pattern = /^\{(.+)\}$/; if (pattern.test(key)) { - const match = key.match(pattern) + const match = key.match(pattern); if (match && match[1]) { - const keys = match[1].split(',') - value = isObject(value) ? value : {} + const keys = match[1].split(','); + value = isObject(value) ? value : {}; keys.forEach((k) => { - set(target, k.trim(), value[k.trim()]) - }) - return true + set(target, k.trim(), value[k.trim()]); + }); + return true; } } } @@ -57,75 +57,82 @@ export function useFormValues({ // Processing form values function handleFormValues(values: Recordable) { if (!isObject(values)) { - return {} + return {}; } - const res: Recordable = {} + const res: Recordable = {}; for (const item of Object.entries(values)) { - let [, value] = item - const [key] = item + let [, value] = item; + const [key] = item; if (!key || (isArray(value) && value.length === 0) || isFunction(value)) { - continue + continue; } - const transformDateFunc = unref(getProps).transformDateFunc + const transformDateFunc = unref(getProps).transformDateFunc; if (isObject(value)) { - value = transformDateFunc?.(value) + value = transformDateFunc?.(value); } if (isArray(value) && value[0]?.format && value[1]?.format) { - value = value.map((item) => transformDateFunc?.(item)) + value = value.map((item) => transformDateFunc?.(item)); } // Remove spaces if (isString(value)) { - value = value.trim() + value = value.trim(); } if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) { // 没有解构成功的,按原样赋值 - set(res, key, value) + set(res, key, value); } } - return handleRangeTimeValue(res) + return handleRangeTimeValue(res); } /** * @description: Processing time interval parameters */ function handleRangeTimeValue(values: Recordable) { - const fieldMapToTime = unref(getProps).fieldMapToTime + const fieldMapToTime = unref(getProps).fieldMapToTime; if (!fieldMapToTime || !Array.isArray(fieldMapToTime)) { - return values + return values; } for (const [field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD'] of fieldMapToTime) { - if (!field || !startTimeKey || !endTimeKey || !values[field]) { - continue + if (!field || !startTimeKey || !endTimeKey) { + continue; + } + // If the value to be converted is empty, remove the field + if (!values[field]) { + Reflect.deleteProperty(values, field); + continue; } - const [startTime, endTime]: string[] = values[field] + const [startTime, endTime]: string[] = values[field]; - values[startTimeKey] = dateUtil(startTime).format(format) - values[endTimeKey] = dateUtil(endTime).format(format) - Reflect.deleteProperty(values, field) + const [startTimeFormat, endTimeFormat] = Array.isArray(format) ? format : [format, format]; + + values[startTimeKey] = dateUtil(startTime).format(startTimeFormat); + values[endTimeKey] = dateUtil(endTime).format(endTimeFormat); + Reflect.deleteProperty(values, field); } - return values + return values; } function initDefault() { - const schemas = unref(getSchema) - const obj: Recordable = {} + const schemas = unref(getSchema); + const obj: Recordable = {}; schemas.forEach((item) => { - const { defaultValue } = item + const { defaultValue } = item; if (!isNullOrUnDef(defaultValue)) { - obj[item.field] = defaultValue + obj[item.field] = defaultValue; if (formModel[item.field] === undefined) { - formModel[item.field] = defaultValue + formModel[item.field] = defaultValue; } } - }) - defaultValueRef.value = cloneDeep(obj) + }); + defaultValueRef.value = cloneDeep(obj); } - return { handleFormValues, initDefault } + return { handleFormValues, initDefault }; } diff --git a/src/components/Form/src/hooks/useLabelWidth.ts b/src/components/Form/src/hooks/useLabelWidth.ts index 7e20feec..3befa1c2 100644 --- a/src/components/Form/src/hooks/useLabelWidth.ts +++ b/src/components/Form/src/hooks/useLabelWidth.ts @@ -1,34 +1,34 @@ -import type { Ref } from 'vue' -import { computed, unref } from 'vue' -import type { FormProps, FormSchema } from '../types/form' -import { isNumber } from '/@/utils/is' +import type { Ref } from 'vue'; +import { computed, unref } from 'vue'; +import type { FormProps, FormSchema } from '../types/form'; +import { isNumber } from '/@/utils/is'; export function useItemLabelWidth(schemaItemRef: Ref, propsRef: Ref) { return computed(() => { - const schemaItem = unref(schemaItemRef) - const { labelCol = {}, wrapperCol = {} } = schemaItem.itemProps || {} - const { labelWidth, disabledLabelWidth } = schemaItem + const schemaItem = unref(schemaItemRef); + const { labelCol = {}, wrapperCol = {} } = schemaItem.itemProps || {}; + const { labelWidth, disabledLabelWidth } = schemaItem; const { labelWidth: globalLabelWidth, labelCol: globalLabelCol, wrapperCol: globWrapperCol, layout, - } = unref(propsRef) + } = unref(propsRef); // If labelWidth is set globally, all items setting if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) { labelCol.style = { textAlign: 'left', - } - return { labelCol, wrapperCol } + }; + return { labelCol, wrapperCol }; } - let width = labelWidth || globalLabelWidth - const col = { ...globalLabelCol, ...labelCol } - const wrapCol = { ...globWrapperCol, ...wrapperCol } + let width = labelWidth || globalLabelWidth; + const col = { ...globalLabelCol, ...labelCol }; + const wrapCol = { ...globWrapperCol, ...wrapperCol }; if (width) { - width = isNumber(width) ? `${width}px` : width + width = isNumber(width) ? `${width}px` : width; } return { @@ -37,6 +37,6 @@ export function useItemLabelWidth(schemaItemRef: Ref, propsRef: Ref< style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` }, ...wrapCol, }, - } - }) + }; + }); } diff --git a/src/components/Form/src/props.ts b/src/components/Form/src/props.ts index d40fd1b8..a9eee16b 100644 --- a/src/components/Form/src/props.ts +++ b/src/components/Form/src/props.ts @@ -1,15 +1,15 @@ -import type { FieldMapToTime, FormSchema } from './types/form' -import type { CSSProperties, PropType } from 'vue' -import type { ColEx } from './types' -import type { TableActionType } from '/@/components/Table' -import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes' -import type { RowProps } from 'ant-design-vue/lib/grid/Row' -import { propTypes } from '/@/utils/propTypes' +import type { FieldMapToTime, FormSchema } from './types/form'; +import type { CSSProperties, PropType } from 'vue'; +import type { ColEx } from './types'; +import type { TableActionType } from '/@/components/Table'; +import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes'; +import type { RowProps } from 'ant-design-vue/lib/grid/Row'; +import { propTypes } from '/@/utils/propTypes'; export const basicProps = { model: { type: Object as PropType, - default: {}, + default: () => ({}), }, // 标签宽度 固定宽度 labelWidth: { @@ -54,7 +54,7 @@ export const basicProps = { transformDateFunc: { type: Function as PropType, default: (date: any) => { - return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date + return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date; }, }, rulesMessageJoinLabel: propTypes.bool.def(true), @@ -100,4 +100,4 @@ export const basicProps = { labelAlign: propTypes.string, rowProps: Object as PropType, -} +}; diff --git a/src/components/Form/src/types/form.ts b/src/components/Form/src/types/form.ts index 099be2b0..f54b1894 100644 --- a/src/components/Form/src/types/form.ts +++ b/src/components/Form/src/types/form.ts @@ -1,223 +1,227 @@ -import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface' -import type { VNode } from 'vue' -import type { ButtonProps as AntdButtonProps } from '/@/components/Button' -import type { FormItem } from './formItem' -import type { ColEx, ComponentType } from './index' -import type { TableActionType } from '/@/components/Table/src/types/table' -import type { CSSProperties } from 'vue' -import type { RowProps } from 'ant-design-vue/lib/grid/Row' +import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface'; +import type { VNode } from 'vue'; +import type { ButtonProps as AntdButtonProps } from '/@/components/Button'; +import type { FormItem } from './formItem'; +import type { ColEx, ComponentType } from './index'; +import type { TableActionType } from '/@/components/Table/src/types/table'; +import type { CSSProperties } from 'vue'; +import type { RowProps } from 'ant-design-vue/lib/grid/Row'; -export type FieldMapToTime = [string, [string, string], string?][] +export type FieldMapToTime = [string, [string, string], (string | [string, string])?][]; export type Rule = RuleObject & { - trigger?: 'blur' | 'change' | ['change', 'blur'] -} + trigger?: 'blur' | 'change' | ['change', 'blur']; +}; export interface RenderCallbackParams { - schema: FormSchema - values: Recordable - model: Recordable - field: string + schema: FormSchema; + values: Recordable; + model: Recordable; + field: string; } export interface ButtonProps extends AntdButtonProps { - text?: string + text?: string; } export interface FormActionType { - submit: () => Promise - setFieldsValue: (values: T) => Promise - resetFields: () => Promise - getFieldsValue: () => Recordable - clearValidate: (name?: string | string[]) => Promise - updateSchema: (data: Partial | Partial[]) => Promise - resetSchema: (data: Partial | Partial[]) => Promise - setProps: (formProps: Partial) => Promise - removeSchemaByField: (field: string | string[]) => Promise + submit: () => Promise; + setFieldsValue: (values: T) => Promise; + resetFields: () => Promise; + getFieldsValue: () => Recordable; + clearValidate: (name?: string | string[]) => Promise; + updateSchema: (data: Partial | Partial[]) => Promise; + resetSchema: (data: Partial | Partial[]) => Promise; + setProps: (formProps: Partial) => Promise; + removeSchemaByField: (field: string | string[]) => Promise; appendSchemaByField: ( schema: FormSchema, prefixField: string | undefined, first?: boolean | undefined, - ) => Promise - validateFields: (nameList?: NamePath[]) => Promise - validate: (nameList?: NamePath[]) => Promise - scrollToField: (name: NamePath, options?: ScrollOptions) => Promise + ) => Promise; + validateFields: (nameList?: NamePath[]) => Promise; + validate: (nameList?: NamePath[]) => Promise; + scrollToField: (name: NamePath, options?: ScrollOptions) => Promise; } -export type RegisterFn = (formInstance: FormActionType) => void +export type RegisterFn = (formInstance: FormActionType) => void; -export type UseFormReturnType = [RegisterFn, FormActionType] +export type UseFormReturnType = [RegisterFn, FormActionType]; export interface FormProps { - name?: string - layout?: 'vertical' | 'inline' | 'horizontal' + name?: string; + layout?: 'vertical' | 'inline' | 'horizontal'; // Form value - model?: Recordable + model?: Recordable; // The width of all items in the entire form - labelWidth?: number | string + labelWidth?: number | string; // alignment - labelAlign?: 'left' | 'right' + labelAlign?: 'left' | 'right'; // Row configuration for the entire form - rowProps?: RowProps + rowProps?: RowProps; // Submit form on reset - submitOnReset?: boolean + submitOnReset?: boolean; // Submit form on form changing - submitOnChange?: boolean + submitOnChange?: boolean; // Col configuration for the entire form - labelCol?: Partial + labelCol?: Partial; // Col configuration for the entire form - wrapperCol?: Partial + wrapperCol?: Partial; // General row style - baseRowStyle?: CSSProperties + baseRowStyle?: CSSProperties; // General col configuration - baseColProps?: Partial + baseColProps?: Partial; // Form configuration rules - schemas?: FormSchema[] + schemas?: FormSchema[]; // Function values used to merge into dynamic control form items - mergeDynamicData?: Recordable + mergeDynamicData?: Recordable; // Compact mode for search forms - compact?: boolean + compact?: boolean; // Blank line span - emptySpan?: number | Partial + emptySpan?: number | Partial; // Internal component size of the form - size?: 'default' | 'small' | 'large' + size?: 'default' | 'small' | 'large'; // Whether to disable - disabled?: boolean + disabled?: boolean; // Time interval fields are mapped into multiple - fieldMapToTime?: FieldMapToTime + fieldMapToTime?: FieldMapToTime; // Placeholder is set automatically - autoSetPlaceHolder?: boolean + autoSetPlaceHolder?: boolean; // Auto submit on press enter on input - autoSubmitOnEnter?: boolean + autoSubmitOnEnter?: boolean; // Check whether the information is added to the label - rulesMessageJoinLabel?: boolean + rulesMessageJoinLabel?: boolean; // Whether to show collapse and expand buttons - showAdvancedButton?: boolean + showAdvancedButton?: boolean; // Whether to focus on the first input box, only works when the first form item is input - autoFocusFirstItem?: boolean + autoFocusFirstItem?: boolean; // Automatically collapse over the specified number of rows - autoAdvancedLine?: number + autoAdvancedLine?: number; // Always show lines - alwaysShowLines?: number + alwaysShowLines?: number; // Whether to show the operation button - showActionButtonGroup?: boolean + showActionButtonGroup?: boolean; // Reset button configuration - resetButtonOptions?: Partial + resetButtonOptions?: Partial; // Confirm button configuration - submitButtonOptions?: Partial + submitButtonOptions?: Partial; // Operation column configuration - actionColOptions?: Partial + actionColOptions?: Partial; // Show reset button - showResetButton?: boolean + showResetButton?: boolean; // Show confirmation button - showSubmitButton?: boolean + showSubmitButton?: boolean; - resetFunc?: () => Promise - submitFunc?: () => Promise - transformDateFunc?: (date: any) => string - colon?: boolean + resetFunc?: () => Promise; + submitFunc?: () => Promise; + transformDateFunc?: (date: any) => string; + colon?: boolean; } export interface FormSchema { // Field name - field: string + field: string; // Event name triggered by internal value change, default change - changeEvent?: string + changeEvent?: string; // Variable name bound to v-model Default value - valueField?: string + valueField?: string; // Label name - label: string | VNode + label: string | VNode; // Auxiliary text - subLabel?: string + subLabel?: string; // Help text on the right side of the text helpMessage?: | string | string[] - | ((renderCallbackParams: RenderCallbackParams) => string | string[]) + | ((renderCallbackParams: RenderCallbackParams) => string | string[]); // BaseHelp component props - helpComponentProps?: Partial + helpComponentProps?: Partial; // Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid - labelWidth?: string | number + labelWidth?: string | number; // Disable the adjustment of labelWidth with global settings of formModel, and manually set labelCol and wrapperCol by yourself - disabledLabelWidth?: boolean + disabledLabelWidth?: boolean; // render component - component: ComponentType + component: ComponentType; // Component parameters componentProps?: | ((opt: { - schema: FormSchema - tableAction: TableActionType - formActionType: FormActionType - formModel: Recordable + schema: FormSchema; + tableAction: TableActionType; + formActionType: FormActionType; + formModel: Recordable; }) => Recordable) - | object + | object; // Required - required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean) + required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); - suffix?: string | number | ((values: RenderCallbackParams) => string | number) + suffix?: string | number | ((values: RenderCallbackParams) => string | number); // Validation rules - rules?: Rule[] + rules?: Rule[]; // Check whether the information is added to the label - rulesMessageJoinLabel?: boolean + rulesMessageJoinLabel?: boolean; // Reference formModelItem - itemProps?: Partial + itemProps?: Partial; // col configuration outside formModelItem - colProps?: Partial + colProps?: Partial; // 默认值 - defaultValue?: any - isAdvanced?: boolean + defaultValue?: any; + + // 是否自动处理与时间相关组件的默认值 + isHandleDateDefaultValue?: boolean; + + isAdvanced?: boolean; // Matching details components - span?: number + span?: number; - ifShow?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean) + ifShow?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); - show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean) + show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); // Render the content in the form-item tag - render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string + render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string; // Rendering col content requires outer wrapper form-item - renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string + renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string; renderComponentContent?: | ((renderCallbackParams: RenderCallbackParams) => any) | VNode | VNode[] - | string + | string; // Custom slot, in from-item - slot?: string + slot?: string; // Custom slot, similar to renderColContent - colSlot?: string + colSlot?: string; - dynamicDisabled?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean) + dynamicDisabled?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); - dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[] + dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[]; } export interface HelpComponentProps { - maxWidth: string + maxWidth: string; // Whether to display the serial number - showIndex: boolean + showIndex: boolean; // Text list - text: any + text: any; // colour - color: string + color: string; // font size - fontSize: string - icon: string - absolute: boolean + fontSize: string; + icon: string; + absolute: boolean; // Positioning - position: any + position: any; } diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts index 9f577f00..294b0805 100644 --- a/src/components/Form/src/types/index.ts +++ b/src/components/Form/src/types/index.ts @@ -1,83 +1,83 @@ -type ColSpanType = number | string +type ColSpanType = number | string; export interface ColEx { - style?: any + style?: any; /** * raster number of cells to occupy, 0 corresponds to display: none * @default none (0) * @type ColSpanType */ - span?: ColSpanType + span?: ColSpanType; /** * raster order, used in flex layout mode * @default 0 * @type ColSpanType */ - order?: ColSpanType + order?: ColSpanType; /** * the layout fill of flex * @default none * @type ColSpanType */ - flex?: ColSpanType + flex?: ColSpanType; /** * the number of cells to offset Col from the left * @default 0 * @type ColSpanType */ - offset?: ColSpanType + offset?: ColSpanType; /** * the number of cells that raster is moved to the right * @default 0 * @type ColSpanType */ - push?: ColSpanType + push?: ColSpanType; /** * the number of cells that raster is moved to the left * @default 0 * @type ColSpanType */ - pull?: ColSpanType + pull?: ColSpanType; /** * <576px and also default setting, could be a span value or an object containing above props * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType */ - xs?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + xs?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; /** * ≥576px, could be a span value or an object containing above props * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType */ - sm?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + sm?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; /** * ≥768px, could be a span value or an object containing above props * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType */ - md?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + md?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; /** * ≥992px, could be a span value or an object containing above props * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType */ - lg?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + lg?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; /** * ≥1200px, could be a span value or an object containing above props * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType */ - xl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + xl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; /** * ≥1600px, could be a span value or an object containing above props * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType */ - xxl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + xxl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType; } export type ComponentType = @@ -114,4 +114,4 @@ export type ComponentType = | 'Slider' | 'Rate' | 'Divider' - | 'ApiTransfer' + | 'ApiTransfer'; diff --git a/src/components/Form1/index.ts b/src/components/Form1/index.ts new file mode 100644 index 00000000..eb7b797f --- /dev/null +++ b/src/components/Form1/index.ts @@ -0,0 +1,17 @@ +import BasicForm from './src/BasicForm.vue' + +export * from './src/types/form' +export * from './src/types/formItem' + +export { useComponentRegister } from './src/hooks/useComponentRegister' +export { useForm } from './src/hooks/useForm' + +export { default as ApiSelect } from './src/components/ApiSelect.vue' +export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue' +export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue' +export { default as ApiTree } from './src/components/ApiTree.vue' +export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue' +export { default as ApiCascader } from './src/components/ApiCascader.vue' +export { default as ApiTransfer } from './src/components/ApiTransfer.vue' + +export { BasicForm } diff --git a/src/components/Form1/src/BasicForm.vue b/src/components/Form1/src/BasicForm.vue new file mode 100644 index 00000000..05acef7a --- /dev/null +++ b/src/components/Form1/src/BasicForm.vue @@ -0,0 +1,353 @@ + + + diff --git a/src/components/Form1/src/componentMap.ts b/src/components/Form1/src/componentMap.ts new file mode 100644 index 00000000..8f447d2c --- /dev/null +++ b/src/components/Form1/src/componentMap.ts @@ -0,0 +1,83 @@ +import type { Component } from 'vue' +import type { ComponentType } from './types/index' + +/** + * Component list, register here to setting it in the form + */ +import { + Input, + Select, + Radio, + Checkbox, + AutoComplete, + Cascader, + DatePicker, + InputNumber, + Switch, + TimePicker, + TreeSelect, + Slider, + Rate, + Divider, +} from 'ant-design-vue' + +import ApiRadioGroup from './components/ApiRadioGroup.vue' +import RadioButtonGroup from './components/RadioButtonGroup.vue' +import ApiSelect from './components/ApiSelect.vue' +import ApiTree from './components/ApiTree.vue' +import ApiTreeSelect from './components/ApiTreeSelect.vue' +import ApiCascader from './components/ApiCascader.vue' +import ApiTransfer from './components/ApiTransfer.vue' +import { BasicUpload } from '/@/components/Upload' +import { StrengthMeter } from '/@/components/StrengthMeter' +import { IconPicker } from '/@/components/Icon' +import { CountdownInput } from '/@/components/CountDown' + +const componentMap = new Map() + +componentMap.set('Input', Input) +componentMap.set('InputGroup', Input.Group) +componentMap.set('InputPassword', Input.Password) +componentMap.set('InputSearch', Input.Search) +componentMap.set('InputTextArea', Input.TextArea) +componentMap.set('InputNumber', InputNumber) +componentMap.set('AutoComplete', AutoComplete) + +componentMap.set('Select', Select) +componentMap.set('ApiSelect', ApiSelect) +componentMap.set('ApiTree', ApiTree) +componentMap.set('TreeSelect', TreeSelect) +componentMap.set('ApiTreeSelect', ApiTreeSelect) +componentMap.set('ApiRadioGroup', ApiRadioGroup) +componentMap.set('Switch', Switch) +componentMap.set('RadioButtonGroup', RadioButtonGroup) +componentMap.set('RadioGroup', Radio.Group) +componentMap.set('Checkbox', Checkbox) +componentMap.set('CheckboxGroup', Checkbox.Group) +componentMap.set('ApiCascader', ApiCascader) +componentMap.set('Cascader', Cascader) +componentMap.set('Slider', Slider) +componentMap.set('Rate', Rate) +componentMap.set('ApiTransfer', ApiTransfer) + +componentMap.set('DatePicker', DatePicker) +componentMap.set('MonthPicker', DatePicker.MonthPicker) +componentMap.set('RangePicker', DatePicker.RangePicker) +componentMap.set('WeekPicker', DatePicker.WeekPicker) +componentMap.set('TimePicker', TimePicker) +componentMap.set('StrengthMeter', StrengthMeter) +componentMap.set('IconPicker', IconPicker) +componentMap.set('InputCountDown', CountdownInput) + +componentMap.set('Upload', BasicUpload) +componentMap.set('Divider', Divider) + +export function add(compName: ComponentType, component: Component) { + componentMap.set(compName, component) +} + +export function del(compName: ComponentType) { + componentMap.delete(compName) +} + +export { componentMap } diff --git a/src/components/Form1/src/components/ApiCascader.vue b/src/components/Form1/src/components/ApiCascader.vue new file mode 100644 index 00000000..80805f33 --- /dev/null +++ b/src/components/Form1/src/components/ApiCascader.vue @@ -0,0 +1,198 @@ + + diff --git a/src/components/Form1/src/components/ApiRadioGroup.vue b/src/components/Form1/src/components/ApiRadioGroup.vue new file mode 100644 index 00000000..712cfba8 --- /dev/null +++ b/src/components/Form1/src/components/ApiRadioGroup.vue @@ -0,0 +1,130 @@ + + + diff --git a/src/components/Form1/src/components/ApiSelect.vue b/src/components/Form1/src/components/ApiSelect.vue new file mode 100644 index 00000000..66718639 --- /dev/null +++ b/src/components/Form1/src/components/ApiSelect.vue @@ -0,0 +1,147 @@ + + diff --git a/src/components/Form1/src/components/ApiTransfer.vue b/src/components/Form1/src/components/ApiTransfer.vue new file mode 100644 index 00000000..2cf1f46c --- /dev/null +++ b/src/components/Form1/src/components/ApiTransfer.vue @@ -0,0 +1,134 @@ + + + diff --git a/src/components/Form1/src/components/ApiTree.vue b/src/components/Form1/src/components/ApiTree.vue new file mode 100644 index 00000000..ea80c2d1 --- /dev/null +++ b/src/components/Form1/src/components/ApiTree.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/components/Form1/src/components/ApiTreeSelect.vue b/src/components/Form1/src/components/ApiTreeSelect.vue new file mode 100644 index 00000000..3f073d34 --- /dev/null +++ b/src/components/Form1/src/components/ApiTreeSelect.vue @@ -0,0 +1,86 @@ + + + diff --git a/src/components/Form1/src/components/FormAction.vue b/src/components/Form1/src/components/FormAction.vue new file mode 100644 index 00000000..8dec49fe --- /dev/null +++ b/src/components/Form1/src/components/FormAction.vue @@ -0,0 +1,135 @@ + + diff --git a/src/components/Form1/src/components/FormItem.vue b/src/components/Form1/src/components/FormItem.vue new file mode 100644 index 00000000..2aa3b4ec --- /dev/null +++ b/src/components/Form1/src/components/FormItem.vue @@ -0,0 +1,390 @@ + diff --git a/src/components/Form1/src/components/RadioButtonGroup.vue b/src/components/Form1/src/components/RadioButtonGroup.vue new file mode 100644 index 00000000..c2c7b220 --- /dev/null +++ b/src/components/Form1/src/components/RadioButtonGroup.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/components/Form1/src/helper.ts b/src/components/Form1/src/helper.ts new file mode 100644 index 00000000..f357da58 --- /dev/null +++ b/src/components/Form1/src/helper.ts @@ -0,0 +1,74 @@ +import type { ValidationRule } from 'ant-design-vue/lib/form/Form' +import type { ComponentType } from './types/index' +import { useI18n } from '/@/hooks/web/useI18n' +import { dateUtil } from '/@/utils/dateUtil' +import { isNumber, isObject } from '/@/utils/is' + +const { t } = useI18n() + +/** + * @description: 生成placeholder + */ +export function createPlaceholderMessage(component: ComponentType) { + if (component.includes('Input') || component.includes('Complete')) { + return t('common.inputText') + } + if (component.includes('Picker')) { + return t('common.chooseText') + } + if ( + component.includes('Select') || + component.includes('Cascader') || + component.includes('Checkbox') || + component.includes('Radio') || + component.includes('Switch') + ) { + // return `请选择${label}`; + return t('common.chooseText') + } + return '' +} + +const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'] + +function genType() { + return [...DATE_TYPE, 'RangePicker'] +} + +export function setComponentRuleType( + rule: ValidationRule, + component: ComponentType, + valueFormat: string, +) { + if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) { + rule.type = valueFormat ? 'string' : 'object' + } else if (['RangePicker', 'Upload', 'CheckboxGroup', 'TimePicker'].includes(component)) { + rule.type = 'array' + } else if (['InputNumber'].includes(component)) { + rule.type = 'number' + } +} + +export function processDateValue(attr: Recordable, component: string) { + const { valueFormat, value } = attr + if (valueFormat) { + attr.value = isObject(value) ? dateUtil(value).format(valueFormat) : value + } else if (DATE_TYPE.includes(component) && value) { + attr.value = dateUtil(attr.value) + } +} + +export function handleInputNumberValue(component?: ComponentType, val?: any) { + if (!component) return val + if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) { + return val && isNumber(val) ? `${val}` : val + } + return val +} + +/** + * 时间字段 + */ +export const dateItemType = genType() + +export const defaultValueComponents = ['Input', 'InputPassword', 'InputSearch', 'InputTextArea'] diff --git a/src/components/Form1/src/hooks/useAdvanced.ts b/src/components/Form1/src/hooks/useAdvanced.ts new file mode 100644 index 00000000..ca532f5c --- /dev/null +++ b/src/components/Form1/src/hooks/useAdvanced.ts @@ -0,0 +1,170 @@ +import type { ColEx } from '../types' +import type { AdvanceState } from '../types/hooks' +import { ComputedRef, getCurrentInstance, Ref } from 'vue' +import type { FormProps, FormSchema } from '../types/form' +import { computed, unref, watch } from 'vue' +import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is' +import { useBreakpoint } from '/@/hooks/event/useBreakpoint' +import { useDebounceFn } from '@vueuse/core' + +const BASIC_COL_LEN = 24 + +interface UseAdvancedContext { + advanceState: AdvanceState + emit: EmitType + getProps: ComputedRef + getSchema: ComputedRef + formModel: Recordable + defaultValueRef: Ref +} + +export default function ({ + advanceState, + emit, + getProps, + getSchema, + formModel, + defaultValueRef, +}: UseAdvancedContext) { + const vm = getCurrentInstance() + + const { realWidthRef, screenEnum, screenRef } = useBreakpoint() + + const getEmptySpan = computed((): number => { + if (!advanceState.isAdvanced) { + return 0 + } + // For some special cases, you need to manually specify additional blank lines + const emptySpan = unref(getProps).emptySpan || 0 + + if (isNumber(emptySpan)) { + return emptySpan + } + if (isObject(emptySpan)) { + const { span = 0 } = emptySpan + const screen = unref(screenRef) as string + + const screenSpan = (emptySpan as any)[screen.toLowerCase()] + return screenSpan || span || 0 + } + return 0 + }) + + const debounceUpdateAdvanced = useDebounceFn(updateAdvanced, 30) + + watch( + [() => unref(getSchema), () => advanceState.isAdvanced, () => unref(realWidthRef)], + () => { + const { showAdvancedButton } = unref(getProps) + if (showAdvancedButton) { + debounceUpdateAdvanced() + } + }, + { immediate: true }, + ) + + function getAdvanced(itemCol: Partial, itemColSum = 0, isLastAction = false) { + const width = unref(realWidthRef) + + const mdWidth = + parseInt(itemCol.md as string) || + parseInt(itemCol.xs as string) || + parseInt(itemCol.sm as string) || + (itemCol.span as number) || + BASIC_COL_LEN + + const lgWidth = parseInt(itemCol.lg as string) || mdWidth + const xlWidth = parseInt(itemCol.xl as string) || lgWidth + const xxlWidth = parseInt(itemCol.xxl as string) || xlWidth + if (width <= screenEnum.LG) { + itemColSum += mdWidth + } else if (width < screenEnum.XL) { + itemColSum += lgWidth + } else if (width < screenEnum.XXL) { + itemColSum += xlWidth + } else { + itemColSum += xxlWidth + } + + if (isLastAction) { + advanceState.hideAdvanceBtn = false + if (itemColSum <= BASIC_COL_LEN * 2) { + // When less than or equal to 2 lines, the collapse and expand buttons are not displayed + advanceState.hideAdvanceBtn = true + advanceState.isAdvanced = true + } else if ( + itemColSum > BASIC_COL_LEN * 2 && + itemColSum <= BASIC_COL_LEN * (unref(getProps).autoAdvancedLine || 3) + ) { + advanceState.hideAdvanceBtn = false + + // More than 3 lines collapsed by default + } else if (!advanceState.isLoad) { + advanceState.isLoad = true + advanceState.isAdvanced = !advanceState.isAdvanced + } + return { isAdvanced: advanceState.isAdvanced, itemColSum } + } + if (itemColSum > BASIC_COL_LEN * (unref(getProps).alwaysShowLines || 1)) { + return { isAdvanced: advanceState.isAdvanced, itemColSum } + } else { + // The first line is always displayed + return { isAdvanced: true, itemColSum } + } + } + + function updateAdvanced() { + let itemColSum = 0 + let realItemColSum = 0 + const { baseColProps = {} } = unref(getProps) + + for (const schema of unref(getSchema)) { + const { show, colProps } = schema + let isShow = true + + if (isBoolean(show)) { + isShow = show + } + + if (isFunction(show)) { + isShow = show({ + schema: schema, + model: formModel, + field: schema.field, + values: { + ...unref(defaultValueRef), + ...formModel, + }, + }) + } + + if (isShow && (colProps || baseColProps)) { + const { itemColSum: sum, isAdvanced } = getAdvanced( + { ...baseColProps, ...colProps }, + itemColSum, + ) + + itemColSum = sum || 0 + if (isAdvanced) { + realItemColSum = itemColSum + } + schema.isAdvanced = isAdvanced + } + } + + // 确保页面发送更新 + vm?.proxy?.$forceUpdate() + + advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan) + + getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true) + + emit('advanced-change') + } + + function handleToggleAdvanced() { + advanceState.isAdvanced = !advanceState.isAdvanced + } + + return { handleToggleAdvanced } +} diff --git a/src/components/Form1/src/hooks/useAutoFocus.ts b/src/components/Form1/src/hooks/useAutoFocus.ts new file mode 100644 index 00000000..e24dd6bb --- /dev/null +++ b/src/components/Form1/src/hooks/useAutoFocus.ts @@ -0,0 +1,40 @@ +import type { ComputedRef, Ref } from 'vue'; +import type { FormSchema, FormActionType, FormProps } from '../types/form'; + +import { unref, nextTick, watchEffect } from 'vue'; + +interface UseAutoFocusContext { + getSchema: ComputedRef; + getProps: ComputedRef; + isInitedDefault: Ref; + formElRef: Ref; +} +export async function useAutoFocus({ + getSchema, + getProps, + formElRef, + isInitedDefault, +}: UseAutoFocusContext) { + watchEffect(async () => { + if (unref(isInitedDefault) || !unref(getProps).autoFocusFirstItem) { + return; + } + await nextTick(); + const schemas = unref(getSchema); + const formEl = unref(formElRef); + const el = (formEl as any)?.$el as HTMLElement; + if (!formEl || !el || !schemas || schemas.length === 0) { + return; + } + + const firstItem = schemas[0]; + // Only open when the first form item is input type + if (!firstItem.component.includes('Input')) { + return; + } + + const inputEl = el.querySelector('.ant-row:first-child input') as Nullable; + if (!inputEl) return; + inputEl?.focus(); + }); +} diff --git a/src/components/Form1/src/hooks/useComponentRegister.ts b/src/components/Form1/src/hooks/useComponentRegister.ts new file mode 100644 index 00000000..218aaa9b --- /dev/null +++ b/src/components/Form1/src/hooks/useComponentRegister.ts @@ -0,0 +1,11 @@ +import type { ComponentType } from '../types/index'; +import { tryOnUnmounted } from '@vueuse/core'; +import { add, del } from '../componentMap'; +import type { Component } from 'vue'; + +export function useComponentRegister(compName: ComponentType, comp: Component) { + add(compName, comp); + tryOnUnmounted(() => { + del(compName); + }); +} diff --git a/src/components/Form1/src/hooks/useForm.ts b/src/components/Form1/src/hooks/useForm.ts new file mode 100644 index 00000000..ed76e134 --- /dev/null +++ b/src/components/Form1/src/hooks/useForm.ts @@ -0,0 +1,122 @@ +import type { FormProps, FormActionType, UseFormReturnType, FormSchema } from '../types/form' +import type { NamePath } from 'ant-design-vue/lib/form/interface' +import type { DynamicProps } from '/#/utils' +import { ref, onUnmounted, unref, nextTick, watch } from 'vue' +import { isProdMode } from '/@/utils/env' +import { error } from '/@/utils/log' +import { getDynamicProps } from '/@/utils' + +export declare type ValidateFields = (nameList?: NamePath[]) => Promise + +type Props = Partial> + +export function useForm(props?: Props): UseFormReturnType { + const formRef = ref>(null) + const loadedRef = ref>(false) + + async function getForm() { + const form = unref(formRef) + if (!form) { + 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 = { + scrollToField: async (name: NamePath, options?: ScrollOptions | undefined) => { + const form = await getForm() + form.scrollToField(name, options) + }, + setProps: async (formProps: Partial) => { + const form = await getForm() + form.setProps(formProps) + }, + + updateSchema: async (data: Partial | Partial[]) => { + const form = await getForm() + form.updateSchema(data) + }, + + resetSchema: async (data: Partial | Partial[]) => { + const form = await getForm() + form.resetSchema(data) + }, + + clearValidate: async (name?: string | string[]) => { + const form = await getForm() + form.clearValidate(name) + }, + + resetFields: async () => { + getForm().then(async (form) => { + await form.resetFields() + }) + }, + + removeSchemaByField: async (field: string | string[]) => { + unref(formRef)?.removeSchemaByField(field) + }, + + // TODO promisify + getFieldsValue: () => { + return unref(formRef)?.getFieldsValue() as T + }, + + setFieldsValue: async (values: T) => { + const form = await getForm() + form.setFieldsValue(values) + }, + + appendSchemaByField: async ( + schema: FormSchema, + prefixField: string | undefined, + first: boolean, + ) => { + const form = await getForm() + form.appendSchemaByField(schema, prefixField, first) + }, + + submit: async (): Promise => { + const form = await getForm() + return form.submit() + }, + + validate: async (nameList?: NamePath[]): Promise => { + const form = await getForm() + return form.validate(nameList) + }, + + validateFields: async (nameList?: NamePath[]): Promise => { + const form = await getForm() + return form.validateFields(nameList) + }, + } + + return [register, methods] +} diff --git a/src/components/Form1/src/hooks/useFormContext.ts b/src/components/Form1/src/hooks/useFormContext.ts new file mode 100644 index 00000000..01dfadd7 --- /dev/null +++ b/src/components/Form1/src/hooks/useFormContext.ts @@ -0,0 +1,17 @@ +import type { InjectionKey } from 'vue'; +import { createContext, useContext } from '/@/hooks/core/useContext'; + +export interface FormContextProps { + resetAction: () => Promise; + submitAction: () => Promise; +} + +const key: InjectionKey = Symbol(); + +export function createFormContext(context: FormContextProps) { + return createContext(context, key); +} + +export function useFormContext() { + return useContext(key); +} diff --git a/src/components/Form1/src/hooks/useFormEvents.ts b/src/components/Form1/src/hooks/useFormEvents.ts new file mode 100644 index 00000000..6bc29696 --- /dev/null +++ b/src/components/Form1/src/hooks/useFormEvents.ts @@ -0,0 +1,314 @@ +import type { ComputedRef, Ref } from 'vue' +import type { FormProps, FormSchema, FormActionType } from '../types/form' +import type { NamePath } from 'ant-design-vue/lib/form/interface' +import { unref, toRaw, nextTick } from 'vue' +import { isArray, isFunction, isObject, isString, isDef, isNullOrUnDef } from '/@/utils/is' +import { deepMerge } from '/@/utils' +import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper' +import { dateUtil } from '/@/utils/dateUtil' +import { cloneDeep, uniqBy } from 'lodash-es' +import { error } from '/@/utils/log' + +interface UseFormActionContext { + emit: EmitType + getProps: ComputedRef + getSchema: ComputedRef + formModel: Recordable + defaultValueRef: Ref + formElRef: Ref + schemaRef: Ref + handleFormValues: Fn +} +export function useFormEvents({ + emit, + getProps, + formModel, + getSchema, + defaultValueRef, + formElRef, + schemaRef, + handleFormValues, +}: UseFormActionContext) { + async function resetFields(): Promise { + const { resetFunc, submitOnReset } = unref(getProps) + resetFunc && isFunction(resetFunc) && (await resetFunc()) + + const formEl = unref(formElRef) + if (!formEl) return + + Object.keys(formModel).forEach((key) => { + const schema = unref(getSchema).find((item) => item.field === key) + const isInput = schema?.component && defaultValueComponents.includes(schema.component) + const defaultValue = cloneDeep(defaultValueRef.value[key]) + formModel[key] = isInput ? defaultValue || '' : defaultValue + }) + nextTick(() => clearValidate()) + + emit('reset', toRaw(formModel)) + submitOnReset && handleSubmit() + } + + /** + * @description: Set form value + */ + async function setFieldsValue(values: Recordable): Promise { + const fields = unref(getSchema) + .map((item) => item.field) + .filter(Boolean) + + // key 支持 a.b.c 的嵌套写法 + const delimiter = '.' + const nestKeyArray = fields.filter((item) => item.indexOf(delimiter) >= 0) + + const validKeys: string[] = [] + Object.keys(values).forEach((key) => { + const schema = unref(getSchema).find((item) => item.field === key) + let value = values[key] + + const hasKey = Reflect.has(values, key) + + value = handleInputNumberValue(schema?.component, value) + // 0| '' is allow + if (hasKey && fields.includes(key)) { + // time type + if (itemIsDateType(key)) { + if (Array.isArray(value)) { + const arr: any[] = [] + for (const ele of value) { + arr.push(ele ? dateUtil(ele) : null) + } + formModel[key] = arr + } else { + const { componentProps } = schema || {} + let _props = componentProps as any + if (typeof componentProps === 'function') { + _props = _props({ formModel }) + } + formModel[key] = value ? (_props?.valueFormat ? value : dateUtil(value)) : null + } + } else { + formModel[key] = value + } + validKeys.push(key) + } else { + nestKeyArray.forEach((nestKey: string) => { + try { + const value = eval('values' + delimiter + nestKey) + if (isDef(value)) { + formModel[nestKey] = value + validKeys.push(nestKey) + } + } catch (e) { + // key not exist + if (isDef(defaultValueRef.value[nestKey])) { + formModel[nestKey] = cloneDeep(defaultValueRef.value[nestKey]) + } + } + }) + } + }) + validateFields(validKeys).catch((_) => {}) + } + /** + * @description: Delete based on field name + */ + async function removeSchemaByField(fields: string | string[]): Promise { + const schemaList: FormSchema[] = cloneDeep(unref(getSchema)) + if (!fields) { + return + } + + let fieldList: string[] = isString(fields) ? [fields] : fields + if (isString(fields)) { + fieldList = [fields] + } + for (const field of fieldList) { + _removeSchemaByFeild(field, schemaList) + } + schemaRef.value = schemaList + } + + /** + * @description: Delete based on field name + */ + function _removeSchemaByFeild(field: string, schemaList: FormSchema[]): void { + if (isString(field)) { + const index = schemaList.findIndex((schema) => schema.field === field) + if (index !== -1) { + delete formModel[field] + schemaList.splice(index, 1) + } + } + } + + /** + * @description: Insert after a certain field, if not insert the last + */ + async function appendSchemaByField(schema: FormSchema, prefixField?: string, first = false) { + const schemaList: FormSchema[] = cloneDeep(unref(getSchema)) + + const index = schemaList.findIndex((schema) => schema.field === prefixField) + + if (!prefixField || index === -1 || first) { + first ? schemaList.unshift(schema) : schemaList.push(schema) + schemaRef.value = schemaList + _setDefaultValue(schema) + return + } + if (index !== -1) { + schemaList.splice(index + 1, 0, schema) + } + _setDefaultValue(schema) + + schemaRef.value = schemaList + } + + async function resetSchema(data: Partial | Partial[]) { + let updateData: Partial[] = [] + if (isObject(data)) { + updateData.push(data as FormSchema) + } + if (isArray(data)) { + updateData = [...data] + } + + const hasField = updateData.every( + (item) => item.component === 'Divider' || (Reflect.has(item, 'field') && item.field), + ) + + if (!hasField) { + error( + 'All children of the form Schema array that need to be updated must contain the `field` field', + ) + return + } + schemaRef.value = updateData as FormSchema[] + } + + async function updateSchema(data: Partial | Partial[]) { + let updateData: Partial[] = [] + if (isObject(data)) { + updateData.push(data as FormSchema) + } + if (isArray(data)) { + updateData = [...data] + } + + const hasField = updateData.every( + (item) => item.component === 'Divider' || (Reflect.has(item, 'field') && item.field), + ) + + if (!hasField) { + error( + 'All children of the form Schema array that need to be updated must contain the `field` field', + ) + return + } + const schema: FormSchema[] = [] + updateData.forEach((item) => { + unref(getSchema).forEach((val) => { + if (val.field === item.field) { + const newSchema = deepMerge(val, item) + schema.push(newSchema as FormSchema) + } else { + schema.push(val) + } + }) + }) + _setDefaultValue(schema) + + schemaRef.value = uniqBy(schema, 'field') + } + + function _setDefaultValue(data: FormSchema | FormSchema[]) { + let schemas: FormSchema[] = [] + if (isObject(data)) { + schemas.push(data as FormSchema) + } + if (isArray(data)) { + schemas = [...data] + } + + const obj: Recordable = {} + const currentFieldsValue = getFieldsValue() + schemas.forEach((item) => { + if ( + item.component != 'Divider' && + Reflect.has(item, 'field') && + item.field && + !isNullOrUnDef(item.defaultValue) && + !(item.field in currentFieldsValue) + ) { + obj[item.field] = item.defaultValue + } + }) + setFieldsValue(obj) + } + + function getFieldsValue(): Recordable { + const formEl = unref(formElRef) + if (!formEl) return {} + return handleFormValues(toRaw(unref(formModel))) + } + + /** + * @description: Is it time + */ + function itemIsDateType(key: string) { + return unref(getSchema).some((item) => { + return item.field === key ? dateItemType.includes(item.component) : false + }) + } + + async function validateFields(nameList?: NamePath[] | undefined) { + return unref(formElRef)?.validateFields(nameList) + } + + async function validate(nameList?: NamePath[] | undefined) { + return await unref(formElRef)?.validate(nameList) + } + + async function clearValidate(name?: string | string[]) { + await unref(formElRef)?.clearValidate(name) + } + + async function scrollToField(name: NamePath, options?: ScrollOptions | undefined) { + await unref(formElRef)?.scrollToField(name, options) + } + + /** + * @description: Form submission + */ + async function handleSubmit(e?: Event): Promise { + e && e.preventDefault() + const { submitFunc } = unref(getProps) + if (submitFunc && isFunction(submitFunc)) { + await submitFunc() + return + } + const formEl = unref(formElRef) + if (!formEl) return + try { + const values = await validate() + const res = handleFormValues(values) + emit('submit', res) + } catch (error: any) { + throw new Error(error) + } + } + + return { + handleSubmit, + clearValidate, + validate, + validateFields, + getFieldsValue, + updateSchema, + resetSchema, + appendSchemaByField, + removeSchemaByField, + resetFields, + setFieldsValue, + scrollToField, + } +} diff --git a/src/components/Form1/src/hooks/useFormValues.ts b/src/components/Form1/src/hooks/useFormValues.ts new file mode 100644 index 00000000..5c917686 --- /dev/null +++ b/src/components/Form1/src/hooks/useFormValues.ts @@ -0,0 +1,131 @@ +import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '/@/utils/is' +import { dateUtil } from '/@/utils/dateUtil' +import { unref } from 'vue' +import type { Ref, ComputedRef } from 'vue' +import type { FormProps, FormSchema } from '../types/form' +import { cloneDeep, set } from 'lodash-es' + +interface UseFormValuesContext { + defaultValueRef: Ref + getSchema: ComputedRef + getProps: ComputedRef + formModel: Recordable +} + +/** + * @desription deconstruct array-link key. This method will mutate the target. + */ +function tryDeconstructArray(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 = Array.isArray(value) ? value : [value] + keys.forEach((k, index) => { + set(target, k.trim(), value[index]) + }) + return true + } + } +} + +/** + * @desription deconstruct object-link key. This method will mutate the target. + */ +function tryDeconstructObject(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({ + defaultValueRef, + getSchema, + formModel, + getProps, +}: UseFormValuesContext) { + // Processing form values + function handleFormValues(values: Recordable) { + if (!isObject(values)) { + return {} + } + 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)) { + continue + } + const transformDateFunc = unref(getProps).transformDateFunc + if (isObject(value)) { + value = transformDateFunc?.(value) + } + + if (isArray(value) && value[0]?.format && value[1]?.format) { + value = value.map((item) => transformDateFunc?.(item)) + } + // Remove spaces + if (isString(value)) { + value = value.trim() + } + if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) { + // 没有解构成功的,按原样赋值 + set(res, key, value) + } + } + return handleRangeTimeValue(res) + } + + /** + * @description: Processing time interval parameters + */ + function handleRangeTimeValue(values: Recordable) { + const fieldMapToTime = unref(getProps).fieldMapToTime + + if (!fieldMapToTime || !Array.isArray(fieldMapToTime)) { + return values + } + + for (const [field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD'] of fieldMapToTime) { + if (!field || !startTimeKey || !endTimeKey || !values[field]) { + continue + } + + const [startTime, endTime]: string[] = values[field] + + values[startTimeKey] = dateUtil(startTime).format(format) + values[endTimeKey] = dateUtil(endTime).format(format) + Reflect.deleteProperty(values, field) + } + + return values + } + + function initDefault() { + const schemas = unref(getSchema) + const obj: Recordable = {} + schemas.forEach((item) => { + const { defaultValue } = item + if (!isNullOrUnDef(defaultValue)) { + obj[item.field] = defaultValue + + if (formModel[item.field] === undefined) { + formModel[item.field] = defaultValue + } + } + }) + defaultValueRef.value = cloneDeep(obj) + } + + return { handleFormValues, initDefault } +} diff --git a/src/components/Form1/src/hooks/useLabelWidth.ts b/src/components/Form1/src/hooks/useLabelWidth.ts new file mode 100644 index 00000000..7e20feec --- /dev/null +++ b/src/components/Form1/src/hooks/useLabelWidth.ts @@ -0,0 +1,42 @@ +import type { Ref } from 'vue' +import { computed, unref } from 'vue' +import type { FormProps, FormSchema } from '../types/form' +import { isNumber } from '/@/utils/is' + +export function useItemLabelWidth(schemaItemRef: Ref, propsRef: Ref) { + return computed(() => { + const schemaItem = unref(schemaItemRef) + const { labelCol = {}, wrapperCol = {} } = schemaItem.itemProps || {} + const { labelWidth, disabledLabelWidth } = schemaItem + + const { + labelWidth: globalLabelWidth, + labelCol: globalLabelCol, + wrapperCol: globWrapperCol, + layout, + } = unref(propsRef) + + // If labelWidth is set globally, all items setting + if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) { + labelCol.style = { + textAlign: 'left', + } + return { labelCol, wrapperCol } + } + let width = labelWidth || globalLabelWidth + const col = { ...globalLabelCol, ...labelCol } + const wrapCol = { ...globWrapperCol, ...wrapperCol } + + if (width) { + width = isNumber(width) ? `${width}px` : width + } + + return { + labelCol: { style: { width }, ...col }, + wrapperCol: { + style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` }, + ...wrapCol, + }, + } + }) +} diff --git a/src/components/Form1/src/props.ts b/src/components/Form1/src/props.ts new file mode 100644 index 00000000..d40fd1b8 --- /dev/null +++ b/src/components/Form1/src/props.ts @@ -0,0 +1,103 @@ +import type { FieldMapToTime, FormSchema } from './types/form' +import type { CSSProperties, PropType } from 'vue' +import type { ColEx } from './types' +import type { TableActionType } from '/@/components/Table' +import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes' +import type { RowProps } from 'ant-design-vue/lib/grid/Row' +import { propTypes } from '/@/utils/propTypes' + +export const basicProps = { + model: { + type: Object as PropType, + default: {}, + }, + // 标签宽度 固定宽度 + labelWidth: { + type: [Number, String] as PropType, + default: 0, + }, + fieldMapToTime: { + type: Array as PropType, + default: () => [], + }, + compact: propTypes.bool, + // 表单配置规则 + schemas: { + type: [Array] as PropType, + default: () => [], + }, + mergeDynamicData: { + type: Object as PropType, + default: null, + }, + baseRowStyle: { + type: Object as PropType, + }, + baseColProps: { + type: Object as PropType>, + }, + autoSetPlaceHolder: propTypes.bool.def(true), + // 在INPUT组件上单击回车时,是否自动提交 + autoSubmitOnEnter: propTypes.bool.def(false), + submitOnReset: propTypes.bool, + submitOnChange: propTypes.bool, + size: propTypes.oneOf(['default', 'small', 'large']).def('default'), + // 禁用表单 + disabled: propTypes.bool, + emptySpan: { + type: [Number, Object] as PropType, + default: 0, + }, + // 是否显示收起展开按钮 + showAdvancedButton: propTypes.bool, + // 转化时间 + transformDateFunc: { + type: Function as PropType, + default: (date: any) => { + return date?.format?.('YYYY-MM-DD HH:mm:ss') ?? date + }, + }, + rulesMessageJoinLabel: propTypes.bool.def(true), + // 超过3行自动折叠 + autoAdvancedLine: propTypes.number.def(3), + // 不受折叠影响的行数 + alwaysShowLines: propTypes.number.def(1), + + // 是否显示操作按钮 + showActionButtonGroup: propTypes.bool.def(true), + // 操作列Col配置 + actionColOptions: Object as PropType>, + // 显示重置按钮 + showResetButton: propTypes.bool.def(true), + // 是否聚焦第一个输入框,只在第一个表单项为input的时候作用 + autoFocusFirstItem: propTypes.bool, + // 重置按钮配置 + resetButtonOptions: Object as PropType>, + + // 显示确认按钮 + showSubmitButton: propTypes.bool.def(true), + // 确认按钮配置 + submitButtonOptions: Object as PropType>, + + // 自定义重置函数 + resetFunc: Function as PropType<() => Promise>, + submitFunc: Function as PropType<() => Promise>, + + // 以下为默认props + hideRequiredMark: propTypes.bool, + + labelCol: Object as PropType>, + + layout: propTypes.oneOf(['horizontal', 'vertical', 'inline']).def('horizontal'), + tableAction: { + type: Object as PropType, + }, + + wrapperCol: Object as PropType>, + + colon: propTypes.bool, + + labelAlign: propTypes.string, + + rowProps: Object as PropType, +} diff --git a/src/components/Form1/src/types/form.ts b/src/components/Form1/src/types/form.ts new file mode 100644 index 00000000..099be2b0 --- /dev/null +++ b/src/components/Form1/src/types/form.ts @@ -0,0 +1,223 @@ +import type { NamePath, RuleObject } from 'ant-design-vue/lib/form/interface' +import type { VNode } from 'vue' +import type { ButtonProps as AntdButtonProps } from '/@/components/Button' +import type { FormItem } from './formItem' +import type { ColEx, ComponentType } from './index' +import type { TableActionType } from '/@/components/Table/src/types/table' +import type { CSSProperties } from 'vue' +import type { RowProps } from 'ant-design-vue/lib/grid/Row' + +export type FieldMapToTime = [string, [string, string], string?][] + +export type Rule = RuleObject & { + trigger?: 'blur' | 'change' | ['change', 'blur'] +} + +export interface RenderCallbackParams { + schema: FormSchema + values: Recordable + model: Recordable + field: string +} + +export interface ButtonProps extends AntdButtonProps { + text?: string +} + +export interface FormActionType { + submit: () => Promise + setFieldsValue: (values: T) => Promise + resetFields: () => Promise + getFieldsValue: () => Recordable + clearValidate: (name?: string | string[]) => Promise + updateSchema: (data: Partial | Partial[]) => Promise + resetSchema: (data: Partial | Partial[]) => Promise + setProps: (formProps: Partial) => Promise + removeSchemaByField: (field: string | string[]) => Promise + appendSchemaByField: ( + schema: FormSchema, + prefixField: string | undefined, + first?: boolean | undefined, + ) => Promise + validateFields: (nameList?: NamePath[]) => Promise + validate: (nameList?: NamePath[]) => Promise + scrollToField: (name: NamePath, options?: ScrollOptions) => Promise +} + +export type RegisterFn = (formInstance: FormActionType) => void + +export type UseFormReturnType = [RegisterFn, FormActionType] + +export interface FormProps { + name?: string + layout?: 'vertical' | 'inline' | 'horizontal' + // Form value + model?: Recordable + // The width of all items in the entire form + labelWidth?: number | string + // alignment + labelAlign?: 'left' | 'right' + // Row configuration for the entire form + rowProps?: RowProps + // Submit form on reset + submitOnReset?: boolean + // Submit form on form changing + submitOnChange?: boolean + // Col configuration for the entire form + labelCol?: Partial + // Col configuration for the entire form + wrapperCol?: Partial + + // General row style + baseRowStyle?: CSSProperties + + // General col configuration + baseColProps?: Partial + + // Form configuration rules + schemas?: FormSchema[] + // Function values used to merge into dynamic control form items + mergeDynamicData?: Recordable + // Compact mode for search forms + compact?: boolean + // Blank line span + emptySpan?: number | Partial + // Internal component size of the form + size?: 'default' | 'small' | 'large' + // Whether to disable + disabled?: boolean + // Time interval fields are mapped into multiple + fieldMapToTime?: FieldMapToTime + // Placeholder is set automatically + autoSetPlaceHolder?: boolean + // Auto submit on press enter on input + autoSubmitOnEnter?: boolean + // Check whether the information is added to the label + rulesMessageJoinLabel?: boolean + // Whether to show collapse and expand buttons + showAdvancedButton?: boolean + // Whether to focus on the first input box, only works when the first form item is input + autoFocusFirstItem?: boolean + // Automatically collapse over the specified number of rows + autoAdvancedLine?: number + // Always show lines + alwaysShowLines?: number + // Whether to show the operation button + showActionButtonGroup?: boolean + + // Reset button configuration + resetButtonOptions?: Partial + + // Confirm button configuration + submitButtonOptions?: Partial + + // Operation column configuration + actionColOptions?: Partial + + // Show reset button + showResetButton?: boolean + // Show confirmation button + showSubmitButton?: boolean + + resetFunc?: () => Promise + submitFunc?: () => Promise + transformDateFunc?: (date: any) => string + colon?: boolean +} +export interface FormSchema { + // Field name + field: string + // Event name triggered by internal value change, default change + changeEvent?: string + // Variable name bound to v-model Default value + valueField?: string + // Label name + label: string | VNode + // Auxiliary text + subLabel?: string + // Help text on the right side of the text + helpMessage?: + | string + | string[] + | ((renderCallbackParams: RenderCallbackParams) => string | string[]) + // BaseHelp component props + helpComponentProps?: Partial + // Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid + labelWidth?: string | number + // Disable the adjustment of labelWidth with global settings of formModel, and manually set labelCol and wrapperCol by yourself + disabledLabelWidth?: boolean + // render component + component: ComponentType + // Component parameters + componentProps?: + | ((opt: { + schema: FormSchema + tableAction: TableActionType + formActionType: FormActionType + formModel: Recordable + }) => Recordable) + | object + // Required + required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean) + + suffix?: string | number | ((values: RenderCallbackParams) => string | number) + + // Validation rules + rules?: Rule[] + // Check whether the information is added to the label + rulesMessageJoinLabel?: boolean + + // Reference formModelItem + itemProps?: Partial + + // col configuration outside formModelItem + colProps?: Partial + + // 默认值 + defaultValue?: any + isAdvanced?: boolean + + // Matching details components + span?: number + + ifShow?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean) + + show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean) + + // Render the content in the form-item tag + render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string + + // Rendering col content requires outer wrapper form-item + renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string + + renderComponentContent?: + | ((renderCallbackParams: RenderCallbackParams) => any) + | VNode + | VNode[] + | string + + // Custom slot, in from-item + slot?: string + + // Custom slot, similar to renderColContent + colSlot?: string + + dynamicDisabled?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean) + + dynamicRules?: (renderCallbackParams: RenderCallbackParams) => Rule[] +} +export interface HelpComponentProps { + maxWidth: string + // Whether to display the serial number + showIndex: boolean + // Text list + text: any + // colour + color: string + // font size + fontSize: string + icon: string + absolute: boolean + // Positioning + position: any +} diff --git a/src/components/Form1/src/types/formItem.ts b/src/components/Form1/src/types/formItem.ts new file mode 100644 index 00000000..77b238ac --- /dev/null +++ b/src/components/Form1/src/types/formItem.ts @@ -0,0 +1,91 @@ +import type { NamePath } from 'ant-design-vue/lib/form/interface'; +import type { ColProps } from 'ant-design-vue/lib/grid/Col'; +import type { HTMLAttributes, VNodeChild } from 'vue'; + +export interface FormItem { + /** + * Used with label, whether to display : after label text. + * @default true + * @type boolean + */ + colon?: boolean; + + /** + * The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time. + * @type any (string | slot) + */ + extra?: string | VNodeChild | JSX.Element; + + /** + * Used with validateStatus, this option specifies the validation status icon. Recommended to be used only with Input. + * @default false + * @type boolean + */ + hasFeedback?: boolean; + + /** + * The prompt message. If not provided, the prompt message will be generated by the validation rule. + * @type any (string | slot) + */ + help?: string | VNodeChild | JSX.Element; + + /** + * Label test + * @type any (string | slot) + */ + label?: string | VNodeChild | JSX.Element; + + /** + * The layout of label. You can set span offset to something like {span: 3, offset: 12} or sm: {span: 3, offset: 12} same as with + * @type Col + */ + labelCol?: ColProps & HTMLAttributes; + + /** + * Whether provided or not, it will be generated by the validation rule. + * @default false + * @type boolean + */ + required?: boolean; + + /** + * The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating' + * @type string + */ + validateStatus?: '' | 'success' | 'warning' | 'error' | 'validating'; + + /** + * The layout for input controls, same as labelCol + * @type Col + */ + wrapperCol?: ColProps; + /** + * Set sub label htmlFor. + */ + htmlFor?: string; + /** + * text align of label + */ + labelAlign?: 'left' | 'right'; + /** + * a key of model. In the setting of validate and resetFields method, the attribute is required + */ + name?: NamePath; + /** + * validation rules of form + */ + rules?: object | object[]; + /** + * Whether to automatically associate form fields. In most cases, you can setting automatic association. + * If the conditions for automatic association are not met, you can manually associate them. See the notes below. + */ + autoLink?: boolean; + /** + * Whether stop validate on first rule of error for this field. + */ + validateFirst?: boolean; + /** + * When to validate the value of children node + */ + validateTrigger?: string | string[] | false; +} diff --git a/src/components/Form1/src/types/hooks.ts b/src/components/Form1/src/types/hooks.ts new file mode 100644 index 00000000..0308e73b --- /dev/null +++ b/src/components/Form1/src/types/hooks.ts @@ -0,0 +1,6 @@ +export interface AdvanceState { + isAdvanced: boolean; + hideAdvanceBtn: boolean; + isLoad: boolean; + actionSpan: number; +} diff --git a/src/components/Form1/src/types/index.ts b/src/components/Form1/src/types/index.ts new file mode 100644 index 00000000..9f577f00 --- /dev/null +++ b/src/components/Form1/src/types/index.ts @@ -0,0 +1,117 @@ +type ColSpanType = number | string +export interface ColEx { + style?: any + /** + * raster number of cells to occupy, 0 corresponds to display: none + * @default none (0) + * @type ColSpanType + */ + span?: ColSpanType + + /** + * raster order, used in flex layout mode + * @default 0 + * @type ColSpanType + */ + order?: ColSpanType + + /** + * the layout fill of flex + * @default none + * @type ColSpanType + */ + flex?: ColSpanType + + /** + * the number of cells to offset Col from the left + * @default 0 + * @type ColSpanType + */ + offset?: ColSpanType + + /** + * the number of cells that raster is moved to the right + * @default 0 + * @type ColSpanType + */ + push?: ColSpanType + + /** + * the number of cells that raster is moved to the left + * @default 0 + * @type ColSpanType + */ + pull?: ColSpanType + + /** + * <576px and also default setting, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + xs?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + + /** + * ≥576px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + sm?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + + /** + * ≥768px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + md?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + + /** + * ≥992px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + lg?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + + /** + * ≥1200px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + xl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType + + /** + * ≥1600px, could be a span value or an object containing above props + * @type { span: ColSpanType, offset: ColSpanType } | ColSpanType + */ + xxl?: { span: ColSpanType; offset: ColSpanType } | ColSpanType +} + +export type ComponentType = + | 'Input' + | 'InputGroup' + | 'InputPassword' + | 'InputSearch' + | 'InputTextArea' + | 'InputNumber' + | 'InputCountDown' + | 'Select' + | 'ApiSelect' + | 'TreeSelect' + | 'ApiTree' + | 'ApiTreeSelect' + | 'ApiRadioGroup' + | 'RadioButtonGroup' + | 'RadioGroup' + | 'Checkbox' + | 'CheckboxGroup' + | 'AutoComplete' + | 'ApiCascader' + | 'Cascader' + | 'DatePicker' + | 'MonthPicker' + | 'RangePicker' + | 'WeekPicker' + | 'TimePicker' + | 'Switch' + | 'StrengthMeter' + | 'Upload' + | 'IconPicker' + | 'Render' + | 'Slider' + | 'Rate' + | 'Divider' + | 'ApiTransfer' diff --git a/src/components/Table/index.ts b/src/components/Table/index.ts index 79540b48..ca538dea 100644 --- a/src/components/Table/index.ts +++ b/src/components/Table/index.ts @@ -7,5 +7,5 @@ export * from './src/types/table' export * from './src/types/pagination' export * from './src/types/tableAction' export { useTable } from './src/hooks/useTable' -export type { FormSchema, FormProps } from '/@/components/Form/src/types/form' +export type { FormSchema, FormProps } from '../Form1/src/types/form' export type { EditRecordRow } from './src/components/editable' diff --git a/src/components/Table/src/componentMap.ts b/src/components/Table/src/componentMap.ts index d1323f64..b185e62f 100644 --- a/src/components/Table/src/componentMap.ts +++ b/src/components/Table/src/componentMap.ts @@ -11,7 +11,7 @@ import { Radio, } from 'ant-design-vue' import type { ComponentType } from './types/componentType' -import { ApiSelect, ApiTreeSelect, RadioButtonGroup, ApiRadioGroup } from '/@/components/Form' +import { ApiSelect, ApiTreeSelect, RadioButtonGroup, ApiRadioGroup } from '../../Form1' const componentMap = new Map() diff --git a/src/components/Table/src/hooks/useTable.ts b/src/components/Table/src/hooks/useTable.ts index 48cb9987..d6300f22 100644 --- a/src/components/Table/src/hooks/useTable.ts +++ b/src/components/Table/src/hooks/useTable.ts @@ -1,7 +1,7 @@ import type { BasicTableProps, TableActionType, FetchParams, BasicColumn } from '../types/table' import type { PaginationProps } from '../types/pagination' import type { DynamicProps } from '/#/utils' -import type { FormActionType } from '/@/components/Form' +import type { FormActionType } from '../../../Form1' import type { WatchStopHandle } from 'vue' import { getDynamicProps } from '/@/utils' import { ref, onUnmounted, unref, watch, toRaw } from 'vue' diff --git a/src/components/Table/src/hooks/useTableForm.ts b/src/components/Table/src/hooks/useTableForm.ts index bc09c7f6..d3733c3e 100644 --- a/src/components/Table/src/hooks/useTableForm.ts +++ b/src/components/Table/src/hooks/useTableForm.ts @@ -1,7 +1,7 @@ import type { ComputedRef, Slots } from 'vue'; import type { BasicTableProps, FetchParams } from '../types/table'; import { unref, computed } from 'vue'; -import type { FormProps } from '/@/components/Form'; +import type { FormProps } from '../../../Form1'; import { isFunction } from '/@/utils/is'; export function useTableForm( diff --git a/src/components/Table/src/props.ts b/src/components/Table/src/props.ts index fcfa0b36..9c2306ec 100644 --- a/src/components/Table/src/props.ts +++ b/src/components/Table/src/props.ts @@ -9,7 +9,7 @@ import type { TableRowSelection, SizeType, } from './types/table' -import type { FormProps } from '/@/components/Form' +import type { FormProps } from '../../Form1' import { DEFAULT_FILTER_FN, DEFAULT_SORT_FN, FETCH_SETTING, DEFAULT_SIZE } from './const' import { propTypes } from '/@/utils/propTypes' diff --git a/src/components/Table/src/types/table.ts b/src/components/Table/src/types/table.ts index 089a7549..65d55ff0 100644 --- a/src/components/Table/src/types/table.ts +++ b/src/components/Table/src/types/table.ts @@ -1,6 +1,6 @@ import type { VNodeChild } from 'vue' import type { PaginationProps } from './pagination' -import type { FormProps } from '/@/components/Form' +import type { FormProps } from '../../../Form1' import type { TableRowSelection as ITableRowSelection } from 'ant-design-vue/lib/table/interface' import type { ColumnProps } from 'ant-design-vue/lib/table' diff --git a/src/hooks/component/useFormItem.ts b/src/hooks/component/useFormItem.ts index 43f55240..29342f9e 100644 --- a/src/hooks/component/useFormItem.ts +++ b/src/hooks/component/useFormItem.ts @@ -48,11 +48,9 @@ export function useRuleFormItem( }, set(value) { if (isEqual(value, defaultState.value)) return - innerState.value = value as T[keyof T] - nextTick(() => { - emit?.(changeEvent, value, ...(toRaw(unref(emitData)) || [])) - }) + emit?.(changeEvent, value, ...(toRaw(unref(emitData)) || [])) + // nextTick(() => {}) }, }) diff --git a/src/views/device/management/DeviceDrawer.vue b/src/views/device/management/DeviceDrawer.vue index 72c97765..b12e744d 100644 --- a/src/views/device/management/DeviceDrawer.vue +++ b/src/views/device/management/DeviceDrawer.vue @@ -7,7 +7,7 @@ width="500px" @ok="handleSubmit" > - +