Showing
19 changed files
with
973 additions
and
21 deletions
src/components/CardList/index.ts
0 → 100644
1 | +<script setup lang="ts"> | |
2 | + import { BasicForm, FormActionType, useForm } from '/@/components/Form'; | |
3 | + import { List, ListProps, Button, Tooltip } from 'ant-design-vue'; | |
4 | + import { computed, Ref, ref, toRaw, unref, useSlots, watch } from 'vue'; | |
5 | + import { useCardListScroll } from './hooks/useCardListScroll'; | |
6 | + import { BasicCardListPropsType, CardListActionType } from './types'; | |
7 | + import CardListLayout from './components/CardListLayout.vue'; | |
8 | + import { usePagination } from './hooks/usePagination'; | |
9 | + import { useLoading } from './hooks/useLoading'; | |
10 | + import { useCardListData } from './hooks/useCardListData'; | |
11 | + import { useListGrid } from './hooks/useListGrid'; | |
12 | + import { FETCH_SETTING } from '../../Table/src/const'; | |
13 | + import { useCardForm } from './hooks/useCardForm'; | |
14 | + import { Icon } from '/@/components/Icon'; | |
15 | + | |
16 | + const emit = defineEmits<{ | |
17 | + (eventName: 'fetchSuccess', result: { items: Recordable[]; total: number }): void; | |
18 | + (eventName: 'fetchError', error: Error): void; | |
19 | + ( | |
20 | + eventName: 'register', | |
21 | + cardListActionType: CardListActionType, | |
22 | + formActions: FormActionType | |
23 | + ): void; | |
24 | + }>(); | |
25 | + | |
26 | + const slots = useSlots(); | |
27 | + | |
28 | + const props = withDefaults(defineProps<BasicCardListPropsType>(), { | |
29 | + showCardListHeader: true, | |
30 | + showCardListSetting: true, | |
31 | + useSearchForm: false, | |
32 | + gutter: 16, | |
33 | + immediate: true, | |
34 | + fetchSetting: () => FETCH_SETTING, | |
35 | + autoCreateKey: true, | |
36 | + }); | |
37 | + | |
38 | + const tableData = ref<Recordable[]>([]); | |
39 | + | |
40 | + const listElRef = ref<Nullable<ComponentElRef>>(null); | |
41 | + | |
42 | + const innerPropsRef = ref<BasicCardListPropsType>(); | |
43 | + | |
44 | + const [registerForm, formActions] = useForm(); | |
45 | + | |
46 | + const getProps = computed<BasicCardListPropsType>(() => { | |
47 | + return { ...props, ...(unref(innerPropsRef) || {}) } as BasicCardListPropsType; | |
48 | + }); | |
49 | + | |
50 | + const { getListGrid, cardListLayout } = useListGrid(getProps); | |
51 | + | |
52 | + const { getPaginationInfo, getPagination, setPagination } = usePagination( | |
53 | + getProps, | |
54 | + cardListLayout | |
55 | + ); | |
56 | + | |
57 | + const { loading, setLoading, getLoading } = useLoading(); | |
58 | + | |
59 | + const { getDataSourceRef, getRowKey, fetch, reload } = useCardListData(getProps, formActions, { | |
60 | + setLoading, | |
61 | + getPaginationInfo, | |
62 | + emit, | |
63 | + tableData, | |
64 | + setPagination, | |
65 | + }); | |
66 | + | |
67 | + const { redoHeight, containerHeight } = useCardListScroll( | |
68 | + listElRef as Ref<Nullable<ComponentElRef>>, | |
69 | + getProps, | |
70 | + getDataSourceRef | |
71 | + ); | |
72 | + | |
73 | + const { replaceFormSlotKey, getFormProps, getFormSlotKeys } = useCardForm( | |
74 | + getProps, | |
75 | + slots, | |
76 | + fetch, | |
77 | + getLoading | |
78 | + ); | |
79 | + | |
80 | + function handleCardListChange() { | |
81 | + reload(); | |
82 | + } | |
83 | + | |
84 | + const getBindData = computed<ListProps>(() => { | |
85 | + const dataSource = unref(getDataSourceRef); | |
86 | + | |
87 | + return { | |
88 | + dataSource, | |
89 | + grid: unref(getListGrid), | |
90 | + loading: unref(loading), | |
91 | + pagination: { | |
92 | + ...toRaw(unref(getPaginationInfo)), | |
93 | + onChange: (current: number, pageSize: number) => { | |
94 | + setPagination({ current, pageSize }); | |
95 | + handleCardListChange(); | |
96 | + }, | |
97 | + }, | |
98 | + rowKey: unref(getRowKey), | |
99 | + }; | |
100 | + }); | |
101 | + | |
102 | + watch(cardListLayout, () => { | |
103 | + const pageSize = cardListLayout.row * cardListLayout.col; | |
104 | + setPagination({ pageSize }); | |
105 | + handleCardListChange(); | |
106 | + }); | |
107 | + | |
108 | + const CartListActionType: CardListActionType = { | |
109 | + setProps, | |
110 | + setLoading, | |
111 | + setPagination, | |
112 | + getPagination, | |
113 | + reload, | |
114 | + }; | |
115 | + | |
116 | + function setProps(props: Partial<BasicCardListPropsType> = {}) { | |
117 | + innerPropsRef.value = { ...(unref(innerPropsRef) || {}), ...props } as BasicCardListPropsType; | |
118 | + } | |
119 | + | |
120 | + emit('register', CartListActionType, formActions); | |
121 | +</script> | |
122 | + | |
123 | +<template> | |
124 | + <main class="basic-card-list p-4 flex flex-col w-full h-full"> | |
125 | + <section class="w-full bg-light-50 dark:bg-dark-900 p-4 mb-4"> | |
126 | + <BasicForm | |
127 | + @register="registerForm" | |
128 | + v-bind="getFormProps" | |
129 | + @advanced-change="redoHeight" | |
130 | + @reset="reload()" | |
131 | + @submit="reload()" | |
132 | + > | |
133 | + <template #[replaceFormSlotKey(item)]="data" v-for="item in getFormSlotKeys"> | |
134 | + <slot :name="item" v-bind="data"></slot> | |
135 | + </template> | |
136 | + </BasicForm> | |
137 | + </section> | |
138 | + | |
139 | + <section class="w-full bg-light-50 dark:bg-dark-900 p-4 flex-auto h-full"> | |
140 | + <List ref="listElRef" v-bind="getBindData"> | |
141 | + <template #header> | |
142 | + <section class="flex justify-between items-center"> | |
143 | + <div> | |
144 | + <span v-if="title"></span> | |
145 | + <slot v-else name="name"></slot> | |
146 | + </div> | |
147 | + <div class="flex flex-auto justify-end mr-4"> | |
148 | + <slot name="toolbar"></slot> | |
149 | + </div> | |
150 | + <div v-if="showCardListSetting" class="flex gap-4"> | |
151 | + <CardListLayout v-model:row="cardListLayout.row" v-model:col="cardListLayout.col" /> | |
152 | + <Tooltip title="刷新"> | |
153 | + <Button type="primary" @click="reload()" :loading="loading"> | |
154 | + <Icon icon="ant-design:reload-outlined" :size="20" /> | |
155 | + </Button> | |
156 | + </Tooltip> | |
157 | + </div> | |
158 | + </section> | |
159 | + </template> | |
160 | + <template #renderItem="{ item }"> | |
161 | + <List.Item> | |
162 | + <slot name="renderItem" :item="item" :totalHeight="containerHeight"></slot> | |
163 | + </List.Item> | |
164 | + </template> | |
165 | + </List> | |
166 | + </section> | |
167 | + </main> | |
168 | +</template> | |
169 | + | |
170 | +<style lang="less" scoped> | |
171 | + .basic-card-list { | |
172 | + :deep(.ant-list) { | |
173 | + .ant-spin-container { | |
174 | + overflow-x: hidden; | |
175 | + overflow-y: auto; | |
176 | + | |
177 | + .ant-row { | |
178 | + row-gap: 16px; | |
179 | + | |
180 | + .ant-list-item { | |
181 | + margin-bottom: 0; | |
182 | + } | |
183 | + } | |
184 | + } | |
185 | + | |
186 | + .ant-list-header { | |
187 | + border-bottom: transparent; | |
188 | + } | |
189 | + | |
190 | + .ant-list-pagination { | |
191 | + margin-top: 16px; | |
192 | + height: 25px; | |
193 | + } | |
194 | + | |
195 | + .ant-list-empty-text { | |
196 | + height: 100%; | |
197 | + display: flex; | |
198 | + align-items: center; | |
199 | + justify-content: center; | |
200 | + } | |
201 | + } | |
202 | + } | |
203 | +</style> | ... | ... |
1 | +<script setup lang="ts"> | |
2 | + import { Button, Popover } from 'ant-design-vue'; | |
3 | + import { reactive, ref, watch } from 'vue'; | |
4 | + import { Icon } from '/@/components/Icon'; | |
5 | + | |
6 | + const emit = defineEmits(['update:row', 'update:col']); | |
7 | + | |
8 | + const props = withDefaults( | |
9 | + defineProps<{ | |
10 | + row?: number; | |
11 | + col?: number; | |
12 | + }>(), | |
13 | + { | |
14 | + row: 2, | |
15 | + col: 5, | |
16 | + } | |
17 | + ); | |
18 | + | |
19 | + const MAX_ROW = 4; | |
20 | + const MAX_COL = 9; | |
21 | + | |
22 | + const visible = ref(false); | |
23 | + | |
24 | + const selectedLayout = reactive({ row: props.row, col: props.col }); | |
25 | + | |
26 | + const handleOver = (row: number, col: number) => { | |
27 | + Object.assign(selectedLayout, { row, col }); | |
28 | + }; | |
29 | + | |
30 | + const handleSelectConfirm = () => { | |
31 | + const { row, col } = selectedLayout; | |
32 | + emit('update:row', row); | |
33 | + emit('update:col', col); | |
34 | + visible.value = false; | |
35 | + }; | |
36 | + | |
37 | + watch( | |
38 | + () => [props.row, props.col], | |
39 | + () => { | |
40 | + selectedLayout.row = props.row; | |
41 | + selectedLayout.col = props.col; | |
42 | + } | |
43 | + ); | |
44 | +</script> | |
45 | + | |
46 | +<template> | |
47 | + <Popover v-model:visible="visible"> | |
48 | + <template #content> | |
49 | + <section class="flex flex-wrap w-36"> | |
50 | + <table class="border-collapse" @click="handleSelectConfirm"> | |
51 | + <tr v-for="row in MAX_ROW" :key="row" class="border border-gray-300 border-solid"> | |
52 | + <td | |
53 | + v-for="col in MAX_COL" | |
54 | + :class="selectedLayout.col >= col && selectedLayout.row >= row ? 'bg-blue-500' : ''" | |
55 | + :key="col" | |
56 | + class="w-4 h-4 border border-gray-300 border-solid cursor-pointer" | |
57 | + @mouseover="handleOver(row, col)" | |
58 | + > | |
59 | + </td> | |
60 | + </tr> | |
61 | + </table> | |
62 | + <div class="text-center w-full"> | |
63 | + {{ selectedLayout.col }} x {{ selectedLayout.row }} 布局 | |
64 | + </div> | |
65 | + </section> | |
66 | + </template> | |
67 | + <Button type="primary"> | |
68 | + <Icon icon="ant-design:layout-filled" :size="20" /> | |
69 | + </Button> | |
70 | + </Popover> | |
71 | +</template> | ... | ... |
1 | +import type { ComputedRef, ExtractPropTypes, Slots } from 'vue'; | |
2 | +import { unref, computed } from 'vue'; | |
3 | +import { isFunction } from '/@/utils/is'; | |
4 | +import { BasicCardListPropsType } from '../types'; | |
5 | +import { FetchParams } from './useCardListData'; | |
6 | +import { basicProps } from '/@/components/Form/src/props'; | |
7 | + | |
8 | +export function useCardForm( | |
9 | + propsRef: ComputedRef<BasicCardListPropsType>, | |
10 | + slots: Slots, | |
11 | + fetch: (opt?: FetchParams | undefined) => Promise<void>, | |
12 | + getLoading: ComputedRef<boolean | undefined> | |
13 | +) { | |
14 | + const getFormProps = computed((): ExtractPropTypes<typeof basicProps> => { | |
15 | + const { formConfig } = unref(propsRef); | |
16 | + const { submitButtonOptions } = formConfig || {}; | |
17 | + return { | |
18 | + showAdvancedButton: true, | |
19 | + ...formConfig, | |
20 | + submitButtonOptions: { loading: unref(getLoading), ...submitButtonOptions }, | |
21 | + compact: true, | |
22 | + } as any; | |
23 | + }); | |
24 | + | |
25 | + const getFormSlotKeys: ComputedRef<string[]> = computed(() => { | |
26 | + const keys = Object.keys(slots); | |
27 | + return keys | |
28 | + .map((item) => (item.startsWith('form-') ? item : null)) | |
29 | + .filter((item) => !!item) as string[]; | |
30 | + }); | |
31 | + | |
32 | + function replaceFormSlotKey(key: string) { | |
33 | + if (!key) return ''; | |
34 | + return key?.replace?.(/form\-/, '') ?? ''; | |
35 | + } | |
36 | + | |
37 | + function handleSearchInfoChange(info: Recordable) { | |
38 | + const { handleSearchInfoFn } = unref(propsRef); | |
39 | + if (handleSearchInfoFn && isFunction(handleSearchInfoFn)) { | |
40 | + info = handleSearchInfoFn(info) || info; | |
41 | + } | |
42 | + fetch({ searchInfo: info, page: 1 }); | |
43 | + } | |
44 | + | |
45 | + return { | |
46 | + getFormProps, | |
47 | + replaceFormSlotKey, | |
48 | + getFormSlotKeys, | |
49 | + handleSearchInfoChange, | |
50 | + }; | |
51 | +} | ... | ... |
1 | +import { WatchStopHandle, onUnmounted, ref, unref, watch } from 'vue'; | |
2 | +import { BasicCardListPropsType, CardListActionType } from '../types'; | |
3 | +import { isProdMode } from '/@/utils/env'; | |
4 | +import { error } from '/@/utils/log'; | |
5 | +import { FormActionType } from '/@/components/Form'; | |
6 | +import { getDynamicProps } from '/@/utils'; | |
7 | +import { PaginationProps } from 'ant-design-vue'; | |
8 | +import { FetchParams } from './useCardListData'; | |
9 | + | |
10 | +type UseTableMethod = CardListActionType & { | |
11 | + getForm: () => FormActionType; | |
12 | +}; | |
13 | + | |
14 | +export function useCardList( | |
15 | + cardListProps?: BasicCardListPropsType | |
16 | +): [(instance: CardListActionType, formInstance: FormActionType) => void, CardListActionType] { | |
17 | + const cardListRef = ref<Nullable<CardListActionType>>(null); | |
18 | + const loadedRef = ref<Nullable<boolean>>(false); | |
19 | + const formRef = ref<Nullable<FormActionType>>(null); | |
20 | + | |
21 | + let stopWatch: WatchStopHandle; | |
22 | + | |
23 | + function register(instance: CardListActionType, formInstance: FormActionType) { | |
24 | + isProdMode() && | |
25 | + onUnmounted(() => { | |
26 | + cardListRef.value = null; | |
27 | + loadedRef.value = null; | |
28 | + }); | |
29 | + | |
30 | + if (unref(loadedRef) && isProdMode() && instance === unref(cardListRef)) return; | |
31 | + | |
32 | + cardListRef.value = instance; | |
33 | + formRef.value = formInstance; | |
34 | + cardListProps && instance.setProps(getDynamicProps(cardListProps)); | |
35 | + loadedRef.value = true; | |
36 | + | |
37 | + stopWatch?.(); | |
38 | + | |
39 | + stopWatch = watch( | |
40 | + () => cardListProps, | |
41 | + () => { | |
42 | + cardListProps && instance.setProps(getDynamicProps(cardListProps)); | |
43 | + }, | |
44 | + { | |
45 | + immediate: true, | |
46 | + deep: true, | |
47 | + } | |
48 | + ); | |
49 | + } | |
50 | + | |
51 | + function getTableInstance(): CardListActionType { | |
52 | + const table = unref(cardListRef); | |
53 | + if (!table) { | |
54 | + error( | |
55 | + 'The CardList instance has not been obtained yet, please make sure the table is presented when performing the table operation!' | |
56 | + ); | |
57 | + } | |
58 | + return table as CardListActionType; | |
59 | + } | |
60 | + | |
61 | + const cardListActionType: UseTableMethod = { | |
62 | + getForm: () => { | |
63 | + return unref(formRef) as unknown as FormActionType; | |
64 | + }, | |
65 | + setLoading: (loading: boolean) => { | |
66 | + return getTableInstance().setLoading(loading); | |
67 | + }, | |
68 | + setProps: (props: Partial<BasicCardListPropsType>) => { | |
69 | + getTableInstance().setProps(props); | |
70 | + }, | |
71 | + getPagination: () => { | |
72 | + return getTableInstance().getPagination(); | |
73 | + }, | |
74 | + setPagination: (pagination: Partial<PaginationProps>) => { | |
75 | + return getTableInstance().setPagination(pagination); | |
76 | + }, | |
77 | + reload: (opt?: FetchParams) => { | |
78 | + return getTableInstance().reload(opt); | |
79 | + }, | |
80 | + }; | |
81 | + | |
82 | + return [register, cardListActionType]; | |
83 | +} | ... | ... |
1 | +import { Ref, computed, onMounted, ref, unref, watch, watchEffect } from 'vue'; | |
2 | +import { BasicCardListPropsType, CardListEmitType, UseLoading, UsePaginationType } from '../types'; | |
3 | +import { FormActionType } from '/@/components/Form'; | |
4 | +import { isBoolean, isFunction } from '/@/utils/is'; | |
5 | +import { FETCH_SETTING, ROW_KEY } from '/@/components/Table/src/const'; | |
6 | +import { PaginationProps } from 'ant-design-vue'; | |
7 | +import { cloneDeep, get } from 'lodash-es'; | |
8 | +import { buildUUID } from '/@/utils/uuid'; | |
9 | +import { useTimeoutFn } from '/@/hooks/core/useTimeout'; | |
10 | + | |
11 | +interface ActionType { | |
12 | + setLoading: UseLoading['setLoading']; | |
13 | + getPaginationInfo: UsePaginationType['getPaginationInfo']; | |
14 | + setPagination: UsePaginationType['setPagination']; | |
15 | + tableData: Ref<Recordable[]>; | |
16 | + emit: CardListEmitType; | |
17 | +} | |
18 | + | |
19 | +export interface FetchParams { | |
20 | + searchInfo?: Recordable; | |
21 | + page?: number; | |
22 | + sortInfo?: Recordable; | |
23 | + filterInfo?: Recordable; | |
24 | +} | |
25 | + | |
26 | +export function useCardListData( | |
27 | + propsRef: Ref<BasicCardListPropsType>, | |
28 | + formActionType: FormActionType, | |
29 | + actionType: ActionType | |
30 | +) { | |
31 | + const { getFieldsValue } = unref(formActionType)! || {}; | |
32 | + const { getPaginationInfo, setLoading, setPagination, tableData, emit } = actionType; | |
33 | + | |
34 | + const dataSourceRef = ref<Recordable[]>([]); | |
35 | + const rawDataSourceRef = ref<Recordable>({}); | |
36 | + | |
37 | + watchEffect(() => { | |
38 | + tableData.value = unref(dataSourceRef); | |
39 | + }); | |
40 | + | |
41 | + watch( | |
42 | + () => unref(propsRef).dataSource, | |
43 | + () => { | |
44 | + const { dataSource, api } = unref(propsRef); | |
45 | + !api && dataSource && (dataSourceRef.value = dataSource); | |
46 | + }, | |
47 | + { | |
48 | + immediate: true, | |
49 | + } | |
50 | + ); | |
51 | + | |
52 | + function setTableKey(items: any[]) { | |
53 | + if (!items || !Array.isArray(items)) return; | |
54 | + items.forEach((item) => { | |
55 | + if (!item[ROW_KEY]) { | |
56 | + item[ROW_KEY] = buildUUID(); | |
57 | + } | |
58 | + if (item.children && item.children.length) { | |
59 | + setTableKey(item.children); | |
60 | + } | |
61 | + }); | |
62 | + } | |
63 | + | |
64 | + const getAutoCreateKey = computed(() => { | |
65 | + return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey; | |
66 | + }); | |
67 | + | |
68 | + const getRowKey = computed(() => { | |
69 | + const { rowKey } = unref(propsRef); | |
70 | + return unref(getAutoCreateKey) ? ROW_KEY : rowKey; | |
71 | + }); | |
72 | + | |
73 | + const getDataSourceRef = computed(() => { | |
74 | + const dataSource = unref(dataSourceRef); | |
75 | + if (!dataSource || dataSource.length === 0) { | |
76 | + return unref(dataSourceRef); | |
77 | + } | |
78 | + if (unref(getAutoCreateKey)) { | |
79 | + const firstItem = dataSource[0]; | |
80 | + const lastItem = dataSource[dataSource.length - 1]; | |
81 | + | |
82 | + if (firstItem && lastItem) { | |
83 | + if (!firstItem[ROW_KEY] || !lastItem[ROW_KEY]) { | |
84 | + const data = cloneDeep(unref(dataSourceRef)); | |
85 | + data.forEach((item) => { | |
86 | + if (!item[ROW_KEY]) { | |
87 | + item[ROW_KEY] = buildUUID(); | |
88 | + } | |
89 | + if (item.children && item.children.length) { | |
90 | + setTableKey(item.children); | |
91 | + } | |
92 | + }); | |
93 | + dataSourceRef.value = data; | |
94 | + } | |
95 | + } | |
96 | + } | |
97 | + return unref(dataSourceRef); | |
98 | + }); | |
99 | + | |
100 | + async function fetch(opt?: FetchParams) { | |
101 | + const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm, pagination } = | |
102 | + unref(propsRef); | |
103 | + | |
104 | + if (!api || !isFunction(api)) return; | |
105 | + | |
106 | + try { | |
107 | + setLoading(true); | |
108 | + const { pageField, sizeField, listField, totalField } = Object.assign( | |
109 | + {}, | |
110 | + FETCH_SETTING, | |
111 | + fetchSetting | |
112 | + ); | |
113 | + let pageParams: Recordable = {}; | |
114 | + | |
115 | + const { current = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps; | |
116 | + | |
117 | + if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) { | |
118 | + pageParams = {}; | |
119 | + } else { | |
120 | + pageParams[pageField] = current; | |
121 | + pageParams[sizeField] = pageSize; | |
122 | + } | |
123 | + | |
124 | + let params: Recordable = { | |
125 | + ...pageParams, | |
126 | + ...(useSearchForm ? getFieldsValue() : {}), | |
127 | + ...searchInfo, | |
128 | + }; | |
129 | + | |
130 | + if (beforeFetch && isFunction(beforeFetch)) { | |
131 | + params = (await beforeFetch(params)) || params; | |
132 | + } | |
133 | + | |
134 | + const res = await api(params); | |
135 | + rawDataSourceRef.value = res; | |
136 | + | |
137 | + const isArrayResult = Array.isArray(res); | |
138 | + | |
139 | + let resultItems: Recordable[] = isArrayResult ? res : get(res, listField); | |
140 | + const resultTotal: number = isArrayResult ? 0 : get(res, totalField); | |
141 | + | |
142 | + // 假如数据变少,导致总页数变少并小于当前选中页码,通过getPaginationRef获取到的页码是不正确的,需获取正确的页码再次执行 | |
143 | + if (resultTotal) { | |
144 | + const currentTotalPage = Math.ceil(resultTotal / pageSize); | |
145 | + if (current > currentTotalPage) { | |
146 | + setPagination({ | |
147 | + current: currentTotalPage, | |
148 | + }); | |
149 | + fetch(opt); | |
150 | + } | |
151 | + } | |
152 | + | |
153 | + if (afterFetch && isFunction(afterFetch)) { | |
154 | + resultItems = (await afterFetch(resultItems, res)) || resultItems; | |
155 | + } | |
156 | + dataSourceRef.value = resultItems; | |
157 | + setPagination({ | |
158 | + total: resultTotal || 0, | |
159 | + }); | |
160 | + if (opt && opt.page) { | |
161 | + setPagination({ | |
162 | + current: opt.page || 1, | |
163 | + }); | |
164 | + } | |
165 | + emit('fetchSuccess', { | |
166 | + items: unref(resultItems), | |
167 | + total: resultTotal, | |
168 | + }); | |
169 | + } catch (error) { | |
170 | + emit('fetchError', error as Error); | |
171 | + dataSourceRef.value = []; | |
172 | + setPagination({ | |
173 | + total: 0, | |
174 | + }); | |
175 | + } finally { | |
176 | + setLoading(false); | |
177 | + } | |
178 | + } | |
179 | + | |
180 | + async function reload(opt?: FetchParams) { | |
181 | + await fetch(opt); | |
182 | + } | |
183 | + | |
184 | + onMounted(() => { | |
185 | + useTimeoutFn(() => { | |
186 | + unref(propsRef).immediate && fetch(); | |
187 | + }, 16); | |
188 | + }); | |
189 | + | |
190 | + return { | |
191 | + reload, | |
192 | + fetch, | |
193 | + getRowKey, | |
194 | + getDataSourceRef, | |
195 | + }; | |
196 | +} | ... | ... |
1 | +import { Ref, ref, unref, watch } from 'vue'; | |
2 | +import { getBoundingClientRect } from '/@/utils/domUtils'; | |
3 | +import { BasicCardListPropsType } from '../types'; | |
4 | +import { useDebounceFn } from '@vueuse/core'; | |
5 | + | |
6 | +export function useCardListScroll( | |
7 | + cardListElRef: Ref<Nullable<ComponentElRef>>, | |
8 | + propsRef: Ref<BasicCardListPropsType>, | |
9 | + getDataSourceRef: Ref<Recordable[]> | |
10 | +) { | |
11 | + const containerHeight = ref(0); | |
12 | + | |
13 | + const redoHeight = () => { | |
14 | + const listEl = unref(cardListElRef)?.$el; | |
15 | + if (!listEl) return; | |
16 | + | |
17 | + const listContainerEl = listEl.querySelector('.ant-spin-container') as HTMLDivElement; | |
18 | + | |
19 | + if (!listContainerEl) return; | |
20 | + | |
21 | + const rect = getBoundingClientRect(listContainerEl); | |
22 | + | |
23 | + if (!rect) return; | |
24 | + | |
25 | + const { top } = rect as DOMRect; | |
26 | + | |
27 | + const { offsetHeight: otherOffsetHeight = 0 } = unref(propsRef); | |
28 | + | |
29 | + const totalHeight = document.documentElement.clientHeight; | |
30 | + | |
31 | + const offsetHeight = 32; | |
32 | + | |
33 | + const paginationHeight = 25 + 16; | |
34 | + | |
35 | + const residualHeight = totalHeight - top - offsetHeight - paginationHeight - otherOffsetHeight; | |
36 | + | |
37 | + listContainerEl.style.maxHeight = `${residualHeight}px`; | |
38 | + listContainerEl.style.height = `${residualHeight}px`; | |
39 | + | |
40 | + containerHeight.value = residualHeight; | |
41 | + }; | |
42 | + | |
43 | + const debounceRedoHeight = useDebounceFn(redoHeight, 100); | |
44 | + | |
45 | + watch( | |
46 | + () => [unref(getDataSourceRef)?.length], | |
47 | + () => { | |
48 | + debounceRedoHeight(); | |
49 | + }, | |
50 | + { | |
51 | + flush: 'post', | |
52 | + } | |
53 | + ); | |
54 | + | |
55 | + return { redoHeight, containerHeight }; | |
56 | +} | ... | ... |
1 | +import { Ref, computed, reactive, unref } from 'vue'; | |
2 | +import { BasicCardListPropsType, ListGridType } from '../types'; | |
3 | +import { getListGridByColumn } from '../utils'; | |
4 | + | |
5 | +export function useListGrid(getProps: Ref<BasicCardListPropsType>) { | |
6 | + const cardListLayout = reactive({ row: 2, col: 5 }); | |
7 | + | |
8 | + const getListGrid = computed<ListGridType>(() => { | |
9 | + const { col } = cardListLayout; | |
10 | + const { gutter = 16 } = unref(getProps); | |
11 | + return { | |
12 | + column: col, | |
13 | + gutter, | |
14 | + ...getListGridByColumn(col), | |
15 | + } as ListGridType; | |
16 | + }); | |
17 | + | |
18 | + return { | |
19 | + getListGrid, | |
20 | + cardListLayout, | |
21 | + }; | |
22 | +} | ... | ... |
1 | +import { computed, ref, unref } from 'vue'; | |
2 | + | |
3 | +export function useLoading() { | |
4 | + const loading = ref(false); | |
5 | + | |
6 | + const setLoading = (status: boolean) => (loading.value = status); | |
7 | + | |
8 | + const getLoading = computed(() => unref(loading)); | |
9 | + | |
10 | + return { | |
11 | + loading, | |
12 | + getLoading, | |
13 | + setLoading, | |
14 | + }; | |
15 | +} | ... | ... |
1 | +import { PaginationProps } from 'ant-design-vue'; | |
2 | +import { Ref, computed, ref, unref } from 'vue'; | |
3 | +import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue'; | |
4 | +import { BasicCardListPropsType } from '../types'; | |
5 | +import { isBoolean } from '/@/utils/is'; | |
6 | + | |
7 | +interface ItemRender { | |
8 | + page: number; | |
9 | + type: 'page' | 'prev' | 'next'; | |
10 | + originalElement: any; | |
11 | +} | |
12 | + | |
13 | +function itemRender({ page, type, originalElement }: ItemRender) { | |
14 | + if (type === 'prev') { | |
15 | + return page === 0 ? null : <LeftOutlined />; | |
16 | + } else if (type === 'next') { | |
17 | + return page === 1 ? null : <RightOutlined />; | |
18 | + } | |
19 | + return originalElement; | |
20 | +} | |
21 | + | |
22 | +export function usePagination( | |
23 | + propsRef: Ref<BasicCardListPropsType>, | |
24 | + cardLayoutRef: Record<'row' | 'col', number> | |
25 | +) { | |
26 | + const configRef = ref<PaginationProps>({ | |
27 | + hideOnSinglePage: false, | |
28 | + }); | |
29 | + | |
30 | + const getPaginationInfo = computed(() => { | |
31 | + const { pagination } = unref(propsRef); | |
32 | + const { col, row } = cardLayoutRef; | |
33 | + | |
34 | + return { | |
35 | + current: 1, | |
36 | + pageSize: col * row, | |
37 | + size: 'small', | |
38 | + defaultPageSize: col * row, | |
39 | + showTotal: (total: number) => `共 ${total} 条数据`, | |
40 | + showSizeChanger: false, | |
41 | + itemRender: itemRender, | |
42 | + showQuickJumper: true, | |
43 | + ...(isBoolean(pagination) ? {} : pagination), | |
44 | + ...unref(configRef), | |
45 | + } as Partial<PaginationProps>; | |
46 | + }); | |
47 | + | |
48 | + const setPagination = (info: Partial<PaginationProps>) => { | |
49 | + const paginationInfo = unref(getPaginationInfo); | |
50 | + configRef.value = { | |
51 | + ...(!isBoolean(paginationInfo) ? paginationInfo : {}), | |
52 | + ...info, | |
53 | + }; | |
54 | + }; | |
55 | + | |
56 | + function getPagination() { | |
57 | + return unref(getPaginationInfo); | |
58 | + } | |
59 | + | |
60 | + return { getPaginationInfo, setPagination, getPagination }; | |
61 | +} | ... | ... |
src/components/CardList/src/types.ts
0 → 100644
1 | +import { PaginationProps } from 'ant-design-vue'; | |
2 | +import { FormProps } from '../../Form'; | |
3 | +import { FetchSetting } from '../../Table'; | |
4 | +import { useLoading } from './hooks/useLoading'; | |
5 | +import { usePagination } from './hooks/usePagination'; | |
6 | +import { FetchParams, useCardListData } from './hooks/useCardListData'; | |
7 | + | |
8 | +// export interface CardList | |
9 | + | |
10 | +export interface BasicCardListPropsType<T = Recordable> { | |
11 | + title?: string; | |
12 | + formConfig?: FormProps; | |
13 | + showCardListSetting?: boolean; | |
14 | + useSearchForm?: boolean; | |
15 | + api?: (params?: any) => Promise<any>; | |
16 | + dataSource?: any[]; | |
17 | + beforeFetch?: (params: FetchParams) => Promise<Recordable>; | |
18 | + pagination?: PaginationProps; | |
19 | + offsetHeight?: number; | |
20 | + gutter?: number; | |
21 | + searchInfo?: Recordable; | |
22 | + fetchSetting?: FetchSetting; | |
23 | + afterFetch?: (items: T[], result: any) => Promise<any[]>; | |
24 | + autoCreateKey?: boolean; | |
25 | + rowKey?: (item: T) => string | number; | |
26 | + immediate?: boolean; | |
27 | + handleSearchInfoFn?: Fn; | |
28 | +} | |
29 | + | |
30 | +export type ListGridType = Record< | |
31 | + 'column' | 'gutter' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', | |
32 | + number | |
33 | +>; | |
34 | + | |
35 | +export type UseLoading = ReturnType<typeof useLoading>; | |
36 | + | |
37 | +export type UsePaginationType = ReturnType<typeof usePagination>; | |
38 | +export type UseCardListDataType = ReturnType<typeof useCardListData>; | |
39 | + | |
40 | +export interface CardListActionType { | |
41 | + setProps: (props: Partial<BasicCardListPropsType>) => void; | |
42 | + setLoading: UseLoading['setLoading']; | |
43 | + setPagination: UsePaginationType['setPagination']; | |
44 | + getPagination: UsePaginationType['getPagination']; | |
45 | + reload: UseCardListDataType['reload']; | |
46 | +} | |
47 | + | |
48 | +export interface CardListEmitType { | |
49 | + (eventName: 'fetchSuccess', result: { items: Recordable[]; total: number }): void; | |
50 | + (eventName: 'fetchError', error: Error): void; | |
51 | +} | |
52 | + | |
53 | +export interface CardListRenderItem<T = Recordable> { | |
54 | + item: T; | |
55 | + totalHeight: number; | |
56 | +} | ... | ... |
src/components/CardList/src/utils/index.ts
0 → 100644
1 | +import { ListGridType } from '../types'; | |
2 | + | |
3 | +export const getListGridByColumn = (col: number): Omit<ListGridType, 'gutter' | 'column'> => { | |
4 | + return { | |
5 | + xxl: col, | |
6 | + xl: col, | |
7 | + lg: Math.min(col - 1, 2), | |
8 | + md: Math.min(col - 1, 2), | |
9 | + sm: Math.min(col - 2, 2), | |
10 | + xs: Math.min(col - 2, 2), | |
11 | + }; | |
12 | +}; | ... | ... |
1 | 1 | export { default as ModeSwitchButton } from './ModeSwitchButton.vue'; |
2 | 2 | export { default as CardLayoutButton } from './CardLayoutButton.vue'; |
3 | 3 | export { default as AuthIcon } from './AuthIcon.vue'; |
4 | +export { default as AuthDropDown } from './AuthDropDown.vue'; | |
4 | 5 | export { |
5 | 6 | EnumTableCardMode, |
6 | 7 | EnumTableChartMode, | ... | ... |
1 | +import { WatchStopHandle, onUnmounted, ref, unref, watch } from 'vue'; | |
2 | +import { OrganizationTreeActionType } from '../src/types'; | |
3 | +import { isProdMode } from '/@/utils/env'; | |
4 | +import { error } from '/@/utils/log'; | |
5 | +import { OrganizationTreePropsType } from '../src/props'; | |
6 | +import { getDynamicProps } from '/@/utils'; | |
7 | + | |
8 | +export function useOrganizationTree( | |
9 | + organizationTreeProps?: Partial<OrganizationTreePropsType> | |
10 | +): [(instance: OrganizationTreeActionType) => void, OrganizationTreeActionType] { | |
11 | + const organizationTreeRef = ref<Nullable<OrganizationTreeActionType>>(null); | |
12 | + const loadedRef = ref<Nullable<boolean>>(false); | |
13 | + let stopWatch: WatchStopHandle; | |
14 | + function register(instance: OrganizationTreeActionType) { | |
15 | + isProdMode() && | |
16 | + onUnmounted(() => { | |
17 | + organizationTreeRef.value = null; | |
18 | + loadedRef.value = null; | |
19 | + }); | |
20 | + | |
21 | + if (unref(loadedRef) && isProdMode() && instance === unref(organizationTreeRef)) return; | |
22 | + | |
23 | + organizationTreeRef.value = instance; | |
24 | + loadedRef.value = true; | |
25 | + | |
26 | + stopWatch?.(); | |
27 | + | |
28 | + stopWatch = watch( | |
29 | + () => organizationTreeProps, | |
30 | + () => { | |
31 | + organizationTreeProps && instance.setProps(getDynamicProps(organizationTreeProps)); | |
32 | + }, | |
33 | + { | |
34 | + immediate: true, | |
35 | + deep: true, | |
36 | + } | |
37 | + ); | |
38 | + } | |
39 | + | |
40 | + function getOrganizationTreeInstance(): OrganizationTreeActionType { | |
41 | + const organizationTree = unref(organizationTreeRef); | |
42 | + if (!organizationTree) { | |
43 | + error( | |
44 | + 'The organization tree instance has not been obtained yet, please make sure the table is presented when performing the table operation!' | |
45 | + ); | |
46 | + } | |
47 | + return organizationTree as OrganizationTreeActionType; | |
48 | + } | |
49 | + | |
50 | + const organizationTreeActionType: OrganizationTreeActionType = { | |
51 | + getSelectKey: () => getOrganizationTreeInstance().getSelectKey(), | |
52 | + clearSelected: () => getOrganizationTreeInstance().clearSelected(), | |
53 | + setProps: (props: Partial<OrganizationTreePropsType>) => | |
54 | + getOrganizationTreeInstance().setProps(props), | |
55 | + }; | |
56 | + | |
57 | + return [register, organizationTreeActionType]; | |
58 | +} | ... | ... |
... | ... | @@ -17,41 +17,41 @@ |
17 | 17 | </div> |
18 | 18 | </div> |
19 | 19 | <div :style="{ width: foldFlag ? '0px' : '100%' }" class="bg-white mr-0 overflow-hidden h-full"> |
20 | - <BasicTree | |
21 | - title="组织列表" | |
22 | - toolbar | |
23 | - search | |
24 | - :clickRowToExpand="false" | |
25 | - :treeData="treeData" | |
26 | - :expandedKeys="treeExpandData" | |
27 | - :replaceFields="{ key: 'id', title: 'name' }" | |
28 | - :selectedKeys="selectedKeys" | |
29 | - @select="handleSelect" | |
30 | - v-bind="$attrs" | |
31 | - /> | |
20 | + <BasicTree v-bind="getBindData" /> | |
32 | 21 | </div> |
33 | 22 | </div> |
34 | 23 | </template> |
35 | 24 | <script lang="ts" setup name="OrganizationIdTree"> |
36 | - import { onMounted, ref, unref } from 'vue'; | |
25 | + import { computed, onMounted, ref, unref, useAttrs } from 'vue'; | |
37 | 26 | import { BasicTree, TreeItem } from '/@/components/Tree'; |
38 | 27 | import { getOrganizationList } from '/@/api/system/system'; |
39 | 28 | import { CaretRightOutlined } from '@ant-design/icons-vue'; |
40 | 29 | import { getBoundingClientRect } from '/@/utils/domUtils'; |
30 | + import { BasicTreePropsType, OrganizationTreeActionType } from './types'; | |
31 | + import { OrganizationTreePropsType } from './props'; | |
32 | + | |
33 | + const props = defineProps<OrganizationTreePropsType>(); | |
34 | + | |
35 | + const attrs = useAttrs(); | |
41 | 36 | |
42 | 37 | const tree = ref<Nullable<HTMLDivElement>>(); |
43 | - const emit = defineEmits(['select']); | |
38 | + const emit = defineEmits(['select', 'register']); | |
44 | 39 | const treeData = ref<TreeItem[]>([]); |
45 | 40 | const selectedKeys = ref<string[]>(); |
46 | - const treeExpandData = ref([]); | |
41 | + const treeExpandData = ref<string[]>([]); | |
42 | + | |
43 | + const innerProps = ref<OrganizationTreePropsType>({}); | |
44 | + | |
47 | 45 | //获取所有父级id |
48 | - function findForAllId(data = [], arr = []) { | |
46 | + function findForAllId(data: Recordable[] = [], arr: string[] = []) { | |
49 | 47 | for (const item of data) { |
50 | 48 | arr.push(item.id); |
51 | 49 | } |
52 | 50 | return arr; |
53 | 51 | } |
54 | - function handleSelect(keys) { | |
52 | + | |
53 | + function handleSelect(keys: string[]) { | |
54 | + selectedKeys.value = keys; | |
55 | 55 | emit('select', keys[0]); |
56 | 56 | } |
57 | 57 | function resetOrganization() { |
... | ... | @@ -81,9 +81,41 @@ |
81 | 81 | |
82 | 82 | setTreeHeight(); |
83 | 83 | }); |
84 | + | |
84 | 85 | defineExpose({ |
85 | 86 | resetOrganization, |
86 | 87 | }); |
88 | + | |
89 | + const getProps = computed<OrganizationTreePropsType>(() => ({ ...props, ...unref(innerProps) })); | |
90 | + | |
91 | + const getBindData = computed(() => { | |
92 | + return { | |
93 | + title: '组织列表', | |
94 | + toolbar: true, | |
95 | + search: true, | |
96 | + clickRowToExpand: false, | |
97 | + treeData: unref(treeData), | |
98 | + expandedKeys: unref(treeExpandData), | |
99 | + replaceFields: { key: 'id', title: 'name' }, | |
100 | + selectedKeys: unref(selectedKeys), | |
101 | + ...attrs, | |
102 | + ...unref(getProps), | |
103 | + onSelect: (keys: string[]) => { | |
104 | + handleSelect(keys); | |
105 | + unref(getProps)?.onSelect?.(keys?.[0]); | |
106 | + }, | |
107 | + } as BasicTreePropsType; | |
108 | + }); | |
109 | + | |
110 | + const setProps = (props: Partial<OrganizationTreePropsType>) => { | |
111 | + innerProps.value = { ...(props || {}) }; | |
112 | + }; | |
113 | + | |
114 | + emit('register', { | |
115 | + clearSelected: resetOrganization, | |
116 | + getSelectKey: () => unref(selectedKeys)?.[0], | |
117 | + setProps, | |
118 | + } as OrganizationTreeActionType); | |
87 | 119 | </script> |
88 | 120 | |
89 | 121 | <style scoped lang="less"> | ... | ... |
1 | +import { ExtractPropTypes } from 'vue'; | |
2 | +import { basicProps } from '/@/components/Tree/src/props'; | |
3 | +import { OrganizationTreePropsType } from './props'; | |
4 | + | |
5 | +export interface OrganizationTreeActionType { | |
6 | + clearSelected(): void; | |
7 | + getSelectKey(): undefined | string; | |
8 | + setProps(props: Partial<OrganizationTreePropsType>): any; | |
9 | +} | |
10 | + | |
11 | +export type BasicTreePropsType = Partial<ExtractPropTypes<typeof basicProps>>; | ... | ... |
1 | 1 | import type { ComputedRef, Ref } from 'vue'; |
2 | -import { DataActionModeEnum } from '/@/enums/toolEnum'; | |
3 | 2 | |
4 | -export type DynamicProps<T> = { | |
5 | - [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>; | |
6 | -}; | |
3 | +import { DataActionModeEnum } from '/@/enums/toolEnum'; | |
7 | 4 | |
8 | 5 | export interface ModalParamsType<T = Recordable> { |
9 | 6 | mode: DataActionModeEnum; |
... | ... | @@ -11,9 +8,30 @@ export interface ModalParamsType<T = Recordable> { |
11 | 8 | [key: string]: any; |
12 | 9 | } |
13 | 10 | |
11 | +export type DynamicProps<T> = { | |
12 | + [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>; | |
13 | +}; | |
14 | + | |
14 | 15 | export interface DefineComponentsBasicExpose<T = Recordable> { |
15 | 16 | getFieldsValue: () => T; |
16 | 17 | setFieldsValue: (value: T) => any; |
17 | 18 | validate?: () => Promise<any>; |
18 | 19 | resetFieldsValue?: (...args) => any; |
19 | 20 | } |
21 | + | |
22 | +declare global { | |
23 | + interface CardListRenderItem<T = Recordable> { | |
24 | + item: T; | |
25 | + totalHeight: number; | |
26 | + } | |
27 | + | |
28 | + import { DataActionModeEnum } from '/@/enums/toolEnum'; | |
29 | + | |
30 | + interface ModalParamsType<T = Recordable> { | |
31 | + mode: DataActionModeEnum; | |
32 | + record: T; | |
33 | + [key: string]: any; | |
34 | + } | |
35 | +} | |
36 | + | |
37 | +export {}; | ... | ... |