| | |
| | | <template> |
| | | <a-cascader |
| | | <Cascader |
| | | v-model:value="state" |
| | | :options="options" |
| | | :load-data="loadData" |
| | |
| | | {{ t('component.form.apiSelectNotFound') }} |
| | | </span> |
| | | </template> |
| | | </a-cascader> |
| | | </Cascader> |
| | | </template> |
| | | <script lang="ts"> |
| | | <script lang="ts" setup> |
| | | import { type Recordable } from '@vben/types'; |
| | | import { defineComponent, PropType, ref, unref, watch, watchEffect } from 'vue'; |
| | | import { PropType, ref, unref, watch } from 'vue'; |
| | | import { Cascader } from 'ant-design-vue'; |
| | | import { propTypes } from '/@/utils/propTypes'; |
| | | import { isFunction } from '/@/utils/is'; |
| | | import type { CascaderProps } 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 { useRuleFormItem } from '@/hooks/component/useFormItem'; |
| | | import { LoadingOutlined } from '@ant-design/icons-vue'; |
| | | import { useI18n } from '/@/hooks/web/useI18n'; |
| | | import { useI18n } from '@/hooks/web/useI18n'; |
| | | |
| | | interface Option { |
| | | value: string; |
| | | label: string; |
| | | value?: string; |
| | | label?: string; |
| | | loading?: boolean; |
| | | isLeaf?: boolean; |
| | | children?: Option[]; |
| | | [key: string]: any; |
| | | } |
| | | export default defineComponent({ |
| | | name: 'ApiCascader', |
| | | components: { |
| | | LoadingOutlined, |
| | | [Cascader.name]: Cascader, |
| | | |
| | | defineOptions({ name: 'ApiCascader' }); |
| | | |
| | | const props = defineProps({ |
| | | value: { |
| | | type: Array, |
| | | }, |
| | | props: { |
| | | value: { |
| | | type: Array, |
| | | }, |
| | | api: { |
| | | type: Function as PropType<(arg?: Recordable<any>) => 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<any>>, |
| | | default: () => ({}), |
| | | }, |
| | | // 是否有下级,默认是 |
| | | isLeaf: { |
| | | type: Function as PropType<(arg: Recordable<any>) => boolean>, |
| | | default: null, |
| | | }, |
| | | displayRenderArray: { |
| | | type: Array, |
| | | }, |
| | | api: { |
| | | type: Function as PropType<(arg?: any) => Promise<Option[] | Recordable<any>>>, |
| | | default: null, |
| | | }, |
| | | 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<any>) => { |
| | | 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 = args; |
| | | 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, |
| | | }; |
| | | numberToString: propTypes.bool, |
| | | resultField: propTypes.string.def(''), |
| | | labelField: propTypes.string.def('label'), |
| | | valueField: propTypes.string.def('value'), |
| | | childrenField: propTypes.string.def('children'), |
| | | apiParamKey: propTypes.string.def('parentCode'), |
| | | immediate: propTypes.bool.def(true), |
| | | // init fetch params |
| | | initFetchParams: { |
| | | type: Object as PropType<Recordable<any>>, |
| | | default: () => ({}), |
| | | }, |
| | | // 是否有下级,默认是 |
| | | isLeaf: { |
| | | type: Function as PropType<(arg: Recordable<any>) => boolean>, |
| | | default: null, |
| | | }, |
| | | displayRenderArray: { |
| | | type: Array, |
| | | }, |
| | | beforeFetch: { |
| | | type: Function as PropType<Fn>, |
| | | default: null, |
| | | }, |
| | | afterFetch: { |
| | | type: Function as PropType<Fn>, |
| | | default: null, |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(['change', 'defaultChange']); |
| | | |
| | | 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]: any = 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<any>) => { |
| | | 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 fetch() { |
| | | let { api, beforeFetch, initFetchParams, afterFetch, resultField } = props; |
| | | if (!api || !isFunction(api)) return; |
| | | apiData.value = []; |
| | | loading.value = true; |
| | | try { |
| | | if (beforeFetch && isFunction(beforeFetch)) { |
| | | initFetchParams = (await beforeFetch(initFetchParams)) || initFetchParams; |
| | | } |
| | | let res = await api(initFetchParams); |
| | | if (afterFetch && isFunction(afterFetch)) { |
| | | res = (await afterFetch(res)) || res; |
| | | } |
| | | if (Array.isArray(res)) { |
| | | apiData.value = res; |
| | | return; |
| | | } |
| | | if (resultField) { |
| | | apiData.value = get(res, resultField) || []; |
| | | } |
| | | } catch (error) { |
| | | console.warn(error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | const loadData: CascaderProps['loadData'] = async (selectedOptions) => { |
| | | const targetOption = selectedOptions[selectedOptions.length - 1]; |
| | | targetOption.loading = true; |
| | | let { api, beforeFetch, afterFetch, resultField, apiParamKey } = props; |
| | | if (!api || !isFunction(api)) return; |
| | | try { |
| | | let param = { |
| | | [apiParamKey]: Reflect.get(targetOption, 'value'), |
| | | }; |
| | | if (beforeFetch && isFunction(beforeFetch)) { |
| | | param = (await beforeFetch(param)) || param; |
| | | } |
| | | let res = await api(param); |
| | | if (afterFetch && isFunction(afterFetch)) { |
| | | res = (await afterFetch(res)) || res; |
| | | } |
| | | if (Array.isArray(res)) { |
| | | const children = generatorOptions(res); |
| | | targetOption.children = children; |
| | | return; |
| | | } |
| | | if (resultField) { |
| | | const children = generatorOptions(get(res, resultField) || []); |
| | | targetOption.children = children; |
| | | } |
| | | } catch (e) { |
| | | console.error(e); |
| | | } finally { |
| | | targetOption.loading = false; |
| | | } |
| | | }; |
| | | |
| | | watch( |
| | | () => props.immediate, |
| | | () => { |
| | | props.immediate && fetch(); |
| | | }, |
| | | { |
| | | immediate: true, |
| | | }, |
| | | ); |
| | | |
| | | watch( |
| | | () => props.initFetchParams, |
| | | () => { |
| | | !unref(isFirstLoad) && fetch(); |
| | | }, |
| | | { deep: true }, |
| | | ); |
| | | |
| | | function handleChange(keys, args) { |
| | | emitData.value = args; |
| | | emit('defaultChange', keys, args); |
| | | } |
| | | |
| | | const handleRenderDisplay: CascaderProps['displayRender'] = ({ labels, selectedOptions }) => { |
| | | if (unref(emitData).length === selectedOptions?.length) { |
| | | return labels.join(' / '); |
| | | } |
| | | if (props.displayRenderArray) { |
| | | return props.displayRenderArray.join(' / '); |
| | | } |
| | | return ''; |
| | | }; |
| | | </script> |