xachary
2023-12-12 f4df2d5a4bd23f346af10580e5ab7de64933bb4c
提交 | 用户 | age
116a1f 1 <template>
V 2   <Tooltip placement="top">
3     <template #title>
4       <span>{{ t('component.table.settingColumn') }}</span>
5     </template>
6     <Popover
7       placement="bottomLeft"
8       trigger="click"
f4df2d 9       @open-change="onOpenChange"
bab28a 10       :overlayClassName="`${prefixCls}__column-list`"
dce3fb 11       :getPopupContainer="getPopupContainer"
116a1f 12     >
V 13       <template #title>
14         <div :class="`${prefixCls}__popover-title`">
15           <Checkbox
16             :indeterminate="indeterminate"
f4df2d 17             v-model:checked="isColumnAllSelected"
X 18             @change="onColumnAllSelectChange"
116a1f 19           >
V 20             {{ t('component.table.settingColumnShow') }}
21           </Checkbox>
22
f4df2d 23           <Checkbox v-model:checked="isIndexColumnShow" @change="onIndexColumnShowChange">
116a1f 24             {{ t('component.table.settingIndexColumnShow') }}
V 25           </Checkbox>
f4df2d 26           <!-- 设置了 rowSelection 才出现 -->
116a1f 27           <Checkbox
f4df2d 28             v-model:checked="isRowSelectionShow"
X 29             @change="onRowSelectionShowChange"
30             v-if="defaultIsRowSelectionShow"
116a1f 31           >
V 32             {{ t('component.table.settingSelectColumnShow') }}
33           </Checkbox>
34
f4df2d 35           <a-button size="small" type="link" @click="onReset">
efbde0 36             {{ t('common.resetText') }}
116a1f 37           </a-button>
V 38         </div>
39       </template>
40
41       <template #content>
42         <ScrollContainer>
f4df2d 43           <Checkbox.Group v-model:value="columnCheckedOptions" ref="columnOptionsRef">
X 44             <template v-for="opt in columnOptions" :key="opt.value">
45               <div :class="`${prefixCls}__check-item`" :data-no="opt.value">
3b3f6c 46                 <DragOutlined class="table-column-drag-icon" />
f4df2d 47                 <Checkbox :value="opt.value">
X 48                   {{ opt.label }}
9edc28 49                 </Checkbox>
116a1f 50
dce3fb 51                 <Tooltip
52                   placement="bottomLeft"
53                   :mouseLeaveDelay="0.4"
54                   :getPopupContainer="getPopupContainer"
55                 >
9edc28 56                   <template #title>
V 57                     {{ t('component.table.settingFixedLeft') }}
58                   </template>
116a1f 59                   <Icon
V 60                     icon="line-md:arrow-align-left"
61                     :class="[
62                       `${prefixCls}__fixed-left`,
63                       {
f4df2d 64                         active: opt.fixed === 'left',
X 65                         disabled: opt.value ? !columnCheckedOptions.includes(opt.value) : true,
116a1f 66                       },
V 67                     ]"
f4df2d 68                     @click="onColumnFixedChange(opt, 'left')"
116a1f 69                   />
V 70                 </Tooltip>
71                 <Divider type="vertical" />
dce3fb 72                 <Tooltip
73                   placement="bottomLeft"
74                   :mouseLeaveDelay="0.4"
75                   :getPopupContainer="getPopupContainer"
76                 >
9edc28 77                   <template #title>
V 78                     {{ t('component.table.settingFixedRight') }}
79                   </template>
116a1f 80                   <Icon
V 81                     icon="line-md:arrow-align-left"
82                     :class="[
83                       `${prefixCls}__fixed-right`,
84                       {
f4df2d 85                         active: opt.fixed === 'right',
X 86                         disabled: opt.value ? !columnCheckedOptions.includes(opt.value) : true,
116a1f 87                       },
V 88                     ]"
f4df2d 89                     @click="onColumnFixedChange(opt, 'right')"
116a1f 90                   />
V 91                 </Tooltip>
92               </div>
93             </template>
bab28a 94           </Checkbox.Group>
116a1f 95         </ScrollContainer>
V 96       </template>
97       <SettingOutlined />
98     </Popover>
99   </Tooltip>
100 </template>
bab28a 101 <script lang="ts" setup>
f4df2d 102   import type { BasicColumn, ColumnOptionsType, ColumnChangeParam } from '../../types/table';
X 103   import { ref, nextTick, unref, computed, useAttrs, watch, onMounted } from 'vue';
116a1f 104   import { Tooltip, Popover, Checkbox, Divider } from 'ant-design-vue';
f4df2d 105   import type { CheckboxChangeEvent } from 'ant-design-vue/lib/checkbox/interface';
116a1f 106   import { SettingOutlined, DragOutlined } from '@ant-design/icons-vue';
762e5d 107   import Icon from '@/components/Icon/Icon.vue';
bab28a 108   import { ScrollContainer } from '@/components/Container';
X 109   import { useI18n } from '@/hooks/web/useI18n';
116a1f 110   import { useTableContext } from '../../hooks/useTableContext';
bab28a 111   import { useDesign } from '@/hooks/web/useDesign';
X 112   import { isFunction, isNil } from '@/utils/is';
113   import { getPopupContainer as getParentContainer } from '@/utils';
f4df2d 114   import { cloneDeep } from 'lodash-es';
a2b594 115   import Sortablejs from 'sortablejs';
L 116   import type Sortable from 'sortablejs';
116a1f 117
f4df2d 118   // 列表设置缓存
X 119   import { useTableSettingStore } from '@/store/modules/tableSetting';
120   import { useRoute } from 'vue-router';
121   import { TableRowSelection } from '@/components/Table/src/types/table';
116a1f 122
f4df2d 123   const tableSettingStore = useTableSettingStore();
116a1f 124
bab28a 125   defineOptions({ name: 'ColumnSetting' });
X 126   const emit = defineEmits(['columns-change']);
116a1f 127
f4df2d 128   const route = useRoute();
X 129
bab28a 130   const { t } = useI18n();
X 131   const { prefixCls } = useDesign('basic-column-setting');
132
f4df2d 133   const attrs = useAttrs();
X 134   const table = useTableContext();
bab28a 135
f4df2d 136   const getPopupContainer = () => {
X 137     return isFunction(attrs.getPopupContainer) ? attrs.getPopupContainer() : getParentContainer();
138   };
bab28a 139
f4df2d 140   // 是否已经从缓存恢复
X 141   let isRestored = false;
142   let isInnerChange = false;
143
144   // 列可选项
145   const columnOptions = ref<ColumnOptionsType[]>([]);
146   const columnOptionsRef = ref(null);
147   // 已选列
148   const columnCheckedOptions = ref<string[]>([]);
149   // 已选变化
150   watch(columnCheckedOptions, () => {
151     // 恢复缓存后生效
152     if (isRestored) {
153       // 显示
154       columnOptions.value
155         .filter((o) => columnCheckedOptions.value.includes(o.value))
156         .forEach((o) => {
157           o.column.defaultHidden = false;
158         });
159       // 隐藏
160       columnOptions.value
161         .filter((o) => !columnCheckedOptions.value.includes(o.value))
162         .forEach((o) => {
163           o.column.defaultHidden = true;
164           o.fixed = undefined;
165         });
166       // 从 列可选项 更新 全选状态
167       isColumnAllSelectedUpdate();
168
169       // 列表列更新
170       tableColumnsUpdate();
171       // 更新列缓存
172       columnOptionsSave();
bab28a 173     }
X 174   });
175
f4df2d 176   // 全选
X 177   const isColumnAllSelected = ref<boolean>(false);
178   const onColumnAllSelectChange = () => {
179     if (columnCheckedOptions.value.length < columnOptions.value.length) {
180       columnCheckedOptions.value = columnOptions.value.map((o) => o.value);
181     } else {
182       columnCheckedOptions.value = [];
183     }
184   };
bab28a 185
f4df2d 186   // 半选状态
X 187   const indeterminate = computed(() => {
188     return (
189       columnCheckedOptions.value.length > 0 &&
190       columnCheckedOptions.value.length < columnOptions.value.length
191     );
192   });
193
194   // 是否显示序号列
195   const isIndexColumnShow = ref<boolean>(false);
196   // 序号列更新
197   const onIndexColumnShowChange = (e: CheckboxChangeEvent) => {
198     // 更新 showIndexColumn
199     showIndexColumnUpdate(e.target.checked);
200     // 更新 showIndexColumn 缓存
201     tableSettingStore.setShowIndexColumn(e.target.checked);
202     // 从无到有需要处理
203     if (e.target.checked) {
204       const columns = cloneDeep(table?.getColumns());
205       const idx = columns.findIndex((o) => o.flag === 'INDEX');
206       // 找到序号列
207       if (idx > -1) {
208         const cache = columns[idx];
209         // 强制左fix
210         cache.fixed = 'left';
211         // 强制移动到 第一/选择列后
212         columns.splice(idx, 1);
213         columns.splice(0, 0, cache);
214         // 设置列表列
215         tableColumnsSet(columns);
216       }
217     }
218   };
219
220   // 是否显示选择列
221   const isRowSelectionShow = ref<boolean>(false);
222   // 选择列更新
223   const onRowSelectionShowChange = (e: CheckboxChangeEvent) => {
224     // 更新 showRowSelection
225     showRowSelectionUpdate(e.target.checked);
226     // 更新 showRowSelection 缓存
227     tableSettingStore.setShowRowSelection(e.target.checked);
228   };
229
230   // 更新列缓存
231   const columnOptionsSave = () => {
232     if (typeof route.name === 'string') {
233       // 按路由 name 作为缓存的key(若一个路由内存在多个表格,需自行调整缓存key来源)
234       tableSettingStore.setColumns(route.name, columnOptions.value);
235     }
236   };
237
238   // 重置
239   const onReset = () => {
240     // 重置默认值
241     isIndexColumnShow.value = defaultIsIndexColumnShow;
242     // 序号列更新
243     onIndexColumnShowChange({
244       target: { checked: defaultIsIndexColumnShow },
245     } as CheckboxChangeEvent);
246     // 重置默认值
247     isRowSelectionShow.value = defaultIsRowSelectionShow;
248     // 选择列更新
249     onRowSelectionShowChange({
250       target: { checked: defaultIsRowSelectionShow },
251     } as CheckboxChangeEvent);
252     // 重置默认值
253     columnOptions.value = cloneDeep(defaultColumnOptions);
254     // 重置排序
255     sortableOrder = defaultSortableOrder;
256     // 排序
257     sortable?.sort(defaultSortableOrder);
258     // 更新表单状态
259     formUpdate();
260   };
261
262   // 设置列的 fixed
263   const onColumnFixedChange = (opt: ColumnOptionsType, type: 'left' | 'right') => {
264     if (type === 'left') {
265       if (!opt.fixed || opt.fixed === 'right') {
266         opt.fixed = 'left';
267       } else {
268         opt.fixed = undefined;
269       }
270     } else if (type === 'right') {
271       if (!opt.fixed || opt.fixed === 'left') {
272         opt.fixed = 'right';
273       } else {
274         opt.fixed = undefined;
275       }
276     }
277
278     // 列表列更新
279     tableColumnsUpdate();
280     // 更新列缓存
281     columnOptionsSave();
282   };
283
284   // 沿用逻辑
285   const sortableFix = async () => {
bab28a 286     // Sortablejs存在bug,不知道在哪个步骤中会向el append了一个childNode,因此这里先清空childNode
X 287     // 有可能复现上述问题的操作:拖拽一个元素,快速的上下移动,最后放到最后的位置中松手
f4df2d 288     if (columnOptionsRef.value) {
X 289       const el = (columnOptionsRef.value as InstanceType<typeof Checkbox.Group>).$el;
bab28a 290       Array.from(el.children).forEach((item) => el.removeChild(item));
X 291     }
292     await nextTick();
f4df2d 293   };
bab28a 294
f4df2d 295   // 列是否显示逻辑
X 296   const columnIfShow = (column?: Partial<Omit<BasicColumn, 'children'>>) => {
297     if (column) {
298       if ('ifShow' in column) {
299         if (typeof column.ifShow === 'boolean') {
300           return column.ifShow;
301         } else if (column.ifShow) {
302           return column.ifShow(column);
bab28a 303         }
f4df2d 304       }
X 305       return true;
bab28a 306     }
f4df2d 307     return false;
X 308   };
bab28a 309
f4df2d 310   // sortable 实例
bab28a 311   let sortable: Sortable;
f4df2d 312   // 排序
bab28a 313   let sortableOrder: string[] = [];
X 314
f4df2d 315   // 获取数据列
X 316   const getTableColumns = () => {
317     return table
318       .getColumns({ ignoreIndex: true, ignoreAction: true })
319       .filter((col) => columnIfShow(col));
320   };
321
322   // 设置列表列
323   const tableColumnsSet = (columns: BasicColumn[]) => {
324     isInnerChange = true;
325     table?.setColumns(columns);
326
327     // 沿用逻辑
328     const columnChangeParams: ColumnChangeParam[] = columns.map((col) => ({
329       dataIndex: col.dataIndex ? col.dataIndex.toString() : '',
330       fixed: col.fixed,
331       visible: !col.defaultHidden,
332     }));
333     emit('columns-change', columnChangeParams);
334   };
335
336   // 列表列更新
337   const tableColumnsUpdate = () => {
338     // 考虑了所有列
339     const columns = cloneDeep(table.getColumns());
340
341     // 从左 fixed 最一列开始排序
342     let count = columns.filter((o) => o.fixed === 'left' || o.fixed === true).length;
343
344     // 按 columnOptions 的排序 调整 table.getColumns() 的顺序和值
345     for (const opt of columnOptions.value) {
346       const colIdx = columns.findIndex((o) => o.dataIndex === opt.value);
347       //
348       if (colIdx > -1) {
349         const target = columns[colIdx];
350         target.defaultHidden = opt.column?.defaultHidden;
351         target.fixed = opt.fixed;
352         columns.splice(colIdx, 1);
353         columns.splice(count++, 0, target); // 递增插入
354       }
355     }
356
357     // 设置列表列
358     tableColumnsSet(columns);
359   };
360
361   // 打开浮窗
362   const onOpenChange = async () => {
363     await nextTick();
364
365     if (columnOptionsRef.value) {
366       // 注册排序实例
367       const el = (columnOptionsRef.value as InstanceType<typeof Checkbox.Group>).$el;
bab28a 368       sortable = Sortablejs.create(unref(el), {
X 369         animation: 500,
370         delay: 400,
371         delayOnTouchOnly: true,
372         handle: '.table-column-drag-icon ',
f4df2d 373         dataIdAttr: 'data-no',
bab28a 374         onEnd: (evt) => {
X 375           const { oldIndex, newIndex } = evt;
376           if (isNil(oldIndex) || isNil(newIndex) || oldIndex === newIndex) {
377             return;
378           }
379
f4df2d 380           const options = cloneDeep(columnOptions.value);
X 381
382           // 排序
bab28a 383           if (oldIndex > newIndex) {
f4df2d 384             options.splice(newIndex, 0, options[oldIndex]);
X 385             options.splice(oldIndex + 1, 1);
bab28a 386           } else {
f4df2d 387             options.splice(newIndex + 1, 0, options[oldIndex]);
X 388             options.splice(oldIndex, 1);
bab28a 389           }
X 390
f4df2d 391           // 更新 列可选项
X 392           columnOptions.value = options;
393
394           // 列表列更新
395           tableColumnsUpdate();
396           // 更新列缓存
397           columnOptionsSave();
bab28a 398         },
X 399       });
400
f4df2d 401       // 从缓存恢复
X 402       sortable.sort(sortableOrder);
bab28a 403     }
f4df2d 404   };
bab28a 405
f4df2d 406   // remove消失的列、push新出现的列
X 407   const diff = () => {
408     if (typeof route.name === 'string') {
409       let cache = tableSettingStore.getColumns(route.name);
410       if (cache) {
411         // value、label是否一致
412         if (
413           JSON.stringify(columnOptions.value.map((o) => ({ value: o.value, label: o.label }))) !==
414           JSON.stringify(cache.map((o) => ({ value: o.value, label: o.label })))
415         ) {
416           const map = columnOptions.value.reduce((map, item) => {
417             map[item.value] = item.label;
418             return map;
419           }, {});
420           if (Array.isArray(cache)) {
421             // remove消失的列
422             cache = cache.filter((o) => map[o.value]);
423             // 更新label
424             cache.forEach((o) => {
425               o.label = map[o.value];
426             });
427             const cacheKeys = cache.map((o) => o.value);
428             // push新出现的列
429             cache = cache.concat(columnOptions.value.filter((o) => !cacheKeys.includes(o.value)));
430             // 更新缓存
431             tableSettingStore.setColumns(route.name, cache);
432           }
433         }
434       }
bab28a 435     }
f4df2d 436   };
bab28a 437
f4df2d 438   // 从缓存恢复
X 439   const restore = () => {
440     // 设置过才恢复
441     if (typeof tableSettingStore.getShowIndexColumn === 'boolean') {
442       isIndexColumnShow.value = tableSettingStore.getShowIndexColumn;
443     }
444     if (typeof tableSettingStore.getShowRowSelection === 'boolean') {
445       isRowSelectionShow.value = defaultIsRowSelectionShow && tableSettingStore.getShowRowSelection;
446     }
447
448     // 序号列更新
449     onIndexColumnShowChange({
450       target: { checked: isIndexColumnShow.value },
451     } as CheckboxChangeEvent);
452     // 选择列更新
453     onRowSelectionShowChange({
454       target: { checked: isRowSelectionShow.value },
455     } as CheckboxChangeEvent);
456
457     if (typeof route.name === 'string') {
458       const cache = tableSettingStore.getColumns(route.name);
459       // 设置过才恢复
460       if (Array.isArray(cache)) {
461         columnOptions.value = cache;
462       }
463     }
464   };
465
466   // 从 列可选项 更新 已选列
467   const columnCheckedOptionsUpdate = () => {
468     columnCheckedOptions.value = columnOptions.value
469       .filter((o) => !o.column?.defaultHidden)
470       .map((o) => o.value);
471   };
472   // 从 列可选项 更新 全选状态
473   const isColumnAllSelectedUpdate = () => {
474     isColumnAllSelected.value = columnOptions.value.length === columnCheckedOptions.value.length;
475   };
476   // 从 列可选项 更新 排序
477   const sortableOrderUpdateByOptions = (options: ColumnOptionsType[]) => {
478     sortableOrder = options.map((o) => o.value);
479   };
480   // 更新 showIndexColumn
481   const showIndexColumnUpdate = (showIndexColumn) => {
482     isInnerChange = true;
483     table.setProps({
484       showIndexColumn,
bab28a 485     });
f4df2d 486   };
X 487   // 更新 rowSelection
488   const showRowSelectionUpdate = (showRowSelection) => {
489     isInnerChange = true;
490     table.setProps({
491       rowSelection: showRowSelection
492         ? {
493             ...defaultRowSelection,
494             fixed: true,
495           }
496         : undefined,
497     });
498   };
bab28a 499
f4df2d 500   // 更新表单状态
X 501   const formUpdate = () => {
502     // 从 列可选项 更新 已选列
503     columnCheckedOptionsUpdate();
bab28a 504
f4df2d 505     // 从 列可选项 更新 全选状态
X 506     isColumnAllSelectedUpdate();
bab28a 507
f4df2d 508     // 从 列可选项 更新 排序
X 509     sortableOrderUpdateByOptions(columnOptions.value);
510
511     // 更新 showIndexColumn
512     showIndexColumnUpdate(isIndexColumnShow.value);
513
514     // 更新 showRowSelection
515     showRowSelectionUpdate(isRowSelectionShow.value);
516
517     // 列表列更新
518     tableColumnsUpdate();
519   };
520
521   // 默认值
522   let defaultIsIndexColumnShow: boolean = false;
523   let defaultIsRowSelectionShow: boolean = false;
524   let defaultRowSelection: TableRowSelection<Recordable<any>>;
525   let defaultColumnOptions: ColumnOptionsType[] = [];
526   let defaultSortableOrder: string[] = [];
527
528   const init = async () => {
529     if (!isRestored) {
530       await sortableFix();
531
532       // 获取数据列
533       const columns = getTableColumns();
534
535       // 沿用逻辑
536       table.setCacheColumns?.(columns);
537
538       // 生成 默认值
539       const options: ColumnOptionsType[] = [];
540       for (const col of columns) {
541         // 只缓存 string 类型的列
542         options.push({
543           label:
544             typeof col.title === 'string'
545               ? col.title
546               : col.customTitle === 'string'
547               ? col.customTitle
548               : '',
549           value:
550             typeof col.dataIndex === 'string'
551               ? col.dataIndex
552               : col.title === 'string'
553               ? col.title
554               : '',
555           column: {
556             defaultHidden: col.defaultHidden,
557           },
558           fixed: col.fixed,
559         });
560       }
561
562       // 默认值 缓存,浮窗出现的时候使用
563       defaultSortableOrder = options.map((o) => o.value);
564
565       // 默认值 缓存
566       defaultIsIndexColumnShow = table.getBindValues.value.showIndexColumn || false;
567       defaultRowSelection = table.getRowSelection();
568       defaultIsRowSelectionShow = !!defaultRowSelection; // 设置了 rowSelection 才出现
569       defaultColumnOptions = options;
570
571       // 默认值 赋值
572       isIndexColumnShow.value = defaultIsIndexColumnShow;
573       isRowSelectionShow.value = defaultIsRowSelectionShow;
574       columnOptions.value = cloneDeep(options);
575
576       // remove消失的列、push新出现的列
577       diff();
578
579       // 从缓存恢复
580       restore();
581
582       // 更新表单状态
583       formUpdate();
584
585       isRestored = true;
586     }
587   };
588
589   // 初始化
590   init();
591
592   // 外部列改变
593   const getColumns = computed(() => {
594     return table?.getColumns();
595   });
596   const getValues = computed(() => {
597     return table?.getBindValues;
598   });
599
600   onMounted(() => {
601     watch([getColumns, getValues], () => {
602       if (!isInnerChange) {
603         isRestored = false;
604         console.log('onMounted isRestored');
605         init();
606       } else {
607         isInnerChange = false;
bab28a 608       }
X 609     });
f4df2d 610   });
116a1f 611 </script>
V 612 <style lang="less">
613   @prefix-cls: ~'@{namespace}-basic-column-setting';
614
3b3f6c 615   .table-column-drag-icon {
116a1f 616     margin: 0 5px;
V 617     cursor: move;
618   }
619
620   .@{prefix-cls} {
621     &__popover-title {
622       display: flex;
ba2415 623       position: relative;
116a1f 624       align-items: center;
V 625       justify-content: space-between;
626     }
627
628     &__check-item {
629       display: flex;
630       align-items: center;
631       min-width: 100%;
632       padding: 4px 16px 8px 0;
633
634       .ant-checkbox-wrapper {
635         width: 100%;
636
637         &:hover {
638           color: @primary-color;
639         }
640       }
641     }
642
643     &__fixed-left,
644     &__fixed-right {
acea18 645       color: rgb(0 0 0 / 45%);
116a1f 646       cursor: pointer;
V 647
648       &.active,
649       &:hover {
650         color: @primary-color;
651       }
652
653       &.disabled {
654         color: @disabled-color;
655         cursor: not-allowed;
656       }
657     }
658
659     &__fixed-right {
660       transform: rotate(180deg);
661     }
662
bab28a 663     &__column-list {
116a1f 664       svg {
V 665         width: 1em !important;
666         height: 1em !important;
667       }
668
669       .ant-popover-inner-content {
670         // max-height: 360px;
671         padding-right: 0;
672         padding-left: 0;
673         // overflow: auto;
674       }
675
676       .ant-checkbox-group {
e47787 677         display: inline-block;
116a1f 678         width: 100%;
V 679         min-width: 260px;
680         // flex-wrap: wrap;
681       }
682
0e7c57 683       .scrollbar {
116a1f 684         height: 220px;
V 685       }
686     }
687   }
688 </style>