useCustomSelection.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. import type { BasicColumn } from '/@/components/Table';
  2. import type { Ref, ComputedRef } from 'vue';
  3. import type { BasicTableProps, PaginationProps, TableRowSelection } from '/@/components/Table';
  4. import { computed, nextTick, onUnmounted, ref, toRaw, unref, watch, watchEffect } from 'vue';
  5. import { omit } from 'lodash-es';
  6. import { throttle } from 'lodash-es';
  7. import { Checkbox, Radio } from 'ant-design-vue';
  8. import { isFunction } from '/@/utils/is';
  9. import { findNodeAll } from '/@/utils/helper/treeHelper';
  10. import { ROW_KEY } from '/@/components/Table/src/const';
  11. import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
  12. import { useMessage } from '/@/hooks/web/useMessage';
  13. import { ModalFunc } from 'ant-design-vue/lib/modal/Modal';
  14. // 自定义选择列的key
  15. export const CUS_SEL_COLUMN_KEY = 'j-custom-selected-column';
  16. // css命名空间,暂且写死已解决此hook无法获取到html元素的问题
  17. const ventSpace = 'zxm';
  18. /**
  19. * 自定义选择列
  20. */
  21. export function useCustomSelection(
  22. propsRef: ComputedRef<BasicTableProps>,
  23. emit: EmitType,
  24. wrapRef: Ref<null | HTMLDivElement>,
  25. getPaginationRef: ComputedRef<boolean | PaginationProps>,
  26. tableData: Ref<Recordable[]>,
  27. childrenColumnName: ComputedRef<string>
  28. ) {
  29. const { createConfirm } = useMessage();
  30. // 表格body元素
  31. const bodyEl = ref<HTMLDivElement>();
  32. // body元素高度
  33. const bodyHeight = ref<number>(0);
  34. // 表格tr高度
  35. const rowHeight = ref<number>(0);
  36. // body 滚动高度
  37. const scrollTop = ref(0);
  38. // 选择的key
  39. const selectedKeys = ref<string[]>([]);
  40. // 选择的行
  41. const selectedRows = ref<Recordable[]>([]);
  42. // 扁平化数据,children数据也会放到一起
  43. const flattedData = computed(() => {
  44. // update-begin--author:liaozhiyang---date:20231016---for:【QQYUN-6774】解决checkbox禁用后全选仍能勾选问题
  45. const data = flattenData(tableData.value, childrenColumnName.value);
  46. const rowSelection = propsRef.value.rowSelection;
  47. if (rowSelection?.type === 'checkbox' && rowSelection.getCheckboxProps) {
  48. for (let i = 0, len = data.length; i < len; i++) {
  49. const record = data[i];
  50. const result = rowSelection.getCheckboxProps(record);
  51. if (result.disabled) {
  52. data.splice(i, 1);
  53. i--;
  54. len--;
  55. }
  56. }
  57. }
  58. return data;
  59. // update-end--author:liaozhiyang---date:20231016---for:【QQYUN-6774】解决checkbox禁用后全选仍能勾选问题
  60. });
  61. const getRowSelectionRef = computed((): TableRowSelection | null => {
  62. const { rowSelection } = unref(propsRef);
  63. if (!rowSelection) {
  64. return null;
  65. }
  66. return {
  67. preserveSelectedRowKeys: true,
  68. // selectedRowKeys: unref(selectedKeys),
  69. // onChange: (selectedRowKeys: string[]) => {
  70. // setSelectedRowKeys(selectedRowKeys);
  71. // },
  72. ...omit(rowSelection, ['onChange', 'selectedRowKeys']),
  73. };
  74. });
  75. // 是否是单选
  76. const isRadio = computed(() => {
  77. return getRowSelectionRef.value?.type === 'radio';
  78. });
  79. const getAutoCreateKey = computed(() => {
  80. return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
  81. });
  82. // 列key字段
  83. const getRowKey = computed(() => {
  84. const { rowKey } = unref(propsRef);
  85. return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
  86. });
  87. // 获取行的key字段数据
  88. const getRecordKey = (record) => {
  89. if (!getRowKey.value) {
  90. return record[ROW_KEY];
  91. } else if (isFunction(getRowKey.value)) {
  92. return getRowKey.value(record);
  93. } else {
  94. return record[getRowKey.value];
  95. }
  96. };
  97. // 分页配置
  98. const getPagination = computed<PaginationProps>(() => {
  99. return typeof getPaginationRef.value === 'boolean' ? {} : getPaginationRef.value;
  100. });
  101. // 当前页条目数量
  102. const currentPageSize = computed(() => {
  103. const { pageSize = 10, total = flattedData.value.length } = getPagination.value;
  104. return pageSize > total ? total : pageSize;
  105. });
  106. // 选择列表头props
  107. const selectHeaderProps = computed(() => {
  108. return {
  109. onSelectAll,
  110. isRadio: isRadio.value,
  111. selectedLength: flattedData.value.filter((data) => selectedKeys.value.includes(getRecordKey(data))).length,
  112. pageSize: currentPageSize.value,
  113. // 【QQYUN-6774】解决checkbox禁用后全选仍能勾选问题
  114. disabled: flattedData.value.length == 0,
  115. };
  116. });
  117. // 监听传入的selectedRowKeys
  118. watch(
  119. () => unref(propsRef)?.rowSelection?.selectedRowKeys,
  120. (val: string[]) => {
  121. if (Array.isArray(val)) {
  122. setSelectedRowKeys(val);
  123. }
  124. },
  125. { immediate: true }
  126. );
  127. // 当任意一个变化时,触发同步检测
  128. watch([selectedKeys, selectedRows], () => {
  129. nextTick(() => {
  130. syncSelectedRows();
  131. });
  132. });
  133. // 监听滚动条事件
  134. const onScrollTopChange = throttle((e) => (scrollTop.value = e?.target?.scrollTop), 150);
  135. let bodyResizeObserver: Nullable<ResizeObserver> = null;
  136. // 获取首行行高
  137. watchEffect(() => {
  138. if (bodyEl.value) {
  139. // 监听div高度变化
  140. bodyResizeObserver = new ResizeObserver((entries) => {
  141. for (let entry of entries) {
  142. if (entry.target === bodyEl.value && entry.contentRect) {
  143. const { height } = entry.contentRect;
  144. bodyHeight.value = Math.ceil(height);
  145. }
  146. }
  147. });
  148. bodyResizeObserver.observe(bodyEl.value);
  149. const el = bodyEl.value?.querySelector(`tbody .${ventSpace}-table-tbody tr .${ventSpace}-table-row`) as HTMLDivElement;
  150. if (el) {
  151. rowHeight.value = el.offsetHeight;
  152. return;
  153. }
  154. }
  155. rowHeight.value = 50;
  156. // 这种写法是为了监听到 size 的变化
  157. propsRef.value.size && void 0;
  158. });
  159. onMountedOrActivated(async () => {
  160. bodyEl.value = await getTableBody(wrapRef.value!);
  161. bodyEl.value.addEventListener('scroll', onScrollTopChange);
  162. });
  163. onUnmounted(() => {
  164. if (bodyEl.value) {
  165. bodyEl.value?.removeEventListener('scroll', onScrollTopChange);
  166. }
  167. if (bodyResizeObserver != null) {
  168. bodyResizeObserver.disconnect();
  169. }
  170. });
  171. // 选择全部
  172. function onSelectAll(checked: boolean) {
  173. // 取消全选
  174. if (!checked) {
  175. selectedKeys.value = [];
  176. selectedRows.value = [];
  177. emitChange();
  178. return;
  179. }
  180. let modal: Nullable<ReturnType<ModalFunc>> = null;
  181. // 全选
  182. const checkAll = () => {
  183. if (modal != null) {
  184. modal.update({
  185. content: '正在分批全选,请稍后……',
  186. cancelButtonProps: { disabled: true },
  187. });
  188. }
  189. let showCount = 0;
  190. // 最小选中数量
  191. let minSelect = 100;
  192. const hidden: Recordable[] = [];
  193. flattedData.value.forEach((item, index, array) => {
  194. if (array.length > 120) {
  195. if (showCount <= minSelect && recordIsShow(index, Math.max((minSelect - 10) / 2, 3))) {
  196. showCount++;
  197. updateSelected(item, checked);
  198. } else {
  199. hidden.push(item);
  200. }
  201. } else {
  202. updateSelected(item, checked);
  203. }
  204. });
  205. if (hidden.length > 0) {
  206. return batchesSelectAll(hidden, checked, minSelect);
  207. } else {
  208. emitChange();
  209. }
  210. };
  211. // 当数据量大于120条时,全选会导致页面卡顿,需进行慢速全选
  212. if (flattedData.value.length > 120) {
  213. modal = createConfirm({
  214. title: '全选',
  215. content: '当前数据量较大,全选可能会导致页面卡顿,确定要执行此操作吗?',
  216. iconType: 'warning',
  217. onOk: () => checkAll(),
  218. });
  219. } else {
  220. checkAll();
  221. }
  222. }
  223. // 分批全选
  224. function batchesSelectAll(hidden: Recordable[], checked: boolean, minSelect: number) {
  225. return new Promise<void>((resolve) => {
  226. (function call() {
  227. // 每隔半秒钟,选择100条数据
  228. setTimeout(() => {
  229. const list = hidden.splice(0, minSelect);
  230. if (list.length > 0) {
  231. list.forEach((item) => {
  232. updateSelected(item, checked);
  233. });
  234. call();
  235. } else {
  236. setTimeout(() => {
  237. emitChange();
  238. // update-begin--author:liaozhiyang---date:20230811---for:【QQYUN-5687】批量选择,提示成功后,又来一个提示
  239. setTimeout(() =>resolve(), 0);
  240. // update-end--author:liaozhiyang---date:20230811---for:【QQYUN-5687】批量选择,提示成功后,又来一个提示
  241. }, 500);
  242. }
  243. }, 300);
  244. })();
  245. });
  246. }
  247. // 选中单个
  248. function onSelect(record, checked) {
  249. updateSelected(record, checked);
  250. emitChange();
  251. }
  252. function updateSelected(record, checked) {
  253. const recordKey = getRecordKey(record);
  254. if (isRadio.value) {
  255. selectedKeys.value = [recordKey];
  256. selectedRows.value = [record];
  257. return;
  258. }
  259. const index = selectedKeys.value.findIndex((key) => key === recordKey);
  260. if (checked) {
  261. if (index === -1) {
  262. selectedKeys.value.push(recordKey);
  263. selectedRows.value.push(record);
  264. }
  265. } else {
  266. if (index !== -1) {
  267. selectedKeys.value.splice(index, 1);
  268. selectedRows.value.splice(index, 1);
  269. }
  270. }
  271. }
  272. // 调用用户自定义的onChange事件
  273. function emitChange() {
  274. const { rowSelection } = unref(propsRef);
  275. if (rowSelection) {
  276. const { onChange } = rowSelection;
  277. if (onChange && isFunction(onChange)) {
  278. setTimeout(() => {
  279. onChange(selectedKeys.value, selectedRows.value);
  280. }, 0);
  281. }
  282. }
  283. emit('selection-change', {
  284. keys: getSelectRowKeys(),
  285. rows: getSelectRows(),
  286. });
  287. }
  288. // 用于判断是否是自定义选择列
  289. function isCustomSelection(column: BasicColumn) {
  290. return column.key === CUS_SEL_COLUMN_KEY;
  291. }
  292. /**
  293. * 判断当前行是否可视,虚拟滚动用
  294. * @param index 行下标
  295. * @param threshold 前后阈值,默认可视区域前后显示3条
  296. */
  297. function recordIsShow(index: number, threshold = 3) {
  298. // 只有数据量大于50条时,才会进行虚拟滚动
  299. const isVirtual = flattedData.value.length > 50;
  300. if (isVirtual) {
  301. // 根据 scrollTop、bodyHeight、rowHeight 计算出当前行是否可视(阈值前后3条)
  302. // flag1 = 判断当前行是否在可视区域上方3条
  303. const flag1 = scrollTop.value - rowHeight.value * threshold < index * rowHeight.value;
  304. // flag2 = 判断当前行是否在可视区域下方3条
  305. const flag2 = index * rowHeight.value < scrollTop.value + bodyHeight.value + rowHeight.value * threshold;
  306. // 全部条件满足时,才显示当前行
  307. return flag1 && flag2;
  308. }
  309. return true;
  310. }
  311. // 自定义渲染Body
  312. function bodyCustomRender(params) {
  313. const { index } = params;
  314. // update-begin--author:liaozhiyang---date:20231009--for:【issues/776】显示100条/页,复选框只能显示3个的问题
  315. if (propsRef.value.canResize && !recordIsShow(index)) {
  316. return '';
  317. }
  318. if (isRadio.value) {
  319. return renderRadioComponent(params);
  320. } else {
  321. return renderCheckboxComponent(params);
  322. }
  323. // update-end--author:liaozhiyang---date:20231009---for:【issues/776】显示100条/页,复选框只能显示3个的问题
  324. }
  325. /**
  326. * 渲染checkbox组件
  327. */
  328. function renderCheckboxComponent({ record }) {
  329. const recordKey = getRecordKey(record);
  330. // 获取用户自定义checkboxProps
  331. const checkboxProps = ((getCheckboxProps) => {
  332. if (typeof getCheckboxProps === 'function') {
  333. try {
  334. return getCheckboxProps(record) ?? {};
  335. } catch (error) {
  336. console.error(error);
  337. }
  338. }
  339. return {};
  340. })(propsRef.value.rowSelection?.getCheckboxProps);
  341. return (
  342. <Checkbox
  343. {...checkboxProps}
  344. key={'j-select__' + recordKey}
  345. checked={selectedKeys.value.includes(recordKey)}
  346. onUpdate:checked={(checked) => onSelect(record, checked)}
  347. />
  348. );
  349. }
  350. /**
  351. * 渲染radio组件
  352. */
  353. function renderRadioComponent({ record }) {
  354. const recordKey = getRecordKey(record);
  355. // update-begin--author:liaozhiyang---date:20231016---for:【QQYUN-6794】table列表增加radio禁用功能
  356. // 获取用户自定义radioProps
  357. const checkboxProps = (() => {
  358. const rowSelection = propsRef.value.rowSelection;
  359. if (rowSelection?.getCheckboxProps) {
  360. return rowSelection.getCheckboxProps(record);
  361. }
  362. return {};
  363. })();
  364. // update-end--author:liaozhiyang---date:20231016---for:【QQYUN-6794】table列表增加radio禁用功能
  365. return (
  366. <Radio
  367. {...checkboxProps}
  368. key={'j-select__' + recordKey}
  369. checked={selectedKeys.value.includes(recordKey)}
  370. onUpdate:checked={(checked) => onSelect(record, checked)}
  371. />
  372. );
  373. }
  374. // 创建选择列
  375. function handleCustomSelectColumn(columns: BasicColumn[]) {
  376. // update-begin--author:liaozhiyang---date:20230919---for:【issues/757】JPopup表格的选择列固定配置不生效
  377. const rowSelection = propsRef.value.rowSelection;
  378. if (!rowSelection) {
  379. return;
  380. }
  381. const isFixedLeft = rowSelection.fixed || columns.some((item) => item.fixed === 'left');
  382. // update-begin--author:liaozhiyang---date:20230919---for:【issues/757】JPopup表格的选择列固定配置不生效
  383. columns.unshift({
  384. title: '选择列',
  385. flag: 'CHECKBOX',
  386. key: CUS_SEL_COLUMN_KEY,
  387. width: 50,
  388. minWidth: 50,
  389. maxWidth: 50,
  390. align: 'center',
  391. ...(isFixedLeft ? { fixed: 'left' } : {}),
  392. customRender: bodyCustomRender,
  393. });
  394. }
  395. // 清空所有选择
  396. function clearSelectedRowKeys() {
  397. onSelectAll(false);
  398. }
  399. // 通过 selectedKeys 同步 selectedRows
  400. function syncSelectedRows() {
  401. if (selectedKeys.value.length !== selectedRows.value.length) {
  402. setSelectedRowKeys(selectedKeys.value);
  403. }
  404. }
  405. // 设置选择的key
  406. function setSelectedRowKeys(rowKeys: string[]) {
  407. const isSomeRowKeys = selectedKeys.value === rowKeys;
  408. selectedKeys.value = rowKeys;
  409. const allSelectedRows = findNodeAll(
  410. toRaw(unref(flattedData)).concat(toRaw(unref(selectedRows))),
  411. (item) => rowKeys.includes(getRecordKey(item)),
  412. {
  413. children: propsRef.value.childrenColumnName ?? 'children',
  414. }
  415. );
  416. const trueSelectedRows: any[] = [];
  417. rowKeys.forEach((key: string) => {
  418. const found = allSelectedRows.find((item) => getRecordKey(item) === key);
  419. found && trueSelectedRows.push(found);
  420. });
  421. // update-begin--author:liaozhiyang---date:20231103---for:【issues/828】解决卡死问题
  422. if (!(isSomeRowKeys && equal(selectedRows.value, trueSelectedRows))) {
  423. selectedRows.value = trueSelectedRows;
  424. emitChange();
  425. }
  426. // update-end--author:liaozhiyang---date:20231103---for:【issues/828】解决卡死问题
  427. }
  428. /**
  429. *2023-11-03
  430. *廖志阳
  431. *检测selectedRows.value和trueSelectedRows是否相等,防止死循环
  432. */
  433. function equal(oldVal, newVal) {
  434. let oldKeys = [],
  435. newKeys = [];
  436. if (oldVal.length === newVal.length) {
  437. oldKeys = oldVal.map((item) => getRecordKey(item));
  438. newKeys = newVal.map((item) => getRecordKey(item));
  439. for (let i = 0, len = oldKeys.length; i < len; i++) {
  440. const findItem = newKeys.find((item) => item === oldKeys[i]);
  441. if (!findItem) {
  442. return false;
  443. }
  444. }
  445. return true;
  446. }
  447. return false;
  448. }
  449. function getSelectRows<T = Recordable>() {
  450. return unref(selectedRows) as T[];
  451. }
  452. function getSelectRowKeys() {
  453. return unref(selectedKeys);
  454. }
  455. function getRowSelection() {
  456. return unref(getRowSelectionRef)!;
  457. }
  458. function deleteSelectRowByKey(key: string) {
  459. const index = selectedKeys.value.findIndex((item) => item === key);
  460. if (index !== -1) {
  461. selectedKeys.value.splice(index, 1);
  462. selectedRows.value.splice(index, 1);
  463. }
  464. }
  465. // 【QQYUN-5837】动态计算 expandIconColumnIndex
  466. const getExpandIconColumnIndex = computed(() => {
  467. const { expandIconColumnIndex } = unref(propsRef);
  468. // 未设置选择列,则保持不变
  469. if (getRowSelectionRef.value == null) {
  470. return expandIconColumnIndex;
  471. }
  472. // 设置了选择列,并且未传入 index 参数,则返回 1
  473. if (expandIconColumnIndex == null) {
  474. return 1;
  475. }
  476. return expandIconColumnIndex;
  477. });
  478. return {
  479. getRowSelection,
  480. getRowSelectionRef,
  481. getSelectRows,
  482. getSelectRowKeys,
  483. setSelectedRowKeys,
  484. deleteSelectRowByKey,
  485. selectHeaderProps,
  486. isCustomSelection,
  487. handleCustomSelectColumn,
  488. clearSelectedRowKeys,
  489. getExpandIconColumnIndex,
  490. };
  491. }
  492. function getTableBody(wrap: HTMLDivElement) {
  493. return new Promise<HTMLDivElement>((resolve) => {
  494. (function fn() {
  495. const bodyEl = wrap.querySelector(`.${ventSpace}-table-wrapper .${ventSpace}-table-body`) as HTMLDivElement;
  496. console.log('debug r', bodyEl);
  497. if (bodyEl) {
  498. resolve(bodyEl);
  499. } else {
  500. setTimeout(fn, 100);
  501. }
  502. })();
  503. });
  504. }
  505. function flattenData<RecordType>(data: RecordType[] | undefined, childrenColumnName: string): RecordType[] {
  506. let list: RecordType[] = [];
  507. (data || []).forEach((record) => {
  508. list.push(record);
  509. if (record && typeof record === 'object' && childrenColumnName in record) {
  510. list = [...list, ...flattenData<RecordType>((record as any)[childrenColumnName], childrenColumnName)];
  511. }
  512. });
  513. return list;
  514. }