初始版本提交
This commit is contained in:
16
src/components/TableForm/index.ts
Normal file
16
src/components/TableForm/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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 { BasicForm };
|
||||
370
src/components/TableForm/src/BasicForm.vue
Normal file
370
src/components/TableForm/src/BasicForm.vue
Normal file
@ -0,0 +1,370 @@
|
||||
<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">
|
||||
<template v-if="schema.component === 'Grid'">
|
||||
<!-- <Row type="flex" :gutters="0" justify="start" align="top"> -->
|
||||
<Col v-for="(col, colIndex) in schema.children" :key="colIndex" :span="col.span">
|
||||
<FormItem
|
||||
v-for="(colItem, idx) in col.list"
|
||||
:key="idx"
|
||||
:tableAction="tableAction"
|
||||
:formActionType="formActionType"
|
||||
:schema="colItem"
|
||||
: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>
|
||||
</Col>
|
||||
<!-- </Row> -->
|
||||
</template>
|
||||
<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, Col } 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';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicForm',
|
||||
components: { FormItem, Form, Row, Col, FormAction },
|
||||
props: basicProps,
|
||||
emits: ['advanced-change', 'reset', 'submit', 'register'],
|
||||
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 schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[];
|
||||
} else {
|
||||
return 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,
|
||||
removeSchemaByFiled,
|
||||
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((_) => {});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
removeSchemaByFiled,
|
||||
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>
|
||||
83
src/components/TableForm/src/componentMap.ts
Normal file
83
src/components/TableForm/src/componentMap.ts
Normal 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 { BasicUpload } from '/@/components/Upload';
|
||||
import { StrengthMeter } from '/@/components/StrengthMeter';
|
||||
import { IconPicker } from '/@/components/Icon';
|
||||
import { CountdownInput } from '/@/components/CountDown';
|
||||
import { Text } from '/@/components/Text';
|
||||
|
||||
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('DatePicker', DatePicker);
|
||||
componentMap.set('MonthPicker', DatePicker.MonthPicker);
|
||||
componentMap.set('RangePicker', DatePicker.RangePicker);
|
||||
componentMap.set('WeekPicker', DatePicker.WeekPicker);
|
||||
componentMap.set('TimePicker', TimePicker);
|
||||
componentMap.set('TimeRangePicker', TimePicker.TimeRangePicker);
|
||||
componentMap.set('StrengthMeter', StrengthMeter);
|
||||
componentMap.set('IconPicker', IconPicker);
|
||||
componentMap.set('InputCountDown', CountdownInput);
|
||||
componentMap.set('Upload', BasicUpload);
|
||||
componentMap.set('Divider', Divider);
|
||||
componentMap.set('Text', Text);
|
||||
|
||||
export function add(compName: ComponentType, component: Component) {
|
||||
componentMap.set(compName, component);
|
||||
}
|
||||
|
||||
export function del(compName: ComponentType) {
|
||||
componentMap.delete(compName);
|
||||
}
|
||||
|
||||
export { componentMap };
|
||||
198
src/components/TableForm/src/components/ApiCascader.vue
Normal file
198
src/components/TableForm/src/components/ApiCascader.vue
Normal 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>
|
||||
127
src/components/TableForm/src/components/ApiCheckboxGroup.vue
Normal file
127
src/components/TableForm/src/components/ApiCheckboxGroup.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<!--
|
||||
* @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
|
||||
-->
|
||||
<template>
|
||||
<CheckboxGroup v-bind="attrs" v-model:value="state" button-style="solid" @change="handleChange">
|
||||
<template v-for="item in getOptions" :key="`${item.value}`">
|
||||
<Checkbox :value="item.value" :disabled="item.disabled">
|
||||
{{ item.label }}
|
||||
</Checkbox>
|
||||
</template>
|
||||
</CheckboxGroup>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue';
|
||||
import { Checkbox } 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: 'ApiCheckboxGroup',
|
||||
components: {
|
||||
CheckboxGroup: Checkbox.Group,
|
||||
// RadioButton: Radio.Button,
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
api: {
|
||||
type: Function as PropType<(arg?: Recordable | string) => Promise<OptionsItem[]>>,
|
||||
default: null,
|
||||
},
|
||||
params: {
|
||||
type: [Object, String] as PropType<Recordable | string>,
|
||||
default: () => ({}),
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
},
|
||||
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>
|
||||
130
src/components/TableForm/src/components/ApiRadioGroup.vue
Normal file
130
src/components/TableForm/src/components/ApiRadioGroup.vue
Normal 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>
|
||||
147
src/components/TableForm/src/components/ApiSelect.vue
Normal file
147
src/components/TableForm/src/components/ApiSelect.vue
Normal 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>
|
||||
86
src/components/TableForm/src/components/ApiTree.vue
Normal file
86
src/components/TableForm/src/components/ApiTree.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<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(''),
|
||||
},
|
||||
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>
|
||||
86
src/components/TableForm/src/components/ApiTreeSelect.vue
Normal file
86
src/components/TableForm/src/components/ApiTreeSelect.vue
Normal 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>
|
||||
135
src/components/TableForm/src/components/FormAction.vue
Normal file
135
src/components/TableForm/src/components/FormAction.vue
Normal 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('收起') : t('展开') }}
|
||||
<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('重置'),
|
||||
},
|
||||
props.resetButtonOptions,
|
||||
);
|
||||
});
|
||||
|
||||
const getSubmitBtnOptions = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
text: t('查询'),
|
||||
},
|
||||
props.submitButtonOptions,
|
||||
);
|
||||
});
|
||||
|
||||
function toggleAdvanced() {
|
||||
emit('toggle-advanced');
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
actionColOpt,
|
||||
getResetBtnOptions,
|
||||
getSubmitBtnOptions,
|
||||
toggleAdvanced,
|
||||
...useFormContext(),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
384
src/components/TableForm/src/components/FormItem.vue
Normal file
384
src/components/TableForm/src/components/FormItem.vue
Normal file
@ -0,0 +1,384 @@
|
||||
<script lang="tsx">
|
||||
import type { PropType, Ref } from 'vue';
|
||||
import type { FormActionType, FormProps } from '../types/form';
|
||||
import type { FormSchema } from '../types/form';
|
||||
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
|
||||
import type { TableActionType } from '/@/components/Table';
|
||||
import { defineComponent, computed, unref, toRefs } from 'vue';
|
||||
import { Form, Col, Divider } 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 { upperFirst, cloneDeep } 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;
|
||||
|
||||
if ((!rules || rules.length === 0) && getRequired) {
|
||||
rules = [{ 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 !== 'TimeRangePicker' &&
|
||||
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>
|
||||
57
src/components/TableForm/src/components/RadioButtonGroup.vue
Normal file
57
src/components/TableForm/src/components/RadioButtonGroup.vue
Normal 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>
|
||||
78
src/components/TableForm/src/helper.ts
Normal file
78
src/components/TableForm/src/helper.ts
Normal file
@ -0,0 +1,78 @@
|
||||
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('请输入');
|
||||
}
|
||||
if (component.includes('Picker')) {
|
||||
return t('请选择');
|
||||
}
|
||||
if (
|
||||
component.includes('Select') ||
|
||||
component.includes('Cascader') ||
|
||||
component.includes('Checkbox') ||
|
||||
component.includes('Radio') ||
|
||||
component.includes('Switch')
|
||||
) {
|
||||
// return `请选择${label}`;
|
||||
return t('请选择');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'];
|
||||
|
||||
function genType() {
|
||||
return [...DATE_TYPE, 'RangePicker', 'TimeRangePicker'];
|
||||
}
|
||||
|
||||
export function setComponentRuleType(
|
||||
rule: ValidationRule,
|
||||
component: ComponentType,
|
||||
valueFormat: string,
|
||||
) {
|
||||
if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) {
|
||||
rule.type = valueFormat ? 'string' : 'object';
|
||||
} else if (
|
||||
['RangePicker', 'TimeRangePicker', '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'];
|
||||
|
||||
export const noFieldComponent = ['Divider', 'Grid', 'ChildTable', 'Text', 'TextLink'];
|
||||
165
src/components/TableForm/src/hooks/useAdvanced.ts
Normal file
165
src/components/TableForm/src/hooks/useAdvanced.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import type { ColEx } from '../types';
|
||||
import type { AdvanceState } from '../types/hooks';
|
||||
import type { ComputedRef, 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 { 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;
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
40
src/components/TableForm/src/hooks/useAutoFocus.ts
Normal file
40
src/components/TableForm/src/hooks/useAutoFocus.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
11
src/components/TableForm/src/hooks/useComponentRegister.ts
Normal file
11
src/components/TableForm/src/hooks/useComponentRegister.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
122
src/components/TableForm/src/hooks/useForm.ts
Normal file
122
src/components/TableForm/src/hooks/useForm.ts
Normal 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();
|
||||
});
|
||||
},
|
||||
|
||||
removeSchemaByFiled: async (field: string | string[]) => {
|
||||
unref(formRef)?.removeSchemaByFiled(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];
|
||||
}
|
||||
17
src/components/TableForm/src/hooks/useFormContext.ts
Normal file
17
src/components/TableForm/src/hooks/useFormContext.ts
Normal 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);
|
||||
}
|
||||
296
src/components/TableForm/src/hooks/useFormEvents.ts
Normal file
296
src/components/TableForm/src/hooks/useFormEvents.ts
Normal file
@ -0,0 +1,296 @@
|
||||
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, isNullOrUnDef, isObject, isString } from '/@/utils/is';
|
||||
import { deepMerge } from '/@/utils';
|
||||
import {
|
||||
dateItemType,
|
||||
handleInputNumberValue,
|
||||
defaultValueComponents,
|
||||
noFieldComponent,
|
||||
} 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);
|
||||
formModel[key] = isInput ? defaultValueRef.value[key] || '' : defaultValueRef.value[key];
|
||||
});
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
validateFields(validKeys).catch((_) => {});
|
||||
}
|
||||
/**
|
||||
* @description: Delete based on field name
|
||||
*/
|
||||
async function removeSchemaByFiled(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) {
|
||||
_removeSchemaByFiled(field, schemaList);
|
||||
}
|
||||
schemaRef.value = schemaList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Delete based on field name
|
||||
*/
|
||||
function _removeSchemaByFiled(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) =>
|
||||
noFieldComponent.includes(item.component!) || (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) =>
|
||||
noFieldComponent.includes(item.component!) || (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 = {};
|
||||
schemas.forEach((item) => {
|
||||
if (
|
||||
item.component != 'Divider' &&
|
||||
Reflect.has(item, 'field') &&
|
||||
item.field &&
|
||||
!isNullOrUnDef(item.defaultValue)
|
||||
) {
|
||||
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,
|
||||
removeSchemaByFiled,
|
||||
resetFields,
|
||||
setFieldsValue,
|
||||
scrollToField,
|
||||
};
|
||||
}
|
||||
128
src/components/TableForm/src/hooks/useFormValues.ts
Normal file
128
src/components/TableForm/src/hooks/useFormValues.ts
Normal file
@ -0,0 +1,128 @@
|
||||
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 { 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;
|
||||
formModel[item.field] = defaultValue;
|
||||
}
|
||||
});
|
||||
defaultValueRef.value = obj;
|
||||
}
|
||||
|
||||
return { handleFormValues, initDefault };
|
||||
}
|
||||
42
src/components/TableForm/src/hooks/useLabelWidth.ts
Normal file
42
src/components/TableForm/src/hooks/useLabelWidth.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
103
src/components/TableForm/src/props.ts
Normal file
103
src/components/TableForm/src/props.ts
Normal 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>,
|
||||
};
|
||||
230
src/components/TableForm/src/types/form.ts
Normal file
230
src/components/TableForm/src/types/form.ts
Normal file
@ -0,0 +1,230 @@
|
||||
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>;
|
||||
removeSchemaByFiled: (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;
|
||||
// grid layout children
|
||||
children?: GridComponentProps[];
|
||||
// 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;
|
||||
}
|
||||
|
||||
export interface GridComponentProps {
|
||||
span: number;
|
||||
list: FormSchema[];
|
||||
}
|
||||
91
src/components/TableForm/src/types/formItem.ts
Normal file
91
src/components/TableForm/src/types/formItem.ts
Normal 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;
|
||||
}
|
||||
6
src/components/TableForm/src/types/hooks.ts
Normal file
6
src/components/TableForm/src/types/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface AdvanceState {
|
||||
isAdvanced: boolean;
|
||||
hideAdvanceBtn: boolean;
|
||||
isLoad: boolean;
|
||||
actionSpan: number;
|
||||
}
|
||||
120
src/components/TableForm/src/types/index.ts
Normal file
120
src/components/TableForm/src/types/index.ts
Normal file
@ -0,0 +1,120 @@
|
||||
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'
|
||||
| 'TimeRangePicker'
|
||||
| 'Switch'
|
||||
| 'StrengthMeter'
|
||||
| 'Upload'
|
||||
| 'IconPicker'
|
||||
| 'Render'
|
||||
| 'Slider'
|
||||
| 'Rate'
|
||||
| 'Grid'
|
||||
| 'ChildTable'
|
||||
| 'Divider'
|
||||
| 'Text';
|
||||
Reference in New Issue
Block a user