new-map
ihzero 2022-11-23 18:43:09 +08:00
parent 13f58ed486
commit 591a49805c
61 changed files with 4040 additions and 697 deletions

View File

@ -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 };

View File

@ -10,6 +10,7 @@
<slot name="formHeader"></slot>
<template v-for="schema in getSchema" :key="schema.field">
<FormItem
:isAdvanced="fieldsIsAdvancedMap[schema.field]"
:tableAction="tableAction"
:formActionType="formActionType"
:schema="schema"
@ -118,9 +119,9 @@
const getSchema = computed((): FormSchema[] => {
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any)
for (const schema of schemas) {
const { defaultValue, component } = schema
const { defaultValue, component, isHandleDateDefaultValue = true } = schema
// handle date type
if (defaultValue && dateItemType.includes(component)) {
if (isHandleDateDefaultValue && defaultValue && dateItemType.includes(component)) {
if (!Array.isArray(defaultValue)) {
schema.defaultValue = dateUtil(defaultValue)
} else {
@ -141,7 +142,7 @@
}
})
const { handleToggleAdvanced } = useAdvanced({
const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({
advanceState,
emit,
getProps,
@ -243,6 +244,7 @@
function setFormModel(key: string, value: any) {
formModel[key] = value
const { validateTrigger } = unref(getBindValue)
if (!validateTrigger || validateTrigger === 'change') {
validateFields([key]).catch((_) => {})
@ -299,6 +301,7 @@
getFormActionBindProps: computed(
(): Recordable => ({ ...getProps.value, ...advanceState }),
),
fieldsIsAdvancedMap,
...formActionType,
}
},

View File

@ -1,5 +1,5 @@
import type { Component } from 'vue'
import type { ComponentType } from './types/index'
import type { Component } from 'vue';
import type { ComponentType } from './types/index';
/**
* Component list, register here to setting it in the form
@ -19,65 +19,65 @@ import {
Slider,
Rate,
Divider,
} from 'ant-design-vue'
} 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'
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<ComponentType, Component>()
const componentMap = new Map<ComponentType, Component>();
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('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('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('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)
componentMap.set('Upload', BasicUpload);
componentMap.set('Divider', Divider);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component)
componentMap.set(compName, component);
}
export function del(compName: ComponentType) {
componentMap.delete(compName)
componentMap.delete(compName);
}
export { componentMap }
export { componentMap };

View File

@ -19,20 +19,20 @@
</a-cascader>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, unref, watch, watchEffect } from 'vue'
import { Cascader } from 'ant-design-vue'
import { propTypes } from '/@/utils/propTypes'
import { isFunction } from '/@/utils/is'
import { get, omit } from 'lodash-es'
import { useRuleFormItem } from '/@/hooks/component/useFormItem'
import { LoadingOutlined } from '@ant-design/icons-vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { defineComponent, PropType, ref, unref, watch, watchEffect } from 'vue';
import { Cascader } from 'ant-design-vue';
import { propTypes } from '/@/utils/propTypes';
import { isFunction } from '/@/utils/is';
import { get, omit } from 'lodash-es';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
interface Option {
value: string
label: string
loading?: boolean
isLeaf?: boolean
children?: Option[]
value: string;
label: string;
loading?: boolean;
isLeaf?: boolean;
children?: Option[];
}
export default defineComponent({
name: 'ApiCascader',
@ -71,117 +71,117 @@
},
emits: ['change', 'defaultChange'],
setup(props, { emit }) {
const apiData = ref<any[]>([])
const options = ref<Option[]>([])
const loading = ref<boolean>(false)
const emitData = ref<any[]>([])
const isFirstLoad = ref(true)
const { t } = useI18n()
const apiData = ref<any[]>([]);
const options = ref<Option[]>([]);
const loading = ref<boolean>(false);
const emitData = ref<any[]>([]);
const isFirstLoad = ref(true);
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData)
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
watch(
apiData,
(data) => {
const opts = generatorOptions(data)
options.value = opts
const opts = generatorOptions(data);
options.value = opts;
},
{ deep: true },
)
);
function generatorOptions(options: any[]): Option[] {
const { labelField, valueField, numberToString, childrenField, isLeaf } = props
const { labelField, valueField, numberToString, childrenField, isLeaf } = props;
return options.reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField]
const value = next[valueField];
const item = {
...omit(next, [labelField, valueField]),
label: next[labelField],
value: numberToString ? `${value}` : value,
isLeaf: isLeaf && typeof isLeaf === 'function' ? isLeaf(next) : false,
}
const children = Reflect.get(next, childrenField)
};
const children = Reflect.get(next, childrenField);
if (children) {
Reflect.set(item, childrenField, generatorOptions(children))
Reflect.set(item, childrenField, generatorOptions(children));
}
prev.push(item)
prev.push(item);
}
return prev
}, [] as Option[])
return prev;
}, [] as Option[]);
}
async function initialFetch() {
const api = props.api
if (!api || !isFunction(api)) return
apiData.value = []
loading.value = true
const api = props.api;
if (!api || !isFunction(api)) return;
apiData.value = [];
loading.value = true;
try {
const res = await api(props.initFetchParams)
const res = await api(props.initFetchParams);
if (Array.isArray(res)) {
apiData.value = res
return
apiData.value = res;
return;
}
if (props.resultField) {
apiData.value = get(res, props.resultField) || []
apiData.value = get(res, props.resultField) || [];
}
} catch (error) {
console.warn(error)
console.warn(error);
} finally {
loading.value = false
loading.value = false;
}
}
async function loadData(selectedOptions: Option[]) {
const targetOption = selectedOptions[selectedOptions.length - 1]
targetOption.loading = true
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
const api = props.api
if (!api || !isFunction(api)) return
const api = props.api;
if (!api || !isFunction(api)) return;
try {
const res = await api({
[props.asyncFetchParamKey]: Reflect.get(targetOption, 'value'),
})
});
if (Array.isArray(res)) {
const children = generatorOptions(res)
targetOption.children = children
return
const children = generatorOptions(res);
targetOption.children = children;
return;
}
if (props.resultField) {
const children = generatorOptions(get(res, props.resultField) || [])
targetOption.children = children
const children = generatorOptions(get(res, props.resultField) || []);
targetOption.children = children;
}
} catch (e) {
console.error(e)
console.error(e);
} finally {
targetOption.loading = false
targetOption.loading = false;
}
}
watchEffect(() => {
props.immediate && initialFetch()
})
props.immediate && initialFetch();
});
watch(
() => props.initFetchParams,
() => {
!unref(isFirstLoad) && initialFetch()
!unref(isFirstLoad) && initialFetch();
},
{ deep: true },
)
);
function handleChange(keys, args) {
emitData.value = keys
emit('defaultChange', keys, args)
emitData.value = keys;
emit('defaultChange', keys, args);
}
function handleRenderDisplay({ labels, selectedOptions }) {
if (unref(emitData).length === selectedOptions.length) {
return labels.join(' / ')
return labels.join(' / ');
}
if (props.displayRenderArray) {
return props.displayRenderArray.join(' / ')
return props.displayRenderArray.join(' / ');
}
return ''
return '';
}
return {
@ -192,7 +192,7 @@
handleChange,
loadData,
handleRenderDisplay,
}
};
},
})
});
</script>

View File

@ -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<OptionsItem[]>([])
const loading = ref(false)
@ -91,6 +91,13 @@
props.immediate && !props.alwaysLoad && fetch()
})
watch(
() => state.value,
(v) => {
emit('update:value', v)
},
)
watch(
() => props.params,
() => {

View File

@ -12,13 +12,13 @@
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, unref, watchEffect } from 'vue'
import { Transfer } from 'ant-design-vue'
import { isFunction } from '/@/utils/is'
import { get, omit } from 'lodash-es'
import { propTypes } from '/@/utils/propTypes'
import { useI18n } from '/@/hooks/web/useI18n'
import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer'
import { computed, defineComponent, watch, ref, unref, watchEffect } from 'vue';
import { Transfer } from 'ant-design-vue';
import { isFunction } from '/@/utils/is';
import { get, omit } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes';
import { useI18n } from '/@/hooks/web/useI18n';
import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer';
export default defineComponent({
name: 'ApiTransfer',
components: { Transfer },
@ -47,18 +47,18 @@
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const _dataSource = ref<TransferItem[]>([])
const _targetKeys = ref<string[]>([])
const { t } = useI18n()
const _dataSource = ref<TransferItem[]>([]);
const _targetKeys = ref<string[]>([]);
const { t } = useI18n();
const getAttrs = computed(() => {
return {
...(!props.api ? { dataSource: unref(_dataSource) } : {}),
...attrs,
}
})
};
});
const getdataSource = computed(() => {
const { labelField, valueField } = props
const { labelField, valueField } = props;
return unref(_dataSource).reduce((prev, next: Recordable) => {
if (next) {
@ -66,69 +66,69 @@
...omit(next, [labelField, valueField]),
title: next[labelField],
key: next[valueField],
})
});
}
return prev
}, [] as TransferItem[])
})
return prev;
}, [] as TransferItem[]);
});
const getTargetKeys = computed<string[]>(() => {
if (unref(_targetKeys).length > 0) {
return unref(_targetKeys)
return unref(_targetKeys);
}
if (Array.isArray(props.value)) {
return props.value
return props.value;
}
return []
})
return [];
});
function handleChange(keys: string[], direction: TransferDirection, moveKeys: string[]) {
_targetKeys.value = keys
console.log(direction)
console.log(moveKeys)
emit('change', keys)
_targetKeys.value = keys;
console.log(direction);
console.log(moveKeys);
emit('change', keys);
}
watchEffect(() => {
props.immediate && !props.alwaysLoad && fetch()
})
props.immediate && !props.alwaysLoad && fetch();
});
watch(
() => props.params,
() => {
fetch()
fetch();
},
{ deep: true },
)
);
async function fetch() {
const api = props.api
const api = props.api;
if (!api || !isFunction(api)) {
if (Array.isArray(props.dataSource)) {
_dataSource.value = props.dataSource
_dataSource.value = props.dataSource;
}
return
return;
}
_dataSource.value = []
_dataSource.value = [];
try {
const res = await api(props.params)
const res = await api(props.params);
if (Array.isArray(res)) {
_dataSource.value = res
emitChange()
return
_dataSource.value = res;
emitChange();
return;
}
if (props.resultField) {
_dataSource.value = get(res, props.resultField) || []
_dataSource.value = get(res, props.resultField) || [];
}
emitChange()
emitChange();
} catch (error) {
console.warn(error)
console.warn(error);
} finally {
}
}
function emitChange() {
emit('options-change', unref(getdataSource))
emit('options-change', unref(getdataSource));
}
return { getTargetKeys, getdataSource, t, getAttrs, handleChange }
return { getTargetKeys, getdataSource, t, getAttrs, handleChange };
},
})
});
</script>

View File

@ -10,12 +10,12 @@
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue'
import { Tree } from 'ant-design-vue'
import { isArray, isFunction } from '/@/utils/is'
import { get } from 'lodash-es'
import { propTypes } from '/@/utils/propTypes'
import { LoadingOutlined } from '@ant-design/icons-vue'
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
import { Tree } from 'ant-design-vue';
import { isArray, isFunction } from '/@/utils/is';
import { get } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes';
import { LoadingOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'ApiTree',
components: { ATree: Tree, LoadingOutlined },
@ -28,63 +28,63 @@
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const treeData = ref<Recordable[]>([])
const isFirstLoaded = ref<Boolean>(false)
const loading = ref(false)
const treeData = ref<Recordable[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
}
})
};
});
function handleChange(...args) {
emit('change', ...args)
emit('change', ...args);
}
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch()
!unref(isFirstLoaded) && fetch();
},
{ deep: true },
)
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch()
v && !isFirstLoaded.value && fetch();
},
)
);
onMounted(() => {
props.immediate && fetch()
})
props.immediate && fetch();
});
async function fetch() {
const { api, afterFetch } = props
if (!api || !isFunction(api)) return
loading.value = true
treeData.value = []
let result
const { api, afterFetch } = props;
if (!api || !isFunction(api)) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params)
result = await api(props.params);
} catch (e) {
console.error(e)
console.error(e);
}
if (afterFetch && isFunction(afterFetch)) {
result = afterFetch(result)
result = afterFetch(result);
}
loading.value = false
if (!result) return
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField)
result = get(result, props.resultField);
}
treeData.value = (result as Recordable[]) || []
isFirstLoaded.value = true
emit('options-change', treeData.value)
treeData.value = (result as Recordable[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
return { getAttrs, loading, handleChange }
return { getAttrs, loading, handleChange };
},
})
});
</script>

View File

@ -44,6 +44,9 @@
formActionType: {
type: Object as PropType<FormActionType>,
},
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 <Comp {...compAttr} />
}
@ -291,6 +292,7 @@
: {
default: () => renderComponentContent,
}
return <Comp {...compAttr}>{compSlot}</Comp>
}

View File

@ -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'];

View File

@ -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<FormProps>
getSchema: ComputedRef<FormSchema[]>
formModel: Recordable
defaultValueRef: Ref<Recordable>
advanceState: AdvanceState;
emit: EmitType;
getProps: ComputedRef<FormProps>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
defaultValueRef: Ref<Recordable>;
}
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<ColEx>, 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 };
}

View File

@ -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<Recordable>
export declare type ValidateFields = (nameList?: NamePath[]) => Promise<Recordable>;
type Props = Partial<DynamicProps<FormProps>>
type Props = Partial<DynamicProps<FormProps>>;
export function useForm(props?: Props): UseFormReturnType {
const formRef = ref<Nullable<FormActionType>>(null)
const loadedRef = ref<Nullable<boolean>>(false)
const formRef = ref<Nullable<FormActionType>>(null);
const loadedRef = ref<Nullable<boolean>>(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<FormProps>) => {
const form = await getForm()
form.setProps(formProps)
const form = await getForm();
form.setProps(formProps);
},
updateSchema: async (data: Partial<FormSchema> | Partial<FormSchema>[]) => {
const form = await getForm()
form.updateSchema(data)
const form = await getForm();
form.updateSchema(data);
},
resetSchema: async (data: Partial<FormSchema> | Partial<FormSchema>[]) => {
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: <T>() => {
return unref(formRef)?.getFieldsValue() as T
return unref(formRef)?.getFieldsValue() as T;
},
setFieldsValue: async <T>(values: T) => {
const form = await getForm()
form.setFieldsValue<T>(values)
const form = await getForm();
form.setFieldsValue<T>(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<any> => {
const form = await getForm()
return form.submit()
const form = await getForm();
return form.submit();
},
validate: async (nameList?: NamePath[]): Promise<Recordable> => {
const form = await getForm()
return form.validate(nameList)
const form = await getForm();
return form.validate(nameList);
},
validateFields: async (nameList?: NamePath[]): Promise<Recordable> => {
const form = await getForm()
return form.validateFields(nameList)
const form = await getForm();
return form.validateFields(nameList);
},
}
};
return [register, methods]
return [register, methods];
}

View File

@ -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<FormProps>
getSchema: ComputedRef<FormSchema[]>
formModel: Recordable
defaultValueRef: Ref<Recordable>
formElRef: Ref<FormActionType>
schemaRef: Ref<FormSchema[]>
handleFormValues: Fn
emit: EmitType;
getProps: ComputedRef<FormProps>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
defaultValueRef: Ref<Recordable>;
formElRef: Ref<FormActionType>;
schemaRef: Ref<FormSchema[]>;
handleFormValues: Fn;
}
export function useFormEvents({
emit,
@ -30,22 +30,22 @@ export function useFormEvents({
handleFormValues,
}: UseFormActionContext) {
async function resetFields(): Promise<void> {
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<void> {
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<void> {
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<FormSchema> | Partial<FormSchema>[]) {
let updateData: Partial<FormSchema>[] = []
let updateData: Partial<FormSchema>[] = [];
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<FormSchema> | Partial<FormSchema>[]) {
let updateData: Partial<FormSchema>[] = []
let updateData: Partial<FormSchema>[] = [];
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<void> {
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,
}
};
}

View File

@ -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<any>
getSchema: ComputedRef<FormSchema[]>
getProps: ComputedRef<FormProps>
formModel: Recordable
defaultValueRef: Ref<any>;
getSchema: ComputedRef<FormSchema[]>;
getProps: ComputedRef<FormProps>;
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 };
}

View File

@ -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<FormSchema>, propsRef: Ref<FormProps>) {
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<FormSchema>, propsRef: Ref<
style: { width: layout === 'vertical' ? '100%' : `calc(100% - ${width})` },
...wrapCol,
},
}
})
};
});
}

View File

@ -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<Recordable>,
default: {},
default: () => ({}),
},
// 标签宽度 固定宽度
labelWidth: {
@ -54,7 +54,7 @@ export const basicProps = {
transformDateFunc: {
type: Function as PropType<Fn>,
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<RowProps>,
}
};

View File

@ -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<void>
setFieldsValue: <T>(values: T) => Promise<void>
resetFields: () => Promise<void>
getFieldsValue: () => Recordable
clearValidate: (name?: string | string[]) => Promise<void>
updateSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>
resetSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>
setProps: (formProps: Partial<FormProps>) => Promise<void>
removeSchemaByField: (field: string | string[]) => Promise<void>
submit: () => Promise<void>;
setFieldsValue: <T>(values: T) => Promise<void>;
resetFields: () => Promise<void>;
getFieldsValue: () => Recordable;
clearValidate: (name?: string | string[]) => Promise<void>;
updateSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>;
resetSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>;
setProps: (formProps: Partial<FormProps>) => Promise<void>;
removeSchemaByField: (field: string | string[]) => Promise<void>;
appendSchemaByField: (
schema: FormSchema,
prefixField: string | undefined,
first?: boolean | undefined,
) => Promise<void>
validateFields: (nameList?: NamePath[]) => Promise<any>
validate: (nameList?: NamePath[]) => Promise<any>
scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>
) => Promise<void>;
validateFields: (nameList?: NamePath[]) => Promise<any>;
validate: (nameList?: NamePath[]) => Promise<any>;
scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>;
}
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<ColEx>
labelCol?: Partial<ColEx>;
// Col configuration for the entire form
wrapperCol?: Partial<ColEx>
wrapperCol?: Partial<ColEx>;
// General row style
baseRowStyle?: CSSProperties
baseRowStyle?: CSSProperties;
// General col configuration
baseColProps?: Partial<ColEx>
baseColProps?: Partial<ColEx>;
// 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<ColEx>
emptySpan?: number | Partial<ColEx>;
// 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<ButtonProps>
resetButtonOptions?: Partial<ButtonProps>;
// Confirm button configuration
submitButtonOptions?: Partial<ButtonProps>
submitButtonOptions?: Partial<ButtonProps>;
// Operation column configuration
actionColOptions?: Partial<ColEx>
actionColOptions?: Partial<ColEx>;
// Show reset button
showResetButton?: boolean
showResetButton?: boolean;
// Show confirmation button
showSubmitButton?: boolean
showSubmitButton?: boolean;
resetFunc?: () => Promise<void>
submitFunc?: () => Promise<void>
transformDateFunc?: (date: any) => string
colon?: boolean
resetFunc?: () => Promise<void>;
submitFunc?: () => Promise<void>;
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>
helpComponentProps?: Partial<HelpComponentProps>;
// 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<FormItem>
itemProps?: Partial<FormItem>;
// col configuration outside formModelItem
colProps?: Partial<ColEx>
colProps?: Partial<ColEx>;
// 默认值
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;
}

View File

@ -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';

View File

@ -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 }

View File

@ -0,0 +1,353 @@
<template>
<Form
v-bind="getBindValue"
:class="getFormClass"
ref="formElRef"
:model="formModel"
@keypress.enter="handleEnterPress"
>
<Row v-bind="getRow">
<slot name="formHeader"></slot>
<template v-for="schema in getSchema" :key="schema.field">
<FormItem
:tableAction="tableAction"
:formActionType="formActionType"
:schema="schema"
:formProps="getProps"
:allDefaultValues="defaultValueRef"
:formModel="formModel"
:setFormModel="setFormModel"
>
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</FormItem>
</template>
<FormAction v-bind="getFormActionBindProps" @toggle-advanced="handleToggleAdvanced">
<template
#[item]="data"
v-for="item in ['resetBefore', 'submitBefore', 'advanceBefore', 'advanceAfter']"
>
<slot :name="item" v-bind="data || {}"></slot>
</template>
</FormAction>
<slot name="formFooter"></slot>
</Row>
</Form>
</template>
<script lang="ts">
import type { FormActionType, FormProps, FormSchema } from './types/form'
import type { AdvanceState } from './types/hooks'
import type { Ref } from 'vue'
import { defineComponent, reactive, ref, computed, unref, onMounted, watch, nextTick } from 'vue'
import { Form, Row } from 'ant-design-vue'
import FormItem from './components/FormItem.vue'
import FormAction from './components/FormAction.vue'
import { dateItemType } from './helper'
import { dateUtil } from '/@/utils/dateUtil'
// import { cloneDeep } from 'lodash-es';
import { deepMerge } from '/@/utils'
import { useFormValues } from './hooks/useFormValues'
import useAdvanced from './hooks/useAdvanced'
import { useFormEvents } from './hooks/useFormEvents'
import { createFormContext } from './hooks/useFormContext'
import { useAutoFocus } from './hooks/useAutoFocus'
import { useModalContext } from '/@/components/Modal'
import { useDebounceFn } from '@vueuse/core'
import { basicProps } from './props'
import { useDesign } from '/@/hooks/web/useDesign'
import { cloneDeep } from 'lodash-es'
export default defineComponent({
name: 'BasicForm',
components: { FormItem, Form, Row, FormAction },
props: basicProps,
emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
setup(props, { emit, attrs }) {
const formModel = reactive<Recordable>({})
const modalFn = useModalContext()
const advanceState = reactive<AdvanceState>({
isAdvanced: true,
hideAdvanceBtn: false,
isLoad: false,
actionSpan: 6,
})
const defaultValueRef = ref<Recordable>({})
const isInitedDefaultRef = ref(false)
const propsRef = ref<Partial<FormProps>>({})
const schemaRef = ref<Nullable<FormSchema[]>>(null)
const formElRef = ref<Nullable<FormActionType>>(null)
const { prefixCls } = useDesign('basic-form')
// Get the basic configuration of the form
const getProps = computed((): FormProps => {
return { ...props, ...unref(propsRef) } as FormProps
})
const getFormClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--compact`]: unref(getProps).compact,
},
]
})
// Get uniform row style and Row configuration for the entire form
const getRow = computed((): Recordable => {
const { baseRowStyle = {}, rowProps } = unref(getProps)
return {
style: baseRowStyle,
...rowProps,
}
})
const getBindValue = computed(
() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable),
)
const getSchema = computed((): FormSchema[] => {
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any)
for (const schema of schemas) {
const { defaultValue, component } = schema
// handle date type
if (defaultValue && dateItemType.includes(component)) {
if (!Array.isArray(defaultValue)) {
schema.defaultValue = dateUtil(defaultValue)
} else {
const def: any[] = []
defaultValue.forEach((item) => {
def.push(dateUtil(item))
})
schema.defaultValue = def
}
}
}
if (unref(getProps).showAdvancedButton) {
return cloneDeep(
schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[],
)
} else {
return cloneDeep(schemas as FormSchema[])
}
})
const { handleToggleAdvanced } = useAdvanced({
advanceState,
emit,
getProps,
getSchema,
formModel,
defaultValueRef,
})
const { handleFormValues, initDefault } = useFormValues({
getProps,
defaultValueRef,
getSchema,
formModel,
})
useAutoFocus({
getSchema,
getProps,
isInitedDefault: isInitedDefaultRef,
formElRef: formElRef as Ref<FormActionType>,
})
const {
handleSubmit,
setFieldsValue,
clearValidate,
validate,
validateFields,
getFieldsValue,
updateSchema,
resetSchema,
appendSchemaByField,
removeSchemaByField,
resetFields,
scrollToField,
} = useFormEvents({
emit,
getProps,
formModel,
getSchema,
defaultValueRef,
formElRef: formElRef as Ref<FormActionType>,
schemaRef: schemaRef as Ref<FormSchema[]>,
handleFormValues,
})
createFormContext({
resetAction: resetFields,
submitAction: handleSubmit,
})
watch(
() => unref(getProps).model,
() => {
const { model } = unref(getProps)
if (!model) return
setFieldsValue(model)
},
{
immediate: true,
},
)
watch(
() => unref(getProps).schemas,
(schemas) => {
resetSchema(schemas ?? [])
},
)
watch(
() => getSchema.value,
(schema) => {
nextTick(() => {
// Solve the problem of modal adaptive height calculation when the form is placed in the modal
modalFn?.redoModalHeight?.()
})
if (unref(isInitedDefaultRef)) {
return
}
if (schema?.length) {
initDefault()
isInitedDefaultRef.value = true
}
},
)
watch(
() => formModel,
useDebounceFn(() => {
unref(getProps).submitOnChange && handleSubmit()
}, 300),
{ deep: true },
)
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps)
}
function setFormModel(key: string, value: any) {
formModel[key] = value
const { validateTrigger } = unref(getBindValue)
if (!validateTrigger || validateTrigger === 'change') {
validateFields([key]).catch((_) => {})
}
emit('field-value-change', key, value)
}
function handleEnterPress(e: KeyboardEvent) {
const { autoSubmitOnEnter } = unref(getProps)
if (!autoSubmitOnEnter) return
if (e.key === 'Enter' && e.target && e.target instanceof HTMLElement) {
const target: HTMLElement = e.target as HTMLElement
if (target && target.tagName && target.tagName.toUpperCase() == 'INPUT') {
handleSubmit()
}
}
}
const formActionType: Partial<FormActionType> = {
getFieldsValue,
setFieldsValue,
resetFields,
updateSchema,
resetSchema,
setProps,
removeSchemaByField,
appendSchemaByField,
clearValidate,
validateFields,
validate,
submit: handleSubmit,
scrollToField: scrollToField,
}
onMounted(() => {
initDefault()
emit('register', formActionType)
})
return {
getBindValue,
handleToggleAdvanced,
handleEnterPress,
formModel,
defaultValueRef,
advanceState,
getRow,
getProps,
formElRef,
getSchema,
formActionType: formActionType as any,
setFormModel,
getFormClass,
getFormActionBindProps: computed(
(): Recordable => ({ ...getProps.value, ...advanceState }),
),
...formActionType,
}
},
})
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-form';
.@{prefix-cls} {
.ant-form-item {
&-label label::after {
margin: 0 6px 0 2px;
}
&-with-help {
margin-bottom: 0;
}
&:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
&.suffix-item {
.ant-form-item-children {
display: flex;
}
.ant-form-item-control {
margin-top: 4px;
}
.suffix {
display: inline-flex;
padding-left: 6px;
margin-top: 1px;
line-height: 1;
align-items: center;
}
}
}
.ant-form-explain {
font-size: 14px;
}
&--compact {
.ant-form-item {
margin-bottom: 8px !important;
}
}
}
</style>

View File

@ -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<ComponentType, Component>()
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 }

View File

@ -0,0 +1,198 @@
<template>
<a-cascader
v-model:value="state"
:options="options"
:load-data="loadData"
change-on-select
@change="handleChange"
:displayRender="handleRenderDisplay"
>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
<template #notFoundContent v-if="loading">
<span>
<LoadingOutlined spin class="mr-1" />
{{ t('component.form.apiSelectNotFound') }}
</span>
</template>
</a-cascader>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, unref, watch, watchEffect } from 'vue'
import { Cascader } from 'ant-design-vue'
import { propTypes } from '/@/utils/propTypes'
import { isFunction } from '/@/utils/is'
import { get, omit } from 'lodash-es'
import { useRuleFormItem } from '/@/hooks/component/useFormItem'
import { LoadingOutlined } from '@ant-design/icons-vue'
import { useI18n } from '/@/hooks/web/useI18n'
interface Option {
value: string
label: string
loading?: boolean
isLeaf?: boolean
children?: Option[]
}
export default defineComponent({
name: 'ApiCascader',
components: {
LoadingOutlined,
[Cascader.name]: Cascader,
},
props: {
value: {
type: Array,
},
api: {
type: Function as PropType<(arg?: Recordable) => Promise<Option[]>>,
default: null,
},
numberToString: propTypes.bool,
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
childrenField: propTypes.string.def('children'),
asyncFetchParamKey: propTypes.string.def('parentCode'),
immediate: propTypes.bool.def(true),
// init fetch params
initFetchParams: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
//
isLeaf: {
type: Function as PropType<(arg: Recordable) => boolean>,
default: null,
},
displayRenderArray: {
type: Array,
},
},
emits: ['change', 'defaultChange'],
setup(props, { emit }) {
const apiData = ref<any[]>([])
const options = ref<Option[]>([])
const loading = ref<boolean>(false)
const emitData = ref<any[]>([])
const isFirstLoad = ref(true)
const { t } = useI18n()
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData)
watch(
apiData,
(data) => {
const opts = generatorOptions(data)
options.value = opts
},
{ deep: true },
)
function generatorOptions(options: any[]): Option[] {
const { labelField, valueField, numberToString, childrenField, isLeaf } = props
return options.reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField]
const item = {
...omit(next, [labelField, valueField]),
label: next[labelField],
value: numberToString ? `${value}` : value,
isLeaf: isLeaf && typeof isLeaf === 'function' ? isLeaf(next) : false,
}
const children = Reflect.get(next, childrenField)
if (children) {
Reflect.set(item, childrenField, generatorOptions(children))
}
prev.push(item)
}
return prev
}, [] as Option[])
}
async function initialFetch() {
const api = props.api
if (!api || !isFunction(api)) return
apiData.value = []
loading.value = true
try {
const res = await api(props.initFetchParams)
if (Array.isArray(res)) {
apiData.value = res
return
}
if (props.resultField) {
apiData.value = get(res, props.resultField) || []
}
} catch (error) {
console.warn(error)
} finally {
loading.value = false
}
}
async function loadData(selectedOptions: Option[]) {
const targetOption = selectedOptions[selectedOptions.length - 1]
targetOption.loading = true
const api = props.api
if (!api || !isFunction(api)) return
try {
const res = await api({
[props.asyncFetchParamKey]: Reflect.get(targetOption, 'value'),
})
if (Array.isArray(res)) {
const children = generatorOptions(res)
targetOption.children = children
return
}
if (props.resultField) {
const children = generatorOptions(get(res, props.resultField) || [])
targetOption.children = children
}
} catch (e) {
console.error(e)
} finally {
targetOption.loading = false
}
}
watchEffect(() => {
props.immediate && initialFetch()
})
watch(
() => props.initFetchParams,
() => {
!unref(isFirstLoad) && initialFetch()
},
{ deep: true },
)
function handleChange(keys, args) {
emitData.value = keys
emit('defaultChange', keys, args)
}
function handleRenderDisplay({ labels, selectedOptions }) {
if (unref(emitData).length === selectedOptions.length) {
return labels.join(' / ')
}
if (props.displayRenderArray) {
return props.displayRenderArray.join(' / ')
}
return ''
}
return {
state,
options,
loading,
t,
handleChange,
loadData,
handleRenderDisplay,
}
},
})
</script>

View File

@ -0,0 +1,130 @@
<!--
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
-->
<template>
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid" @change="handleChange">
<template v-for="item in getOptions" :key="`${item.value}`">
<RadioButton v-if="props.isBtn" :value="item.value" :disabled="item.disabled">
{{ item.label }}
</RadioButton>
<Radio v-else :value="item.value" :disabled="item.disabled">
{{ item.label }}
</Radio>
</template>
</RadioGroup>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue';
import { Radio } from 'ant-design-vue';
import { isFunction } from '/@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { propTypes } from '/@/utils/propTypes';
import { get, omit } from 'lodash-es';
import { useI18n } from '/@/hooks/web/useI18n';
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
export default defineComponent({
name: 'ApiRadioGroup',
components: {
RadioGroup: Radio.Group,
RadioButton: Radio.Button,
Radio,
},
props: {
api: {
type: Function as PropType<(arg?: Recordable | string) => Promise<OptionsItem[]>>,
default: null,
},
params: {
type: [Object, String] as PropType<Recordable | string>,
default: () => ({}),
},
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
},
isBtn: {
type: [Boolean] as PropType<boolean>,
default: false,
},
numberToString: propTypes.bool,
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
},
emits: ['options-change', 'change'],
setup(props, { emit }) {
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const isFirstLoad = ref(true);
const emitData = ref<any[]>([]);
const attrs = useAttrs();
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props);
// Processing options value
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
return unref(options).reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField];
prev.push({
label: next[labelField],
value: numberToString ? `${value}` : value,
...omit(next, [labelField, valueField]),
});
}
return prev;
}, [] as OptionsItem[]);
});
watchEffect(() => {
props.immediate && fetch();
});
watch(
() => props.params,
() => {
!unref(isFirstLoad) && fetch();
},
{ deep: true },
);
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
options.value = [];
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
emitChange();
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
emitChange();
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
function emitChange() {
emit('options-change', unref(getOptions));
}
function handleChange(_, ...args) {
emitData.value = args;
}
return { state, getOptions, attrs, loading, t, handleChange, props };
},
});
</script>

View File

@ -0,0 +1,147 @@
<template>
<Select
@dropdown-visible-change="handleFetch"
v-bind="$attrs"
@change="handleChange"
:options="getOptions"
v-model:value="state"
>
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
<template #notFoundContent v-if="loading">
<span>
<LoadingOutlined spin class="mr-1" />
{{ t('component.form.apiSelectNotFound') }}
</span>
</template>
</Select>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue'
import { Select } from 'ant-design-vue'
import { isFunction } from '/@/utils/is'
import { useRuleFormItem } from '/@/hooks/component/useFormItem'
import { useAttrs } from '/@/hooks/core/useAttrs'
import { get, omit } from 'lodash-es'
import { LoadingOutlined } from '@ant-design/icons-vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { propTypes } from '/@/utils/propTypes'
type OptionsItem = { label: string; value: string; disabled?: boolean }
export default defineComponent({
name: 'ApiSelect',
components: {
Select,
LoadingOutlined,
},
inheritAttrs: false,
props: {
value: [Array, Object, String, Number],
numberToString: propTypes.bool,
api: {
type: Function as PropType<(arg?: Recordable) => Promise<OptionsItem[]>>,
default: null,
},
// api params
params: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
// support xxx.xxx.xx
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true),
alwaysLoad: propTypes.bool.def(false),
},
emits: ['options-change', 'change'],
setup(props, { emit }) {
const options = ref<OptionsItem[]>([])
const loading = ref(false)
const isFirstLoad = ref(true)
const emitData = ref<any[]>([])
const attrs = useAttrs()
const { t } = useI18n()
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData)
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props
return unref(options).reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField]
prev.push({
...omit(next, [labelField, valueField]),
label: next[labelField],
value: numberToString ? `${value}` : value,
})
}
return prev
}, [] as OptionsItem[])
})
watchEffect(() => {
props.immediate && !props.alwaysLoad && fetch()
})
watch(
() => props.params,
() => {
!unref(isFirstLoad) && fetch()
},
{ deep: true },
)
async function fetch() {
const api = props.api
if (!api || !isFunction(api)) return
options.value = []
try {
loading.value = true
const res = await api(props.params)
if (Array.isArray(res)) {
options.value = res
emitChange()
return
}
if (props.resultField) {
options.value = get(res, props.resultField) || []
}
emitChange()
} catch (error) {
console.warn(error)
} finally {
loading.value = false
}
}
async function handleFetch(visible) {
if (visible) {
if (props.alwaysLoad) {
await fetch()
} else if (!props.immediate && unref(isFirstLoad)) {
await fetch()
isFirstLoad.value = false
}
}
}
function emitChange() {
emit('options-change', unref(getOptions))
}
function handleChange(_, ...args) {
emitData.value = args
}
return { state, attrs, getOptions, loading, t, handleFetch, handleChange }
},
})
</script>

View File

@ -0,0 +1,134 @@
<template>
<Transfer
:data-source="getdataSource"
:filter-option="filterOption"
:render="(item) => item.title"
:showSelectAll="showSelectAll"
:selectedKeys="selectedKeys"
:targetKeys="getTargetKeys"
:showSearch="showSearch"
@change="handleChange"
/>
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, unref, watchEffect } from 'vue'
import { Transfer } from 'ant-design-vue'
import { isFunction } from '/@/utils/is'
import { get, omit } from 'lodash-es'
import { propTypes } from '/@/utils/propTypes'
import { useI18n } from '/@/hooks/web/useI18n'
import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer'
export default defineComponent({
name: 'ApiTransfer',
components: { Transfer },
props: {
value: { type: Array as PropType<Array<string>> },
api: {
type: Function as PropType<(arg?: Recordable) => Promise<TransferItem[]>>,
default: null,
},
params: { type: Object },
dataSource: { type: Array as PropType<Array<TransferItem>> },
immediate: propTypes.bool.def(true),
alwaysLoad: propTypes.bool.def(false),
afterFetch: { type: Function as PropType<Fn> },
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('title'),
valueField: propTypes.string.def('key'),
showSearch: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
filterOption: {
type: Function as PropType<(inputValue: string, item: TransferItem) => boolean>,
},
selectedKeys: { type: Array as PropType<Array<string>> },
showSelectAll: { type: Boolean, default: false },
targetKeys: { type: Array as PropType<Array<string>> },
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const _dataSource = ref<TransferItem[]>([])
const _targetKeys = ref<string[]>([])
const { t } = useI18n()
const getAttrs = computed(() => {
return {
...(!props.api ? { dataSource: unref(_dataSource) } : {}),
...attrs,
}
})
const getdataSource = computed(() => {
const { labelField, valueField } = props
return unref(_dataSource).reduce((prev, next: Recordable) => {
if (next) {
prev.push({
...omit(next, [labelField, valueField]),
title: next[labelField],
key: next[valueField],
})
}
return prev
}, [] as TransferItem[])
})
const getTargetKeys = computed<string[]>(() => {
if (unref(_targetKeys).length > 0) {
return unref(_targetKeys)
}
if (Array.isArray(props.value)) {
return props.value
}
return []
})
function handleChange(keys: string[], direction: TransferDirection, moveKeys: string[]) {
_targetKeys.value = keys
console.log(direction)
console.log(moveKeys)
emit('change', keys)
}
watchEffect(() => {
props.immediate && !props.alwaysLoad && fetch()
})
watch(
() => props.params,
() => {
fetch()
},
{ deep: true },
)
async function fetch() {
const api = props.api
if (!api || !isFunction(api)) {
if (Array.isArray(props.dataSource)) {
_dataSource.value = props.dataSource
}
return
}
_dataSource.value = []
try {
const res = await api(props.params)
if (Array.isArray(res)) {
_dataSource.value = res
emitChange()
return
}
if (props.resultField) {
_dataSource.value = get(res, props.resultField) || []
}
emitChange()
} catch (error) {
console.warn(error)
} finally {
}
}
function emitChange() {
emit('options-change', unref(getdataSource))
}
return { getTargetKeys, getdataSource, t, getAttrs, handleChange }
},
})
</script>

View File

@ -0,0 +1,90 @@
<template>
<a-tree v-bind="getAttrs" @change="handleChange">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
</a-tree>
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue'
import { Tree } from 'ant-design-vue'
import { isArray, isFunction } from '/@/utils/is'
import { get } from 'lodash-es'
import { propTypes } from '/@/utils/propTypes'
import { LoadingOutlined } from '@ant-design/icons-vue'
export default defineComponent({
name: 'ApiTree',
components: { ATree: Tree, LoadingOutlined },
props: {
api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
params: { type: Object },
immediate: { type: Boolean, default: true },
resultField: propTypes.string.def(''),
afterFetch: { type: Function as PropType<Fn> },
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const treeData = ref<Recordable[]>([])
const isFirstLoaded = ref<Boolean>(false)
const loading = ref(false)
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
}
})
function handleChange(...args) {
emit('change', ...args)
}
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch()
},
{ deep: true },
)
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch()
},
)
onMounted(() => {
props.immediate && fetch()
})
async function fetch() {
const { api, afterFetch } = props
if (!api || !isFunction(api)) return
loading.value = true
treeData.value = []
let result
try {
result = await api(props.params)
} catch (e) {
console.error(e)
}
if (afterFetch && isFunction(afterFetch)) {
result = afterFetch(result)
}
loading.value = false
if (!result) return
if (!isArray(result)) {
result = get(result, props.resultField)
}
treeData.value = (result as Recordable[]) || []
isFirstLoaded.value = true
emit('options-change', treeData.value)
}
return { getAttrs, loading, handleChange }
},
})
</script>

View File

@ -0,0 +1,86 @@
<template>
<a-tree-select v-bind="getAttrs" @change="handleChange">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
</a-tree-select>
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, onMounted, unref } from 'vue';
import { TreeSelect } from 'ant-design-vue';
import { isArray, isFunction } from '/@/utils/is';
import { get } from 'lodash-es';
import { propTypes } from '/@/utils/propTypes';
import { LoadingOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'ApiTreeSelect',
components: { ATreeSelect: TreeSelect, LoadingOutlined },
props: {
api: { type: Function as PropType<(arg?: Recordable) => Promise<Recordable>> },
params: { type: Object },
immediate: { type: Boolean, default: true },
resultField: propTypes.string.def(''),
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const treeData = ref<Recordable[]>([]);
const isFirstLoaded = ref<Boolean>(false);
const loading = ref(false);
const getAttrs = computed(() => {
return {
...(props.api ? { treeData: unref(treeData) } : {}),
...attrs,
};
});
function handleChange(...args) {
emit('change', ...args);
}
watch(
() => props.params,
() => {
!unref(isFirstLoaded) && fetch();
},
{ deep: true },
);
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch();
},
);
onMounted(() => {
props.immediate && fetch();
});
async function fetch() {
const { api } = props;
if (!api || !isFunction(api)) return;
loading.value = true;
treeData.value = [];
let result;
try {
result = await api(props.params);
} catch (e) {
console.error(e);
}
loading.value = false;
if (!result) return;
if (!isArray(result)) {
result = get(result, props.resultField);
}
treeData.value = (result as Recordable[]) || [];
isFirstLoaded.value = true;
emit('options-change', treeData.value);
}
return { getAttrs, loading, handleChange };
},
});
</script>

View File

@ -0,0 +1,135 @@
<template>
<a-col v-bind="actionColOpt" v-if="showActionButtonGroup">
<div style="width: 100%" :style="{ textAlign: actionColOpt.style.textAlign }">
<FormItem>
<slot name="resetBefore"></slot>
<Button
type="default"
class="mr-2"
v-bind="getResetBtnOptions"
@click="resetAction"
v-if="showResetButton"
>
{{ getResetBtnOptions.text }}
</Button>
<slot name="submitBefore"></slot>
<Button
type="primary"
class="mr-2"
v-bind="getSubmitBtnOptions"
@click="submitAction"
v-if="showSubmitButton"
>
{{ getSubmitBtnOptions.text }}
</Button>
<slot name="advanceBefore"></slot>
<Button
type="link"
size="small"
@click="toggleAdvanced"
v-if="showAdvancedButton && !hideAdvanceBtn"
>
{{ isAdvanced ? t('component.form.putAway') : t('component.form.unfold') }}
<BasicArrow class="ml-1" :expand="!isAdvanced" up />
</Button>
<slot name="advanceAfter"></slot>
</FormItem>
</div>
</a-col>
</template>
<script lang="ts">
import type { ColEx } from '../types/index';
//import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import { defineComponent, computed, PropType } from 'vue';
import { Form, Col } from 'ant-design-vue';
import { Button, ButtonProps } from '/@/components/Button';
import { BasicArrow } from '/@/components/Basic';
import { useFormContext } from '../hooks/useFormContext';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';
type ButtonOptions = Partial<ButtonProps> & { text: string };
export default defineComponent({
name: 'BasicFormAction',
components: {
FormItem: Form.Item,
Button,
BasicArrow,
[Col.name]: Col,
},
props: {
showActionButtonGroup: propTypes.bool.def(true),
showResetButton: propTypes.bool.def(true),
showSubmitButton: propTypes.bool.def(true),
showAdvancedButton: propTypes.bool.def(true),
resetButtonOptions: {
type: Object as PropType<ButtonOptions>,
default: () => ({}),
},
submitButtonOptions: {
type: Object as PropType<ButtonOptions>,
default: () => ({}),
},
actionColOptions: {
type: Object as PropType<Partial<ColEx>>,
default: () => ({}),
},
actionSpan: propTypes.number.def(6),
isAdvanced: propTypes.bool,
hideAdvanceBtn: propTypes.bool,
},
emits: ['toggle-advanced'],
setup(props, { emit }) {
const { t } = useI18n();
const actionColOpt = computed(() => {
const { showAdvancedButton, actionSpan: span, actionColOptions } = props;
const actionSpan = 24 - span;
const advancedSpanObj = showAdvancedButton
? { span: actionSpan < 6 ? 24 : actionSpan }
: {};
const actionColOpt: Partial<ColEx> = {
style: { textAlign: 'right' },
span: showAdvancedButton ? 6 : 4,
...advancedSpanObj,
...actionColOptions,
};
return actionColOpt;
});
const getResetBtnOptions = computed((): ButtonOptions => {
return Object.assign(
{
text: t('common.resetText'),
},
props.resetButtonOptions,
);
});
const getSubmitBtnOptions = computed(() => {
return Object.assign(
{
text: t('common.queryText'),
},
props.submitButtonOptions,
);
});
function toggleAdvanced() {
emit('toggle-advanced');
}
return {
t,
actionColOpt,
getResetBtnOptions,
getSubmitBtnOptions,
toggleAdvanced,
...useFormContext(),
};
},
});
</script>

View File

@ -0,0 +1,390 @@
<script lang="tsx">
import type { PropType, Ref } from 'vue'
import { computed, defineComponent, toRefs, unref } from 'vue'
import type { FormActionType, FormProps, FormSchema } from '../types/form'
import type { ValidationRule } from 'ant-design-vue/lib/form/Form'
import type { TableActionType } from '/@/components/Table'
import { Col, Divider, Form } from 'ant-design-vue'
import { componentMap } from '../componentMap'
import { BasicHelp } from '/@/components/Basic'
import { isBoolean, isFunction, isNull } from '/@/utils/is'
import { getSlot } from '/@/utils/helper/tsxHelper'
import { createPlaceholderMessage, setComponentRuleType } from '../helper'
import { cloneDeep, upperFirst } from 'lodash-es'
import { useItemLabelWidth } from '../hooks/useLabelWidth'
import { useI18n } from '/@/hooks/web/useI18n'
export default defineComponent({
name: 'BasicFormItem',
inheritAttrs: false,
props: {
schema: {
type: Object as PropType<FormSchema>,
default: () => ({}),
},
formProps: {
type: Object as PropType<FormProps>,
default: () => ({}),
},
allDefaultValues: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
formModel: {
type: Object as PropType<Recordable>,
default: () => ({}),
},
setFormModel: {
type: Function as PropType<(key: string, value: any) => void>,
default: null,
},
tableAction: {
type: Object as PropType<TableActionType>,
},
formActionType: {
type: Object as PropType<FormActionType>,
},
},
setup(props, { slots }) {
const { t } = useI18n()
const { schema, formProps } = toRefs(props) as {
schema: Ref<FormSchema>
formProps: Ref<FormProps>
}
const itemLabelWidthProp = useItemLabelWidth(schema, formProps)
const getValues = computed(() => {
const { allDefaultValues, formModel, schema } = props
const { mergeDynamicData } = props.formProps
return {
field: schema.field,
model: formModel,
values: {
...mergeDynamicData,
...allDefaultValues,
...formModel,
} as Recordable,
schema: schema,
}
})
const getComponentsProps = computed(() => {
const { schema, tableAction, formModel, formActionType } = props
let { componentProps = {} } = schema
if (isFunction(componentProps)) {
componentProps = componentProps({ schema, tableAction, formModel, formActionType }) ?? {}
}
if (schema.component === 'Divider') {
componentProps = Object.assign({ type: 'horizontal' }, componentProps, {
orientation: 'left',
plain: true,
})
}
return componentProps as Recordable
})
const getDisable = computed(() => {
const { disabled: globDisabled } = props.formProps
const { dynamicDisabled } = props.schema
const { disabled: itemDisabled = false } = unref(getComponentsProps)
let disabled = !!globDisabled || itemDisabled
if (isBoolean(dynamicDisabled)) {
disabled = dynamicDisabled
}
if (isFunction(dynamicDisabled)) {
disabled = dynamicDisabled(unref(getValues))
}
return disabled
})
function getShow(): { isShow: boolean; isIfShow: boolean } {
const { show, ifShow } = props.schema
const { showAdvancedButton } = props.formProps
const itemIsAdvanced = showAdvancedButton
? isBoolean(props.schema.isAdvanced)
? props.schema.isAdvanced
: true
: true
let isShow = true
let isIfShow = true
if (isBoolean(show)) {
isShow = show
}
if (isBoolean(ifShow)) {
isIfShow = ifShow
}
if (isFunction(show)) {
isShow = show(unref(getValues))
}
if (isFunction(ifShow)) {
isIfShow = ifShow(unref(getValues))
}
isShow = isShow && itemIsAdvanced
return { isShow, isIfShow }
}
function handleRules(): ValidationRule[] {
const {
rules: defRules = [],
component,
rulesMessageJoinLabel,
label,
dynamicRules,
required,
} = props.schema
if (isFunction(dynamicRules)) {
return dynamicRules(unref(getValues)) as ValidationRule[]
}
let rules: ValidationRule[] = cloneDeep(defRules) as ValidationRule[]
const { rulesMessageJoinLabel: globalRulesMessageJoinLabel } = props.formProps
const joinLabel = Reflect.has(props.schema, 'rulesMessageJoinLabel')
? rulesMessageJoinLabel
: globalRulesMessageJoinLabel
const defaultMsg = createPlaceholderMessage(component) + `${joinLabel ? label : ''}`
function validator(rule: any, value: any) {
const msg = rule.message || defaultMsg
if (value === undefined || isNull(value)) {
//
return Promise.reject(msg)
} else if (Array.isArray(value) && value.length === 0) {
//
return Promise.reject(msg)
} else if (typeof value === 'string' && value.trim() === '') {
//
return Promise.reject(msg)
} else if (
typeof value === 'object' &&
Reflect.has(value, 'checked') &&
Reflect.has(value, 'halfChecked') &&
Array.isArray(value.checked) &&
Array.isArray(value.halfChecked) &&
value.checked.length === 0 &&
value.halfChecked.length === 0
) {
// tree
return Promise.reject(msg)
}
return Promise.resolve()
}
const getRequired = isFunction(required) ? required(unref(getValues)) : required
/*
* 1若设置了required属性又没有其他的rules就创建一个验证规则
* 2若设置了required属性又存在其他的rules则只rules中不存在required属性时才添加验证required的规则
* 也就是说rules中的required优先级大于required
*/
if (getRequired) {
if (!rules || rules.length === 0) {
rules = [{ required: getRequired, validator }]
} else {
const requiredIndex: number = rules.findIndex((rule) => Reflect.has(rule, 'required'))
if (requiredIndex === -1) {
rules.push({ required: getRequired, validator })
}
}
}
const requiredRuleIndex: number = rules.findIndex(
(rule) => Reflect.has(rule, 'required') && !Reflect.has(rule, 'validator'),
)
if (requiredRuleIndex !== -1) {
const rule = rules[requiredRuleIndex]
const { isShow } = getShow()
if (!isShow) {
rule.required = false
}
if (component) {
if (!Reflect.has(rule, 'type')) {
rule.type = component === 'InputNumber' ? 'number' : 'string'
}
rule.message = rule.message || defaultMsg
if (component.includes('Input') || component.includes('Textarea')) {
rule.whitespace = true
}
const valueFormat = unref(getComponentsProps)?.valueFormat
setComponentRuleType(rule, component, valueFormat)
}
}
// Maximum input length rule check
const characterInx = rules.findIndex((val) => val.max)
if (characterInx !== -1 && !rules[characterInx].validator) {
rules[characterInx].message =
rules[characterInx].message ||
t('component.form.maxTip', [rules[characterInx].max] as Recordable)
}
return rules
}
function renderComponent() {
const {
renderComponentContent,
component,
field,
changeEvent = 'change',
valueField,
} = props.schema
const isCheck = component && ['Switch', 'Checkbox'].includes(component)
const eventKey = `on${upperFirst(changeEvent)}`
const on = {
[eventKey]: (...args: Nullable<Recordable>[]) => {
const [e] = args
if (propsData[eventKey]) {
propsData[eventKey](...args)
}
const target = e ? e.target : null
const value = target ? (isCheck ? target.checked : target.value) : e
props.setFormModel(field, value)
},
}
const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>
const { autoSetPlaceHolder, size } = props.formProps
const propsData: Recordable = {
allowClear: true,
getPopupContainer: (trigger: Element) => trigger.parentNode,
size,
...unref(getComponentsProps),
disabled: unref(getDisable),
}
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder
// RangePicker place is an array
if (isCreatePlaceholder && component !== 'RangePicker' && component) {
propsData.placeholder =
unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component)
}
propsData.codeField = field
propsData.formValues = unref(getValues)
const bindValue: Recordable = {
[valueField || (isCheck ? 'checked' : 'value')]: props.formModel[field],
}
const compAttr: Recordable = {
...propsData,
...on,
...bindValue,
}
if (!renderComponentContent) {
return <Comp {...compAttr} />
}
const compSlot = isFunction(renderComponentContent)
? { ...renderComponentContent(unref(getValues)) }
: {
default: () => renderComponentContent,
}
return <Comp {...compAttr}>{compSlot}</Comp>
}
function renderLabelHelpMessage() {
const { label, helpMessage, helpComponentProps, subLabel } = props.schema
const renderLabel = subLabel ? (
<span>
{label} <span class="text-secondary">{subLabel}</span>
</span>
) : (
label
)
const getHelpMessage = isFunction(helpMessage) ? helpMessage(unref(getValues)) : helpMessage
if (!getHelpMessage || (Array.isArray(getHelpMessage) && getHelpMessage.length === 0)) {
return renderLabel
}
return (
<span>
{renderLabel}
<BasicHelp placement="top" class="mx-1" text={getHelpMessage} {...helpComponentProps} />
</span>
)
}
function renderItem() {
const { itemProps, slot, render, field, suffix, component } = props.schema
const { labelCol, wrapperCol } = unref(itemLabelWidthProp)
const { colon } = props.formProps
if (component === 'Divider') {
return (
<Col span={24}>
<Divider {...unref(getComponentsProps)}>{renderLabelHelpMessage()}</Divider>
</Col>
)
} else {
const getContent = () => {
return slot
? getSlot(slots, slot, unref(getValues))
: render
? render(unref(getValues))
: renderComponent()
}
const showSuffix = !!suffix
const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix
return (
<Form.Item
name={field}
colon={colon}
class={{ 'suffix-item': showSuffix }}
{...(itemProps as Recordable)}
label={renderLabelHelpMessage()}
rules={handleRules()}
labelCol={labelCol}
wrapperCol={wrapperCol}
>
<div style="display:flex">
<div style="flex:1;">{getContent()}</div>
{showSuffix && <span class="suffix">{getSuffix}</span>}
</div>
</Form.Item>
)
}
}
return () => {
const { colProps = {}, colSlot, renderColContent, component } = props.schema
if (!componentMap.has(component)) {
return null
}
const { baseColProps = {} } = props.formProps
const realColProps = { ...baseColProps, ...colProps }
const { isIfShow, isShow } = getShow()
const values = unref(getValues)
const getContent = () => {
return colSlot
? getSlot(slots, colSlot, values)
: renderColContent
? renderColContent(values)
: renderItem()
}
return (
isIfShow && (
<Col {...realColProps} v-show={isShow}>
{getContent()}
</Col>
)
)
}
},
})
</script>

View File

@ -0,0 +1,57 @@
<!--
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
-->
<template>
<RadioGroup v-bind="attrs" v-model:value="state" button-style="solid">
<template v-for="item in getOptions" :key="`${item.value}`">
<RadioButton :value="item.value" :disabled="item.disabled">
{{ item.label }}
</RadioButton>
</template>
</RadioGroup>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue';
import { Radio } from 'ant-design-vue';
import { isString } from '/@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
type RadioItem = string | OptionsItem;
export default defineComponent({
name: 'RadioButtonGroup',
components: {
RadioGroup: Radio.Group,
RadioButton: Radio.Button,
},
props: {
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
},
options: {
type: Array as PropType<RadioItem[]>,
default: () => [],
},
},
setup(props) {
const attrs = useAttrs();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props);
// Processing options value
const getOptions = computed((): OptionsItem[] => {
const { options } = props;
if (!options || options?.length === 0) return [];
const isStringArr = options.some((item) => isString(item));
if (!isStringArr) return options as OptionsItem[];
return options.map((item) => ({ label: item, value: item })) as OptionsItem[];
});
return { state, getOptions, attrs };
},
});
</script>

View File

@ -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']

View File

@ -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<FormProps>
getSchema: ComputedRef<FormSchema[]>
formModel: Recordable
defaultValueRef: Ref<Recordable>
}
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<ColEx>, 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 }
}

View File

@ -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<FormSchema[]>;
getProps: ComputedRef<FormProps>;
isInitedDefault: Ref<boolean>;
formElRef: Ref<FormActionType>;
}
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<HTMLInputElement>;
if (!inputEl) return;
inputEl?.focus();
});
}

View File

@ -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);
});
}

View File

@ -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<Recordable>
type Props = Partial<DynamicProps<FormProps>>
export function useForm(props?: Props): UseFormReturnType {
const formRef = ref<Nullable<FormActionType>>(null)
const loadedRef = ref<Nullable<boolean>>(false)
async function getForm() {
const form = unref(formRef)
if (!form) {
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<FormProps>) => {
const form = await getForm()
form.setProps(formProps)
},
updateSchema: async (data: Partial<FormSchema> | Partial<FormSchema>[]) => {
const form = await getForm()
form.updateSchema(data)
},
resetSchema: async (data: Partial<FormSchema> | Partial<FormSchema>[]) => {
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: <T>() => {
return unref(formRef)?.getFieldsValue() as T
},
setFieldsValue: async <T>(values: T) => {
const form = await getForm()
form.setFieldsValue<T>(values)
},
appendSchemaByField: async (
schema: FormSchema,
prefixField: string | undefined,
first: boolean,
) => {
const form = await getForm()
form.appendSchemaByField(schema, prefixField, first)
},
submit: async (): Promise<any> => {
const form = await getForm()
return form.submit()
},
validate: async (nameList?: NamePath[]): Promise<Recordable> => {
const form = await getForm()
return form.validate(nameList)
},
validateFields: async (nameList?: NamePath[]): Promise<Recordable> => {
const form = await getForm()
return form.validateFields(nameList)
},
}
return [register, methods]
}

View File

@ -0,0 +1,17 @@
import type { InjectionKey } from 'vue';
import { createContext, useContext } from '/@/hooks/core/useContext';
export interface FormContextProps {
resetAction: () => Promise<void>;
submitAction: () => Promise<void>;
}
const key: InjectionKey<FormContextProps> = Symbol();
export function createFormContext(context: FormContextProps) {
return createContext<FormContextProps>(context, key);
}
export function useFormContext() {
return useContext<FormContextProps>(key);
}

View File

@ -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<FormProps>
getSchema: ComputedRef<FormSchema[]>
formModel: Recordable
defaultValueRef: Ref<Recordable>
formElRef: Ref<FormActionType>
schemaRef: Ref<FormSchema[]>
handleFormValues: Fn
}
export function useFormEvents({
emit,
getProps,
formModel,
getSchema,
defaultValueRef,
formElRef,
schemaRef,
handleFormValues,
}: UseFormActionContext) {
async function resetFields(): Promise<void> {
const { resetFunc, submitOnReset } = unref(getProps)
resetFunc && isFunction(resetFunc) && (await resetFunc())
const formEl = unref(formElRef)
if (!formEl) return
Object.keys(formModel).forEach((key) => {
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<void> {
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<void> {
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<FormSchema> | Partial<FormSchema>[]) {
let updateData: Partial<FormSchema>[] = []
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<FormSchema> | Partial<FormSchema>[]) {
let updateData: Partial<FormSchema>[] = []
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<void> {
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,
}
}

View File

@ -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<any>
getSchema: ComputedRef<FormSchema[]>
getProps: ComputedRef<FormProps>
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 }
}

View File

@ -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<FormSchema>, propsRef: Ref<FormProps>) {
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,
},
}
})
}

View File

@ -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<Recordable>,
default: {},
},
// 标签宽度 固定宽度
labelWidth: {
type: [Number, String] as PropType<number | string>,
default: 0,
},
fieldMapToTime: {
type: Array as PropType<FieldMapToTime>,
default: () => [],
},
compact: propTypes.bool,
// 表单配置规则
schemas: {
type: [Array] as PropType<FormSchema[]>,
default: () => [],
},
mergeDynamicData: {
type: Object as PropType<Recordable>,
default: null,
},
baseRowStyle: {
type: Object as PropType<CSSProperties>,
},
baseColProps: {
type: Object as PropType<Partial<ColEx>>,
},
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<number>,
default: 0,
},
// 是否显示收起展开按钮
showAdvancedButton: propTypes.bool,
// 转化时间
transformDateFunc: {
type: Function as PropType<Fn>,
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<Partial<ColEx>>,
// 显示重置按钮
showResetButton: propTypes.bool.def(true),
// 是否聚焦第一个输入框只在第一个表单项为input的时候作用
autoFocusFirstItem: propTypes.bool,
// 重置按钮配置
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
// 显示确认按钮
showSubmitButton: propTypes.bool.def(true),
// 确认按钮配置
submitButtonOptions: Object as PropType<Partial<ButtonProps>>,
// 自定义重置函数
resetFunc: Function as PropType<() => Promise<void>>,
submitFunc: Function as PropType<() => Promise<void>>,
// 以下为默认props
hideRequiredMark: propTypes.bool,
labelCol: Object as PropType<Partial<ColEx>>,
layout: propTypes.oneOf(['horizontal', 'vertical', 'inline']).def('horizontal'),
tableAction: {
type: Object as PropType<TableActionType>,
},
wrapperCol: Object as PropType<Partial<ColEx>>,
colon: propTypes.bool,
labelAlign: propTypes.string,
rowProps: Object as PropType<RowProps>,
}

View File

@ -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<void>
setFieldsValue: <T>(values: T) => Promise<void>
resetFields: () => Promise<void>
getFieldsValue: () => Recordable
clearValidate: (name?: string | string[]) => Promise<void>
updateSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>
resetSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>
setProps: (formProps: Partial<FormProps>) => Promise<void>
removeSchemaByField: (field: string | string[]) => Promise<void>
appendSchemaByField: (
schema: FormSchema,
prefixField: string | undefined,
first?: boolean | undefined,
) => Promise<void>
validateFields: (nameList?: NamePath[]) => Promise<any>
validate: (nameList?: NamePath[]) => Promise<any>
scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>
}
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<ColEx>
// Col configuration for the entire form
wrapperCol?: Partial<ColEx>
// General row style
baseRowStyle?: CSSProperties
// General col configuration
baseColProps?: Partial<ColEx>
// 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<ColEx>
// 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<ButtonProps>
// Confirm button configuration
submitButtonOptions?: Partial<ButtonProps>
// Operation column configuration
actionColOptions?: Partial<ColEx>
// Show reset button
showResetButton?: boolean
// Show confirmation button
showSubmitButton?: boolean
resetFunc?: () => Promise<void>
submitFunc?: () => Promise<void>
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<HelpComponentProps>
// 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<FormItem>
// col configuration outside formModelItem
colProps?: Partial<ColEx>
// 默认值
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
}

View File

@ -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 <Col>
* @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;
}

View File

@ -0,0 +1,6 @@
export interface AdvanceState {
isAdvanced: boolean;
hideAdvanceBtn: boolean;
isLoad: boolean;
actionSpan: number;
}

View File

@ -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'

View File

@ -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'

View File

@ -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<ComponentType, Component>()

View File

@ -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'

View File

@ -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(

View File

@ -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'

View File

@ -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'

View File

@ -48,11 +48,9 @@ export function useRuleFormItem<T extends Recordable>(
},
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(() => {})
},
})

View File

@ -7,7 +7,7 @@
width="500px"
@ok="handleSubmit"
>
<BasicForm @register="registerForm"> </BasicForm>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup>
@ -57,6 +57,40 @@
return object
}
const handleSubmit = async () => {
//
// const obj = {}
// obj?.result?.records?.forEach(async (e) => {
// const { bigdataCameraParams } = e
// const params = {
// type: 1,
// agricultural_base_id: 26,
// sn: e.number,
// monitoring_point: e.monitoringPoint,
// extends: {
// ip: bigdataCameraParams.ip,
// port: bigdataCameraParams.port + '',
// rtsp_url: bigdataCameraParams.url,
// username: bigdataCameraParams.username,
// password: bigdataCameraParams.password,
// passage: bigdataCameraParams.nchannelId + '',
// },
// }
// try {
// await createDevice(params)
// } catch (e) {
// console.log(params)
// console.log('')
// console.log(e)
// }
// })
// // const values = await validate()
// return
try {
const values = await validate()
let params = {}

View File

@ -2,7 +2,7 @@ import { BasicColumn, FormSchema } from '/@/components/Table'
import dayjs from 'dayjs'
import { h } from 'vue'
import { Tag } from 'ant-design-vue'
import { ColEx } from '/@/components/Form/src/types'
import { ColEx } from '../../../components/Form1/src/types'
// import { formatDataByObject } from '/@/utils/index'
// import { getGriculturalDeviceBasic, getaGriculturalDevicePoint } from '/@/api/sys/user'

View File

@ -2,7 +2,7 @@
<div>
<BasicForm @register="registerForm" />
<List
:grid="{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 5, column: 1 }"
:grid="{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 5, column: 8 }"
:data-source="list"
>
<template #renderItem="{ item }">
@ -80,7 +80,7 @@
return formatDataByObject(resData)
},
params: {
device_type: 4,
device_type: 1,
agricultural_basic: e,
},
labelField: 'label',

View File

@ -7,7 +7,7 @@
</div>
</div>
</template>
<div class="">{{ item.base_name }} </div>
<div class="">{{ item.base_name }}-{{ item.monitoring_point }} </div>
</Card>
</template>

View File

@ -5,7 +5,7 @@ import { Tag } from 'ant-design-vue'
import { Switch } from 'ant-design-vue'
import { useMessage } from '/@/hooks/web/useMessage'
import { updateFriendinks } from '/@/api/sys/user'
import { ColEx } from '/@/components/Form/src/types'
import { ColEx } from '../../../components/Form1/src/types'
import { Tinymce } from '/@/components/Tinymce/index'
import { uploadApi } from '/@/api/upload'

View File

@ -98,7 +98,7 @@
const obj = formatChartData()
setOptions({
grid: { left: '2%', right: '4%', top: '10%', bottom: '2%', containLabel: true },
grid: { left: '2%', right: '20px', top: '10%', bottom: '2%', containLabel: true },
legend: {
data: obj.legendData,
top: '0%',

View File

@ -91,7 +91,7 @@
// const max = Math.max(...largestOfFour(Data.series)) ?? 100
setOptions({
grid: { left: '4%', right: '4%', top: '10%', bottom: '2%', containLabel: true },
grid: { left: '2%', right: '20px', top: '10%', bottom: '2%', containLabel: true },
legend: {
data: obj.legendData,
top: '0%',

View File

@ -78,7 +78,7 @@
const chartsInit = () => {
const obj = formatChartData()
setOptions({
grid: { left: '2%', right: '2%', top: '10%', bottom: '2%', containLabel: true },
grid: { left: '2%', right: '20px', top: '10%', bottom: '2%', containLabel: true },
legend: {
data: obj.legendData,
top: '0%',

View File

@ -225,7 +225,7 @@
}
})
setOptions({
grid: { left: '2%', right: '2%', top: '30px', bottom: '2%', containLabel: true },
grid: { left: '2%', right: '20px', top: '30px', bottom: '2%', containLabel: true },
legend: {
data: data.map((e) => e.name),
top: '0%',

View File

@ -221,7 +221,7 @@
})
setOptions({
grid: { left: '2%', right: '2%', top: '30px', bottom: '2%', containLabel: true },
grid: { left: '2%', right: '20px', top: '30px', bottom: '2%', containLabel: true },
legend: {
data: data.map((e) => e.name),
top: '0%',

View File

@ -94,6 +94,7 @@
import { computed } from '@vue/reactivity'
// import Am from './components/Star'
const initTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
// const initTime = '2022-10-20 00:00'
localStorage.removeItem('warning_id')
export default defineComponent({
components: {
@ -142,13 +143,13 @@
localStorage.setItem('warning_id', arr.join(','))
fliterData.forEach((e, index) => {
openNotificationWithIcon(e.content, 4.5 + index * 0.5)
openNotificationWithIcon(e, 10 + index * 1)
})
}
const openNotificationWithIcon = (message: string, duration = 4.5) => {
const openNotificationWithIcon = (message, duration = 4.5) => {
notification.warning({
message: '报警',
message: message.base_name,
duration: duration,
class: 'warning-class',
style: {
@ -159,7 +160,7 @@
if (document.body.clientWidth < 3000) return document.body
return document.body.querySelector(`.visualization—xx`)
},
description: message,
description: message.point_name + '' + message.content,
})
}
@ -234,6 +235,10 @@
.ant-notification-notice-message {
color: #fff;
}
.anticon-close {
color: #fff;
}
}
</style>
<style scoped lang="less">