未验证 提交 97fe8e20 编写于 作者: J Jobin 提交者: GitHub

feat(ApiCascader): add asynchronous cascader component (#1321)

上级 5c491a42
import { MockMethod } from 'vite-plugin-mock';
import { resultSuccess } from '../_util';
const areaList: any[] = [
{
id: '530825900854620160',
code: '430000',
parentCode: '100000',
levelType: 1,
name: '湖南省',
province: '湖南省',
city: null,
district: null,
town: null,
village: null,
parentPath: '430000',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530825900883980288',
code: '430100',
parentCode: '430000',
levelType: 2,
name: '长沙市',
province: '湖南省',
city: '长沙市',
district: null,
town: null,
village: null,
parentPath: '430000,430100',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530825900951089152',
code: '430102',
parentCode: '430100',
levelType: 3,
name: '芙蓉区',
province: '湖南省',
city: '长沙市',
district: '芙蓉区',
town: null,
village: null,
parentPath: '430000,430100,430102',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530825901014003712',
code: '430104',
parentCode: '430100',
levelType: 3,
name: '岳麓区',
province: '湖南省',
city: '长沙市',
district: '岳麓区',
town: null,
village: null,
parentPath: '430000,430100,430104',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530825900988837888',
code: '430103',
parentCode: '430100',
levelType: 3,
name: '天心区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: null,
village: null,
parentPath: '430000,430100,430103',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 16:33:42',
customized: false,
usable: true,
},
{
id: '530826672489115648',
code: '430103002',
parentCode: '430103',
levelType: 4,
name: '坡子街街道',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: null,
parentPath: '430000,430100,430103,430103002',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-12-14 15:26:43',
customized: false,
usable: true,
},
{
id: '530840241171607552',
code: '430103002001',
parentCode: '430103002',
levelType: 5,
name: '八角亭社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '八角亭社区',
parentPath: '430000,430100,430103,430103002,430103002001',
createTime: '2020-11-30 15:47:31',
updateTime: '2021-01-20 14:07:23',
customized: false,
usable: true,
},
{
id: '530840241200967680',
code: '430103002002',
parentCode: '430103002',
levelType: 5,
name: '西牌楼社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '西牌楼社区',
parentPath: '430000,430100,430103,430103002,430103002002',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241230327808',
code: '430103002003',
parentCode: '430103002',
levelType: 5,
name: '太平街社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '太平街社区',
parentPath: '430000,430100,430103,430103002,430103002003',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241259687936',
code: '430103002005',
parentCode: '430103002',
levelType: 5,
name: '坡子街社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '坡子街社区',
parentPath: '430000,430100,430103,430103002,430103002005',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241284853760',
code: '430103002006',
parentCode: '430103002',
levelType: 5,
name: '青山祠社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '青山祠社区',
parentPath: '430000,430100,430103,430103002,430103002006',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241310019584',
code: '430103002007',
parentCode: '430103002',
levelType: 5,
name: '沙河社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '沙河社区',
parentPath: '430000,430100,430103,430103002,430103002007',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241381322752',
code: '430103002008',
parentCode: '430103002',
levelType: 5,
name: '碧湘社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '碧湘社区',
parentPath: '430000,430100,430103,430103002,430103002008',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241410682880',
code: '430103002009',
parentCode: '430103002',
levelType: 5,
name: '创远社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '创远社区',
parentPath: '430000,430100,430103,430103002,430103002009',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241431654400',
code: '430103002010',
parentCode: '430103002',
levelType: 5,
name: '楚湘社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '楚湘社区',
parentPath: '430000,430100,430103,430103002,430103002010',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241465208832',
code: '430103002011',
parentCode: '430103002',
levelType: 5,
name: '西湖社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '西湖社区',
parentPath: '430000,430100,430103,430103002,430103002011',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241502957568',
code: '430103002012',
parentCode: '430103002',
levelType: 5,
name: '登仁桥社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '登仁桥社区',
parentPath: '430000,430100,430103,430103002,430103002012',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
{
id: '530840241553289216',
code: '430103002013',
parentCode: '430103002',
levelType: 5,
name: '文庙坪社区',
province: '湖南省',
city: '长沙市',
district: '天心区',
town: '坡子街街道',
village: '文庙坪社区',
parentPath: '430000,430100,430103,430103002,430103002013',
createTime: '2020-11-30 15:47:31',
updateTime: '2020-11-30 17:30:41',
customized: false,
usable: true,
},
];
export default [
{
url: '/basic-api/cascader/getAreaRecord',
timeout: 1000,
method: 'post',
response: ({ body }) => {
const { parentCode } = body || {};
if (!parentCode) {
return resultSuccess(areaList.filter((it) => it.code === '430000'));
}
return resultSuccess(areaList.filter((it) => it.parentCode === parentCode));
},
},
] as MockMethod[];
import { defHttp } from '/@/utils/http/axios';
import { AreaModel, AreaParams } from '/@/api/demo/model/areaModel';
enum Api {
AREA_RECORD = '/cascader/getAreaRecord',
}
export const areaRecord = (data: AreaParams) =>
defHttp.post<AreaModel>({ url: Api.AREA_RECORD, data });
export interface AreaModel {
id: string;
code: string;
parentCode: string;
name: string;
levelType: number;
[key: string]: string | number;
}
export interface AreaParams {
parentCode: string;
}
......@@ -10,5 +10,6 @@ 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 ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
export { default as ApiCascader } from './src/components/ApiCascader.vue';
export { BasicForm };
......@@ -25,6 +25,7 @@ import ApiRadioGroup from './components/ApiRadioGroup.vue';
import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.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';
......@@ -50,6 +51,7 @@ 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);
......
<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';
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);
// 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,
handleChange,
loadData,
handleRenderDisplay,
};
},
});
</script>
......@@ -98,6 +98,7 @@ export type ComponentType =
| 'Checkbox'
| 'CheckboxGroup'
| 'AutoComplete'
| 'ApiCascader'
| 'Cascader'
| 'DatePicker'
| 'MonthPicker'
......
......@@ -52,6 +52,7 @@
>
修改查询按钮
</a-button>
<a-button @click="handleLoad" class="mr-2"> 联动回显 </a-button>
</div>
<CollapseContainer title="useForm示例">
<BasicForm @register="register" @submit="handleSubmit" />
......@@ -64,6 +65,7 @@
import { CollapseContainer } from '/@/components/Container/index';
import { useMessage } from '/@/hooks/web/useMessage';
import { PageWrapper } from '/@/components/Page';
import { areaRecord } from '/@/api/demo/cascader';
const schemas: FormSchema[] = [
{
......@@ -166,6 +168,48 @@
],
},
},
{
field: 'field8',
component: 'ApiCascader',
label: '联动',
colProps: {
span: 8,
},
componentProps: {
api: areaRecord,
apiParamKey: 'parentCode',
dataField: 'data',
labelField: 'name',
valueField: 'code',
initFetchParams: {
parentCode: '',
},
isLeaf: (record) => {
return !(record.levelType < 3);
},
},
},
{
field: 'field9',
component: 'ApiCascader',
label: '联动回显',
colProps: {
span: 8,
},
componentProps: {
api: areaRecord,
apiParamKey: 'parentCode',
dataField: 'data',
labelField: 'name',
valueField: 'code',
initFetchParams: {
parentCode: '',
},
isLeaf: (record) => {
return !(record.levelType < 3);
},
},
},
];
export default defineComponent({
......@@ -173,7 +217,7 @@
setup() {
const { createMessage } = useMessage();
const [register, { setProps }] = useForm({
const [register, { setProps, setFieldsValue, updateSchema }] = useForm({
labelWidth: 120,
schemas,
actionColOptions: {
......@@ -181,6 +225,35 @@
},
fieldMapToTime: [['fieldTime', ['startTime', 'endTime'], 'YYYY-MM']],
});
async function handleLoad() {
const promiseFn = function () {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
field9: ['430000', '430100', '430102'],
province: '湖南省',
city: '长沙市',
district: '岳麓区',
});
}, 1000);
});
};
const item = await promiseFn();
const { field9, province, city, district } = item as any;
await updateSchema({
field: 'field9',
componentProps: {
displayRenderArray: [province, city, district],
},
});
await setFieldsValue({
field9,
});
}
return {
register,
schemas,
......@@ -188,6 +261,7 @@
createMessage.success('click search,values:' + JSON.stringify(values));
},
setProps,
handleLoad,
};
},
});
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册