Commit 9a4aa30b670f58293a6217db4d0733ca284d4936

Authored by ww
1 parent 80ee6a6f

feat: BasicCardList 组件新增选择功能

... ... @@ -12,6 +12,7 @@
12 12 import { FETCH_SETTING } from '../../Table/src/const';
13 13 import { useCardForm } from './hooks/useCardForm';
14 14 import { Icon } from '/@/components/Icon';
  15 + import { useCardListSelected } from './hooks/useCardListSelected';
15 16
16 17 const emit = defineEmits<{
17 18 (eventName: 'fetchSuccess', result: { items: Recordable[]; total: number }): void;
... ... @@ -33,6 +34,7 @@
33 34 immediate: true,
34 35 fetchSetting: () => FETCH_SETTING,
35 36 autoCreateKey: true,
  37 + offsetHeight: 0,
36 38 });
37 39
38 40 const tableData = ref<Recordable[]>([]);
... ... @@ -56,7 +58,15 @@
56 58
57 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 70 setLoading,
61 71 getPaginationInfo,
62 72 emit,
... ... @@ -77,14 +87,30 @@
77 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 106 function handleCardListChange() {
81 107 reload();
82 108 }
83 109
84   - const getBindData = computed<ListProps>(() => {
  110 + const getBindValues = computed<ListProps>(() => {
85 111 const dataSource = unref(getDataSourceRef);
86 112
87   - return {
  113 + const props = {
88 114 dataSource,
89 115 grid: unref(getListGrid),
90 116 loading: unref(loading),
... ... @@ -95,8 +121,9 @@
95 121 handleCardListChange();
96 122 },
97 123 },
98   - rowKey: unref(getRowKey),
  124 + rowKey: (record: Recordable) => record[unref(getRowKey)],
99 125 } as ListProps;
  126 + return props;
100 127 });
101 128
102 129 watch(cardListLayout, () => {
... ... @@ -111,6 +138,10 @@
111 138 setPagination,
112 139 getPagination,
113 140 reload,
  141 + selectedAll,
  142 + clearSelectedKeys,
  143 + getSelectedKeys,
  144 + getSelectedRecords,
114 145 };
115 146
116 147 function setProps(props: Partial<BasicCardListPropsType> = {}) {
... ... @@ -137,7 +168,7 @@
137 168 </section>
138 169
139 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 172 <template #header>
142 173 <section class="flex justify-between items-center">
143 174 <div>
... ... @@ -150,6 +181,9 @@
150 181 <slot name="toolbar"></slot>
151 182 </div>
152 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 187 <CardListLayout v-model:row="cardListLayout.row" v-model:col="cardListLayout.col" />
154 188 <Tooltip title="刷新">
155 189 <Button type="primary" @click="reload()" :loading="loading">
... ... @@ -159,9 +193,18 @@
159 193 </div>
160 194 </section>
161 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 208 </List.Item>
166 209 </template>
167 210 </List>
... ... @@ -175,6 +218,38 @@
175 218 .ant-spin-container {
176 219 overflow-x: hidden;
177 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 255 .ant-list-header {
... ...
1   -import { WatchStopHandle, onUnmounted, ref, unref, watch } from 'vue';
  1 +import { WatchStopHandle, ref, unref, watch } from 'vue';
2 2 import { BasicCardListPropsType, CardListActionType } from '../types';
3 3 import { isProdMode } from '/@/utils/env';
4 4 import { error } from '/@/utils/log';
... ... @@ -6,6 +6,7 @@ import { FormActionType } from '/@/components/Form';
6 6 import { getDynamicProps } from '/@/utils';
7 7 import { PaginationProps } from 'ant-design-vue';
8 8 import { FetchParams } from './useCardListData';
  9 +import { tryOnUnmounted } from '@vueuse/core';
9 10
10 11 type UseTableMethod = CardListActionType & {
11 12 getForm: () => FormActionType;
... ... @@ -22,7 +23,7 @@ export function useCardList(
22 23
23 24 function register(instance: CardListActionType, formInstance: FormActionType) {
24 25 isProdMode() &&
25   - onUnmounted(() => {
  26 + tryOnUnmounted(() => {
26 27 cardListRef.value = null;
27 28 loadedRef.value = null;
28 29 });
... ... @@ -77,6 +78,18 @@ export function useCardList(
77 78 reload: (opt?: FetchParams) => {
78 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 95 return [register, cardListActionType];
... ...
... ... @@ -49,25 +49,22 @@ export function useCardListData(
49 49 }
50 50 );
51 51
52   - function setTableKey(items: any[]) {
  52 + function setCardKey(items: any[]) {
53 53 if (!items || !Array.isArray(items)) return;
54 54 items.forEach((item) => {
55 55 if (!item[ROW_KEY]) {
56 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 66 const { rowKey } = unref(propsRef);
70   - return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
  67 + return unref(getAutoCreateKey) ? ROW_KEY : rowKey!;
71 68 });
72 69
73 70 const getDataSourceRef = computed(() => {
... ... @@ -87,13 +84,14 @@ export function useCardListData(
87 84 item[ROW_KEY] = buildUUID();
88 85 }
89 86 if (item.children && item.children.length) {
90   - setTableKey(item.children);
  87 + setCardKey(item.children);
91 88 }
92 89 });
93 90 dataSourceRef.value = data;
94 91 }
95 92 }
96 93 }
  94 +
97 95 return unref(dataSourceRef);
98 96 });
99 97
... ... @@ -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 210 async function reload(opt?: FetchParams) {
180 211 await fetch(opt);
181 212 }
182 213
  214 + function setCardListData<T = Recordable>(values: T[]) {
  215 + dataSourceRef.value = values as Recordable[];
  216 + }
  217 +
183 218 onMounted(() => {
184 219 useTimeoutFn(() => {
185 220 unref(propsRef).immediate && fetch();
... ... @@ -191,5 +226,8 @@ export function useCardListData(
191 226 fetch,
192 227 getRowKey,
193 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 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 26 const getHasSelectedRecordStatus = computed(() =>
7 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 90 return {
  91 + selectedKeys,
  92 + selectedClass,
  93 + getShouldUseDefaultStyle,
13 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 4 import { useLoading } from './hooks/useLoading';
5 5 import { usePagination } from './hooks/usePagination';
6 6 import { FetchParams, useCardListData } from './hooks/useCardListData';
  7 +import { useCardListSelected } from './hooks/useCardListSelected';
7 8
8 9 // export interface CardList
9 10
... ... @@ -22,11 +23,11 @@ export interface BasicCardListPropsType<T = Recordable> {
22 23 fetchSetting?: FetchSetting;
23 24 afterFetch?: (items: T[], result: any) => Promise<any[]>;
24 25 autoCreateKey?: boolean;
25   - rowKey?: (item: T) => string | number;
  26 + rowKey?: string | number;
26 27 immediate?: boolean;
27 28 handleSearchInfoFn?: Fn;
28 29 baseLayout?: Record<'row' | 'col', number>;
29   - selections?: boolean;
  30 + selections?: boolean | CardListSelectionsType;
30 31 }
31 32
32 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 37 export type UseLoading = ReturnType<typeof useLoading>;
37 38
38 39 export type UsePaginationType = ReturnType<typeof usePagination>;
  40 +
39 41 export type UseCardListDataType = ReturnType<typeof useCardListData>;
40 42
  43 +export type UseCardListSelected = ReturnType<typeof useCardListSelected>;
  44 +
41 45 export interface CardListActionType {
42 46 setProps: (props: Partial<BasicCardListPropsType>) => void;
43 47 setLoading: UseLoading['setLoading'];
44 48 setPagination: UsePaginationType['setPagination'];
45 49 getPagination: UsePaginationType['getPagination'];
46 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 57 export interface CardListEmitType {
... ... @@ -51,13 +59,8 @@ export interface CardListEmitType {
51 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 62 export interface CardListSelectionsType<T = Recordable> {
  63 + customCheckedStyle?: boolean;
60 64 onSelect?: (record: T, selected: boolean) => any;
61 65 onSelectAll: (selectedRecords: T[]) => any;
62   - onSelectInvert: (selectedRecords: T[]) => any;
63 66 }
... ...