Commit 9a4aa30b670f58293a6217db4d0733ca284d4936

Authored by ww
1 parent 80ee6a6f

feat: BasicCardList 组件新增选择功能

@@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
12 import { FETCH_SETTING } from '../../Table/src/const'; 12 import { FETCH_SETTING } from '../../Table/src/const';
13 import { useCardForm } from './hooks/useCardForm'; 13 import { useCardForm } from './hooks/useCardForm';
14 import { Icon } from '/@/components/Icon'; 14 import { Icon } from '/@/components/Icon';
  15 + import { useCardListSelected } from './hooks/useCardListSelected';
15 16
16 const emit = defineEmits<{ 17 const emit = defineEmits<{
17 (eventName: 'fetchSuccess', result: { items: Recordable[]; total: number }): void; 18 (eventName: 'fetchSuccess', result: { items: Recordable[]; total: number }): void;
@@ -33,6 +34,7 @@ @@ -33,6 +34,7 @@
33 immediate: true, 34 immediate: true,
34 fetchSetting: () => FETCH_SETTING, 35 fetchSetting: () => FETCH_SETTING,
35 autoCreateKey: true, 36 autoCreateKey: true,
  37 + offsetHeight: 0,
36 }); 38 });
37 39
38 const tableData = ref<Recordable[]>([]); 40 const tableData = ref<Recordable[]>([]);
@@ -56,7 +58,15 @@ @@ -56,7 +58,15 @@
56 58
57 const { loading, setLoading, getLoading } = useLoading(); 59 const { loading, setLoading, getLoading } = useLoading();
58 60
59 - const { getDataSourceRef, getRowKey, fetch, reload } = useCardListData(getProps, formActions, { 61 + const {
  62 + getDataSourceRef,
  63 + getRowKey,
  64 + fetch,
  65 + reload,
  66 + updateTableDataRecord,
  67 + findCardDataRecord,
  68 + setCardListData,
  69 + } = useCardListData(getProps, formActions, {
60 setLoading, 70 setLoading,
61 getPaginationInfo, 71 getPaginationInfo,
62 emit, 72 emit,
@@ -77,14 +87,30 @@ @@ -77,14 +87,30 @@
77 getLoading 87 getLoading
78 ); 88 );
79 89
  90 + const {
  91 + handlerSelected,
  92 + selectedClass,
  93 + selectedKeys,
  94 + selectAllToggle,
  95 + selectedAll,
  96 + clearSelectedKeys,
  97 + getSelectedKeys,
  98 + getSelectedRecords,
  99 + } = useCardListSelected(getDataSourceRef, getProps, {
  100 + updateTableDataRecord,
  101 + findCardDataRecord,
  102 + getRowKey,
  103 + setCardListData,
  104 + });
  105 +
80 function handleCardListChange() { 106 function handleCardListChange() {
81 reload(); 107 reload();
82 } 108 }
83 109
84 - const getBindData = computed<ListProps>(() => { 110 + const getBindValues = computed<ListProps>(() => {
85 const dataSource = unref(getDataSourceRef); 111 const dataSource = unref(getDataSourceRef);
86 112
87 - return { 113 + const props = {
88 dataSource, 114 dataSource,
89 grid: unref(getListGrid), 115 grid: unref(getListGrid),
90 loading: unref(loading), 116 loading: unref(loading),
@@ -95,8 +121,9 @@ @@ -95,8 +121,9 @@
95 handleCardListChange(); 121 handleCardListChange();
96 }, 122 },
97 }, 123 },
98 - rowKey: unref(getRowKey), 124 + rowKey: (record: Recordable) => record[unref(getRowKey)],
99 } as ListProps; 125 } as ListProps;
  126 + return props;
100 }); 127 });
101 128
102 watch(cardListLayout, () => { 129 watch(cardListLayout, () => {
@@ -111,6 +138,10 @@ @@ -111,6 +138,10 @@
111 setPagination, 138 setPagination,
112 getPagination, 139 getPagination,
113 reload, 140 reload,
  141 + selectedAll,
  142 + clearSelectedKeys,
  143 + getSelectedKeys,
  144 + getSelectedRecords,
114 }; 145 };
115 146
116 function setProps(props: Partial<BasicCardListPropsType> = {}) { 147 function setProps(props: Partial<BasicCardListPropsType> = {}) {
@@ -137,7 +168,7 @@ @@ -137,7 +168,7 @@
137 </section> 168 </section>
138 169
139 <section class="w-full bg-light-50 dark:bg-dark-900 p-4 flex-auto h-full"> 170 <section class="w-full bg-light-50 dark:bg-dark-900 p-4 flex-auto h-full">
140 - <List ref="listElRef" v-bind="getBindData"> 171 + <List ref="listElRef" v-bind="getBindValues">
141 <template #header> 172 <template #header>
142 <section class="flex justify-between items-center"> 173 <section class="flex justify-between items-center">
143 <div> 174 <div>
@@ -150,6 +181,9 @@ @@ -150,6 +181,9 @@
150 <slot name="toolbar"></slot> 181 <slot name="toolbar"></slot>
151 </div> 182 </div>
152 <div v-if="showCardListSetting" class="flex gap-4"> 183 <div v-if="showCardListSetting" class="flex gap-4">
  184 + <Button v-if="getProps.selections" @click="selectAllToggle" type="primary">
  185 + {{ selectedKeys.length === getDataSourceRef.length ? '反选' : '全选' }}
  186 + </Button>
153 <CardListLayout v-model:row="cardListLayout.row" v-model:col="cardListLayout.col" /> 187 <CardListLayout v-model:row="cardListLayout.row" v-model:col="cardListLayout.col" />
154 <Tooltip title="刷新"> 188 <Tooltip title="刷新">
155 <Button type="primary" @click="reload()" :loading="loading"> 189 <Button type="primary" @click="reload()" :loading="loading">
@@ -159,9 +193,18 @@ @@ -159,9 +193,18 @@
159 </div> 193 </div>
160 </section> 194 </section>
161 </template> 195 </template>
162 - <template #renderItem="{ item }">  
163 - <List.Item :style="{ '--totalHeight': containerHeight }">  
164 - <slot name="renderItem" :item="item" :totalHeight="containerHeight"></slot> 196 + <template #renderItem="{ item }: CardListRenderItem">
  197 + <List.Item
  198 + :class="selectedKeys.includes(item[getRowKey]) ? selectedClass : ''"
  199 + @click="handlerSelected($event, item)"
  200 + >
  201 + <slot
  202 + name="renderItem"
  203 + :item="item"
  204 + :totalHeight="containerHeight"
  205 + :checked="item?.checked"
  206 + >
  207 + </slot>
165 </List.Item> 208 </List.Item>
166 </template> 209 </template>
167 </List> 210 </List>
@@ -175,6 +218,38 @@ @@ -175,6 +218,38 @@
175 .ant-spin-container { 218 .ant-spin-container {
176 overflow-x: hidden; 219 overflow-x: hidden;
177 overflow-y: auto; 220 overflow-y: auto;
  221 +
  222 + .ant-list-item {
  223 + border: 2px transparent solid;
  224 + position: relative;
  225 +
  226 + &.basic-card-list-item-checked {
  227 + border: 2px solid #377dff;
  228 +
  229 + &::after {
  230 + position: absolute;
  231 + inset-block-start: 2px;
  232 + inset-inline-end: 2px;
  233 + width: 0;
  234 + height: 0;
  235 + border: 10px solid #1677ff;
  236 + border-block-end: 10px solid transparent;
  237 + border-inline-start: 10px solid transparent;
  238 + // border-start-end-radius: 6px;
  239 + content: '';
  240 + }
  241 +
  242 + &::before {
  243 + content: '';
  244 + position: absolute;
  245 + background-color: #53a2fd;
  246 + width: 100%;
  247 + height: 100%;
  248 + opacity: 0.2;
  249 + z-index: 99;
  250 + }
  251 + }
  252 + }
178 } 253 }
179 254
180 .ant-list-header { 255 .ant-list-header {
1 -import { WatchStopHandle, onUnmounted, ref, unref, watch } from 'vue'; 1 +import { WatchStopHandle, ref, unref, watch } from 'vue';
2 import { BasicCardListPropsType, CardListActionType } from '../types'; 2 import { BasicCardListPropsType, CardListActionType } from '../types';
3 import { isProdMode } from '/@/utils/env'; 3 import { isProdMode } from '/@/utils/env';
4 import { error } from '/@/utils/log'; 4 import { error } from '/@/utils/log';
@@ -6,6 +6,7 @@ import { FormActionType } from '/@/components/Form'; @@ -6,6 +6,7 @@ import { FormActionType } from '/@/components/Form';
6 import { getDynamicProps } from '/@/utils'; 6 import { getDynamicProps } from '/@/utils';
7 import { PaginationProps } from 'ant-design-vue'; 7 import { PaginationProps } from 'ant-design-vue';
8 import { FetchParams } from './useCardListData'; 8 import { FetchParams } from './useCardListData';
  9 +import { tryOnUnmounted } from '@vueuse/core';
9 10
10 type UseTableMethod = CardListActionType & { 11 type UseTableMethod = CardListActionType & {
11 getForm: () => FormActionType; 12 getForm: () => FormActionType;
@@ -22,7 +23,7 @@ export function useCardList( @@ -22,7 +23,7 @@ export function useCardList(
22 23
23 function register(instance: CardListActionType, formInstance: FormActionType) { 24 function register(instance: CardListActionType, formInstance: FormActionType) {
24 isProdMode() && 25 isProdMode() &&
25 - onUnmounted(() => { 26 + tryOnUnmounted(() => {
26 cardListRef.value = null; 27 cardListRef.value = null;
27 loadedRef.value = null; 28 loadedRef.value = null;
28 }); 29 });
@@ -77,6 +78,18 @@ export function useCardList( @@ -77,6 +78,18 @@ export function useCardList(
77 reload: (opt?: FetchParams) => { 78 reload: (opt?: FetchParams) => {
78 return getTableInstance().reload(opt); 79 return getTableInstance().reload(opt);
79 }, 80 },
  81 + selectedAll: () => {
  82 + return getTableInstance().selectedAll();
  83 + },
  84 + clearSelectedKeys: () => {
  85 + return getTableInstance().clearSelectedKeys();
  86 + },
  87 + getSelectedKeys: () => {
  88 + return getTableInstance().getSelectedKeys();
  89 + },
  90 + getSelectedRecords: () => {
  91 + return getTableInstance().getSelectedRecords();
  92 + },
80 }; 93 };
81 94
82 return [register, cardListActionType]; 95 return [register, cardListActionType];
@@ -49,25 +49,22 @@ export function useCardListData( @@ -49,25 +49,22 @@ export function useCardListData(
49 } 49 }
50 ); 50 );
51 51
52 - function setTableKey(items: any[]) { 52 + function setCardKey(items: any[]) {
53 if (!items || !Array.isArray(items)) return; 53 if (!items || !Array.isArray(items)) return;
54 items.forEach((item) => { 54 items.forEach((item) => {
55 if (!item[ROW_KEY]) { 55 if (!item[ROW_KEY]) {
56 item[ROW_KEY] = buildUUID(); 56 item[ROW_KEY] = buildUUID();
57 } 57 }
58 - if (item.children && item.children.length) {  
59 - setTableKey(item.children);  
60 - }  
61 }); 58 });
62 } 59 }
63 60
64 - const getAutoCreateKey = computed(() => {  
65 - return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey; 61 + const getAutoCreateKey = computed<boolean>(() => {
  62 + return !!(unref(propsRef).autoCreateKey && !unref(propsRef).rowKey);
66 }); 63 });
67 64
68 - const getRowKey = computed(() => { 65 + const getRowKey = computed<string | number>(() => {
69 const { rowKey } = unref(propsRef); 66 const { rowKey } = unref(propsRef);
70 - return unref(getAutoCreateKey) ? ROW_KEY : rowKey; 67 + return unref(getAutoCreateKey) ? ROW_KEY : rowKey!;
71 }); 68 });
72 69
73 const getDataSourceRef = computed(() => { 70 const getDataSourceRef = computed(() => {
@@ -87,13 +84,14 @@ export function useCardListData( @@ -87,13 +84,14 @@ export function useCardListData(
87 item[ROW_KEY] = buildUUID(); 84 item[ROW_KEY] = buildUUID();
88 } 85 }
89 if (item.children && item.children.length) { 86 if (item.children && item.children.length) {
90 - setTableKey(item.children); 87 + setCardKey(item.children);
91 } 88 }
92 }); 89 });
93 dataSourceRef.value = data; 90 dataSourceRef.value = data;
94 } 91 }
95 } 92 }
96 } 93 }
  94 +
97 return unref(dataSourceRef); 95 return unref(dataSourceRef);
98 }); 96 });
99 97
@@ -176,10 +174,47 @@ export function useCardListData( @@ -176,10 +174,47 @@ export function useCardListData(
176 } 174 }
177 } 175 }
178 176
  177 + function findCardDataRecord(rowKey: string | number) {
  178 + if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
  179 +
  180 + const rowKeyName = unref(getRowKey);
  181 + if (!rowKeyName) return;
  182 +
  183 + const findRow = (array: any[]) => {
  184 + let ret;
  185 + array.some(function iter(r) {
  186 + if (Reflect.has(r, rowKeyName) && r[rowKeyName] === rowKey) {
  187 + ret = r;
  188 + return true;
  189 + }
  190 + });
  191 + return ret;
  192 + };
  193 +
  194 + return findRow(dataSourceRef.value);
  195 + }
  196 +
  197 + function updateTableDataRecord(
  198 + rowKey: string | number,
  199 + record: Recordable
  200 + ): Recordable | undefined {
  201 + const row = findCardDataRecord(rowKey);
  202 + if (row) {
  203 + for (const field in row) {
  204 + if (Reflect.has(record, field)) row[field] = record[field];
  205 + }
  206 + return row;
  207 + }
  208 + }
  209 +
179 async function reload(opt?: FetchParams) { 210 async function reload(opt?: FetchParams) {
180 await fetch(opt); 211 await fetch(opt);
181 } 212 }
182 213
  214 + function setCardListData<T = Recordable>(values: T[]) {
  215 + dataSourceRef.value = values as Recordable[];
  216 + }
  217 +
183 onMounted(() => { 218 onMounted(() => {
184 useTimeoutFn(() => { 219 useTimeoutFn(() => {
185 unref(propsRef).immediate && fetch(); 220 unref(propsRef).immediate && fetch();
@@ -191,5 +226,8 @@ export function useCardListData( @@ -191,5 +226,8 @@ export function useCardListData(
191 fetch, 226 fetch,
192 getRowKey, 227 getRowKey,
193 getDataSourceRef, 228 getDataSourceRef,
  229 + setCardListData,
  230 + findCardDataRecord,
  231 + updateTableDataRecord,
194 }; 232 };
195 } 233 }
1 -import { ComputedRef, computed, unref } from 'vue'; 1 +import { ComputedRef, computed, ref, toRaw, unref, watch } from 'vue';
  2 +import { BasicCardListPropsType, CardListSelectionsType, UseCardListDataType } from '../types';
  3 +import { isBoolean } from '/@/utils/is';
  4 +import { cloneDeep } from 'lodash-es';
  5 +
  6 +export const CHECKED_FIELD = 'CARD_LIST_CHECKED_STATUS';
  7 +
  8 +export const DEFAULT_SELECTED_CLASS = 'basic-card-list-item-checked';
  9 +
  10 +interface CardListSelectActionType {
  11 + updateTableDataRecord: UseCardListDataType['updateTableDataRecord'];
  12 + findCardDataRecord: UseCardListDataType['findCardDataRecord'];
  13 + getRowKey: UseCardListDataType['getRowKey'];
  14 + setCardListData: UseCardListDataType['setCardListData'];
  15 +}
2 16
3 export function useCardListSelected( 17 export function useCardListSelected(
4 - getDataSourceRef: ComputedRef<(Recordable & { checked?: boolean })[]> 18 + getDataSourceRef: ComputedRef<Recordable[]>,
  19 + propsRef: ComputedRef<BasicCardListPropsType>,
  20 + { getRowKey }: CardListSelectActionType
5 ) { 21 ) {
  22 + const selectedKeys = ref<string[]>([]);
  23 +
  24 + const selectedClass = ref('');
  25 +
6 const getHasSelectedRecordStatus = computed(() => 26 const getHasSelectedRecordStatus = computed(() =>
7 unref(getDataSourceRef).find((item) => item.checked) 27 unref(getDataSourceRef).find((item) => item.checked)
8 ); 28 );
9 29
10 - // function handlerSelect 30 + const getShouldUseDefaultStyle = computed(
  31 + () =>
  32 + isBoolean(unref(propsRef).selections) ||
  33 + !(unref(propsRef).selections as CardListSelectionsType).customCheckedStyle
  34 + );
  35 +
  36 + watch(
  37 + () => unref(propsRef).selections,
  38 + () => {
  39 + selectedClass.value =
  40 + isBoolean(unref(propsRef).selections) ||
  41 + !(unref(propsRef).selections as CardListSelectionsType).customCheckedStyle
  42 + ? DEFAULT_SELECTED_CLASS
  43 + : '';
  44 + },
  45 + {
  46 + immediate: true,
  47 + }
  48 + );
  49 +
  50 + function handlerSelected(_event: MouseEvent, record: Recordable) {
  51 + if (!unref(propsRef).selections) return;
  52 + const rowKey = record[unref(getRowKey)];
  53 + const index = unref(selectedKeys).findIndex((key) => key === rowKey);
  54 + ~index ? selectedKeys.value.splice(index, 1) : unref(selectedKeys).push(rowKey);
  55 +
  56 + if (isBoolean(unref(propsRef).selections)) return;
  57 +
  58 + (unref(propsRef).selections as CardListSelectionsType)?.onSelect?.(record, !!~index);
  59 + }
  60 +
  61 + function selectedAll() {
  62 + selectedKeys.value = unref(getDataSourceRef).map((item) => item[unref(getRowKey)]);
  63 +
  64 + if (isBoolean(unref(propsRef).selections)) return;
  65 +
  66 + (unref(propsRef).selections as CardListSelectionsType)?.onSelectAll?.(
  67 + toRaw(unref(getDataSourceRef))
  68 + );
  69 + }
  70 +
  71 + function clearSelectedKeys() {
  72 + selectedKeys.value = [];
  73 + }
  74 +
  75 + function getSelectedKeys() {
  76 + return toRaw(unref(selectedKeys));
  77 + }
  78 +
  79 + function getSelectedRecords() {
  80 + const data = cloneDeep(unref(getDataSourceRef));
  81 + return data.filter((item) => unref(selectedKeys).includes(item[unref(getRowKey)]));
  82 + }
  83 +
  84 + function selectAllToggle() {
  85 + unref(getDataSourceRef).length === unref(selectedKeys).length
  86 + ? (selectedKeys.value = [])
  87 + : selectedAll();
  88 + }
11 89
12 return { 90 return {
  91 + selectedKeys,
  92 + selectedClass,
  93 + getShouldUseDefaultStyle,
13 getHasSelectedRecordStatus, 94 getHasSelectedRecordStatus,
  95 + handlerSelected,
  96 + selectedAll,
  97 + selectAllToggle,
  98 + clearSelectedKeys,
  99 + getSelectedKeys,
  100 + getSelectedRecords,
14 }; 101 };
15 } 102 }
@@ -4,6 +4,7 @@ import { FetchSetting } from '../../Table'; @@ -4,6 +4,7 @@ import { FetchSetting } from '../../Table';
4 import { useLoading } from './hooks/useLoading'; 4 import { useLoading } from './hooks/useLoading';
5 import { usePagination } from './hooks/usePagination'; 5 import { usePagination } from './hooks/usePagination';
6 import { FetchParams, useCardListData } from './hooks/useCardListData'; 6 import { FetchParams, useCardListData } from './hooks/useCardListData';
  7 +import { useCardListSelected } from './hooks/useCardListSelected';
7 8
8 // export interface CardList 9 // export interface CardList
9 10
@@ -22,11 +23,11 @@ export interface BasicCardListPropsType<T = Recordable> { @@ -22,11 +23,11 @@ export interface BasicCardListPropsType<T = Recordable> {
22 fetchSetting?: FetchSetting; 23 fetchSetting?: FetchSetting;
23 afterFetch?: (items: T[], result: any) => Promise<any[]>; 24 afterFetch?: (items: T[], result: any) => Promise<any[]>;
24 autoCreateKey?: boolean; 25 autoCreateKey?: boolean;
25 - rowKey?: (item: T) => string | number; 26 + rowKey?: string | number;
26 immediate?: boolean; 27 immediate?: boolean;
27 handleSearchInfoFn?: Fn; 28 handleSearchInfoFn?: Fn;
28 baseLayout?: Record<'row' | 'col', number>; 29 baseLayout?: Record<'row' | 'col', number>;
29 - selections?: boolean; 30 + selections?: boolean | CardListSelectionsType;
30 } 31 }
31 32
32 export type ListGridType = Record<'column' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', number> & { 33 export type ListGridType = Record<'column' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', number> & {
@@ -36,14 +37,21 @@ export type ListGridType = Record<'column' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | @@ -36,14 +37,21 @@ export type ListGridType = Record<'column' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' |
36 export type UseLoading = ReturnType<typeof useLoading>; 37 export type UseLoading = ReturnType<typeof useLoading>;
37 38
38 export type UsePaginationType = ReturnType<typeof usePagination>; 39 export type UsePaginationType = ReturnType<typeof usePagination>;
  40 +
39 export type UseCardListDataType = ReturnType<typeof useCardListData>; 41 export type UseCardListDataType = ReturnType<typeof useCardListData>;
40 42
  43 +export type UseCardListSelected = ReturnType<typeof useCardListSelected>;
  44 +
41 export interface CardListActionType { 45 export interface CardListActionType {
42 setProps: (props: Partial<BasicCardListPropsType>) => void; 46 setProps: (props: Partial<BasicCardListPropsType>) => void;
43 setLoading: UseLoading['setLoading']; 47 setLoading: UseLoading['setLoading'];
44 setPagination: UsePaginationType['setPagination']; 48 setPagination: UsePaginationType['setPagination'];
45 getPagination: UsePaginationType['getPagination']; 49 getPagination: UsePaginationType['getPagination'];
46 reload: UseCardListDataType['reload']; 50 reload: UseCardListDataType['reload'];
  51 + selectedAll: UseCardListSelected['selectedAll'];
  52 + clearSelectedKeys: UseCardListSelected['clearSelectedKeys'];
  53 + getSelectedKeys: UseCardListSelected['getSelectedKeys'];
  54 + getSelectedRecords: UseCardListSelected['getSelectedRecords'];
47 } 55 }
48 56
49 export interface CardListEmitType { 57 export interface CardListEmitType {
@@ -51,13 +59,8 @@ export interface CardListEmitType { @@ -51,13 +59,8 @@ export interface CardListEmitType {
51 (eventName: 'fetchError', error: Error): void; 59 (eventName: 'fetchError', error: Error): void;
52 } 60 }
53 61
54 -export interface CardListRenderItem<T = Recordable & { checked?: boolean }> {  
55 - item: T;  
56 - totalHeight: number;  
57 -}  
58 -  
59 export interface CardListSelectionsType<T = Recordable> { 62 export interface CardListSelectionsType<T = Recordable> {
  63 + customCheckedStyle?: boolean;
60 onSelect?: (record: T, selected: boolean) => any; 64 onSelect?: (record: T, selected: boolean) => any;
61 onSelectAll: (selectedRecords: T[]) => any; 65 onSelectAll: (selectedRecords: T[]) => any;
62 - onSelectInvert: (selectedRecords: T[]) => any;  
63 } 66 }