BasicTree.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <script lang="tsx">
  2. import type { CSSProperties } from 'vue';
  3. import type {
  4. FieldNames,
  5. TreeState,
  6. TreeItem,
  7. KeyType,
  8. CheckKeys,
  9. TreeActionType,
  10. } from './types/tree';
  11. import {
  12. defineComponent,
  13. reactive,
  14. computed,
  15. unref,
  16. ref,
  17. watchEffect,
  18. toRaw,
  19. watch,
  20. onMounted,
  21. } from 'vue';
  22. import TreeHeader from './components/TreeHeader.vue';
  23. import { Tree, Spin, Empty } from 'ant-design-vue';
  24. import { TreeIcon } from './TreeIcon';
  25. import { ScrollContainer } from '/@/components/Container';
  26. import { omit, get, difference, cloneDeep } from 'lodash-es';
  27. import { isArray, isBoolean, isEmpty, isFunction } from '/@/utils/is';
  28. import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
  29. import { filter, treeToList, eachTree } from '/@/utils/helper/treeHelper';
  30. import { useTree } from './hooks/useTree';
  31. import { useContextMenu } from '/@/hooks/web/useContextMenu';
  32. import { CreateContextOptions } from '/@/components/ContextMenu';
  33. import { treeEmits, treeProps } from './types/tree';
  34. import { createBEM } from '/@/utils/bem';
  35. import type { TreeProps } from 'ant-design-vue/es/tree/Tree';
  36. export default defineComponent({
  37. name: 'BasicTree',
  38. inheritAttrs: false,
  39. props: treeProps,
  40. emits: treeEmits,
  41. setup(props, { attrs, slots, emit, expose }) {
  42. const [bem] = createBEM('tree');
  43. const state = reactive<TreeState>({
  44. checkStrictly: props.checkStrictly,
  45. expandedKeys: props.expandedKeys || [],
  46. selectedKeys: props.selectedKeys || [],
  47. checkedKeys: props.checkedKeys || [],
  48. });
  49. const searchState = reactive({
  50. startSearch: false,
  51. searchText: '',
  52. searchData: [] as TreeItem[],
  53. });
  54. const treeDataRef = ref<TreeItem[]>([]);
  55. const [createContextMenu] = useContextMenu();
  56. const getFieldNames = computed((): Required<FieldNames> => {
  57. const { fieldNames } = props;
  58. return {
  59. children: 'children',
  60. title: 'title',
  61. key: 'key',
  62. ...fieldNames,
  63. };
  64. });
  65. const getBindValues = computed(() => {
  66. let propsData = {
  67. blockNode: true,
  68. ...attrs,
  69. ...props,
  70. expandedKeys: state.expandedKeys,
  71. selectedKeys: state.selectedKeys,
  72. checkedKeys: state.checkedKeys,
  73. checkStrictly: state.checkStrictly,
  74. fieldNames: unref(getFieldNames),
  75. 'onUpdate:expandedKeys': (v: KeyType[]) => {
  76. state.expandedKeys = v;
  77. emit('update:expandedKeys', v);
  78. },
  79. 'onUpdate:selectedKeys': (v: KeyType[]) => {
  80. state.selectedKeys = v;
  81. emit('update:selectedKeys', v);
  82. },
  83. onCheck: (v: CheckKeys, e) => {
  84. let currentValue = toRaw(state.checkedKeys) as KeyType[];
  85. if (isArray(currentValue) && searchState.startSearch) {
  86. const value = e.node.eventKey;
  87. currentValue = difference(currentValue, getChildrenKeys(value));
  88. if (e.checked) {
  89. currentValue.push(value);
  90. }
  91. state.checkedKeys = currentValue;
  92. } else {
  93. state.checkedKeys = v;
  94. }
  95. const rawVal = toRaw(state.checkedKeys);
  96. emit('update:value', rawVal);
  97. emit('check', rawVal, e);
  98. },
  99. onRightClick: handleRightClick,
  100. };
  101. return omit(propsData, 'treeData', 'class') as TreeProps;
  102. });
  103. const getTreeData = computed((): TreeItem[] =>
  104. searchState.startSearch ? searchState.searchData : unref(treeDataRef),
  105. );
  106. const getNotFound = computed((): boolean => {
  107. return !getTreeData.value || getTreeData.value.length === 0;
  108. });
  109. const {
  110. deleteNodeByKey,
  111. insertNodeByKey,
  112. insertNodesByKey,
  113. filterByLevel,
  114. updateNodeByKey,
  115. getAllKeys,
  116. getChildrenKeys,
  117. getEnabledKeys,
  118. getSelectedNode,
  119. } = useTree(treeDataRef, getFieldNames);
  120. function getIcon(params: TreeItem, icon?: string) {
  121. if (!icon) {
  122. if (props.renderIcon && isFunction(props.renderIcon)) {
  123. return props.renderIcon(params);
  124. }
  125. }
  126. return icon;
  127. }
  128. async function handleRightClick({ event, node }: Recordable) {
  129. const { rightMenuList: menuList = [], beforeRightClick } = props;
  130. let contextMenuOptions: CreateContextOptions = { event, items: [] };
  131. if (beforeRightClick && isFunction(beforeRightClick)) {
  132. let result = await beforeRightClick(node, event);
  133. if (Array.isArray(result)) {
  134. contextMenuOptions.items = result;
  135. } else {
  136. Object.assign(contextMenuOptions, result);
  137. }
  138. } else {
  139. contextMenuOptions.items = menuList;
  140. }
  141. if (!contextMenuOptions.items?.length) return;
  142. contextMenuOptions.items = contextMenuOptions.items.filter((item) => !item.hidden);
  143. createContextMenu(contextMenuOptions);
  144. }
  145. function setExpandedKeys(keys: KeyType[]) {
  146. state.expandedKeys = keys;
  147. }
  148. function getExpandedKeys() {
  149. return state.expandedKeys;
  150. }
  151. function setSelectedKeys(keys: KeyType[]) {
  152. state.selectedKeys = keys;
  153. }
  154. function getSelectedKeys() {
  155. return state.selectedKeys;
  156. }
  157. function setCheckedKeys(keys: CheckKeys) {
  158. state.checkedKeys = keys;
  159. }
  160. function getCheckedKeys() {
  161. return state.checkedKeys;
  162. }
  163. function checkAll(checkAll: boolean) {
  164. state.checkedKeys = checkAll ? getEnabledKeys() : ([] as KeyType[]);
  165. }
  166. function expandAll(expandAll: boolean) {
  167. state.expandedKeys = expandAll ? getAllKeys() : ([] as KeyType[]);
  168. }
  169. function onStrictlyChange(strictly: boolean) {
  170. state.checkStrictly = strictly;
  171. }
  172. watch(
  173. () => props.searchValue,
  174. (val) => {
  175. if (val !== searchState.searchText) {
  176. searchState.searchText = val;
  177. }
  178. },
  179. {
  180. immediate: true,
  181. },
  182. );
  183. watch(
  184. () => props.treeData,
  185. (val) => {
  186. if (val) {
  187. handleSearch(searchState.searchText);
  188. }
  189. },
  190. );
  191. function handleSearch(searchValue: string) {
  192. if (searchValue !== searchState.searchText) searchState.searchText = searchValue;
  193. emit('update:searchValue', searchValue);
  194. if (!searchValue) {
  195. searchState.startSearch = false;
  196. return;
  197. }
  198. const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
  199. unref(props);
  200. searchState.startSearch = true;
  201. const { title: titleField, key: keyField } = unref(getFieldNames);
  202. const matchedKeys: string[] = [];
  203. searchState.searchData = filter(
  204. unref(treeDataRef),
  205. (node) => {
  206. const result = filterFn
  207. ? filterFn(searchValue, node, unref(getFieldNames))
  208. : node[titleField]?.includes(searchValue) ?? false;
  209. if (result) {
  210. matchedKeys.push(node[keyField]);
  211. }
  212. return result;
  213. },
  214. unref(getFieldNames),
  215. );
  216. if (expandOnSearch) {
  217. const expandKeys = treeToList(searchState.searchData).map((val) => {
  218. return val[keyField];
  219. });
  220. if (expandKeys && expandKeys.length) {
  221. setExpandedKeys(expandKeys);
  222. }
  223. }
  224. if (checkOnSearch && checkable && matchedKeys.length) {
  225. setCheckedKeys(matchedKeys);
  226. }
  227. if (selectedOnSearch && matchedKeys.length) {
  228. setSelectedKeys(matchedKeys);
  229. }
  230. }
  231. function handleClickNode(key: string, children: TreeItem[]) {
  232. if (!props.clickRowToExpand || !children || children.length === 0) return;
  233. if (!state.expandedKeys.includes(key)) {
  234. setExpandedKeys([...state.expandedKeys, key]);
  235. } else {
  236. const keys = [...state.expandedKeys];
  237. const index = keys.findIndex((item) => item === key);
  238. if (index !== -1) {
  239. keys.splice(index, 1);
  240. }
  241. setExpandedKeys(keys);
  242. }
  243. }
  244. watchEffect(() => {
  245. treeDataRef.value = props.treeData as TreeItem[];
  246. });
  247. onMounted(() => {
  248. const level = parseInt(props.defaultExpandLevel);
  249. if (level > 0) {
  250. state.expandedKeys = filterByLevel(level);
  251. } else if (props.defaultExpandAll) {
  252. expandAll(true);
  253. }
  254. });
  255. watchEffect(() => {
  256. state.expandedKeys = props.expandedKeys;
  257. });
  258. watchEffect(() => {
  259. state.selectedKeys = props.selectedKeys;
  260. });
  261. watchEffect(() => {
  262. state.checkedKeys = props.checkedKeys;
  263. });
  264. watch(
  265. () => props.value,
  266. () => {
  267. state.checkedKeys = toRaw(props.value || []);
  268. },
  269. { immediate: true },
  270. );
  271. watch(
  272. () => state.checkedKeys,
  273. () => {
  274. const v = toRaw(state.checkedKeys);
  275. emit('update:value', v);
  276. emit('change', v);
  277. },
  278. );
  279. watchEffect(() => {
  280. state.checkStrictly = props.checkStrictly;
  281. });
  282. const instance: TreeActionType = {
  283. getTreeData: () => treeDataRef,
  284. setExpandedKeys,
  285. getExpandedKeys,
  286. setSelectedKeys,
  287. getSelectedKeys,
  288. setCheckedKeys,
  289. getCheckedKeys,
  290. insertNodeByKey,
  291. insertNodesByKey,
  292. deleteNodeByKey,
  293. updateNodeByKey,
  294. getSelectedNode,
  295. checkAll,
  296. expandAll,
  297. filterByLevel: (level: number) => {
  298. state.expandedKeys = filterByLevel(level);
  299. },
  300. setSearchValue: (value: string) => {
  301. handleSearch(value);
  302. },
  303. getSearchValue: () => {
  304. return searchState.searchText;
  305. },
  306. };
  307. function renderAction(node: TreeItem) {
  308. const { actionList } = props;
  309. if (!actionList || actionList.length === 0) return;
  310. return actionList.map((item, index) => {
  311. let nodeShow = true;
  312. if (isFunction(item.show)) {
  313. nodeShow = item.show?.(node);
  314. } else if (isBoolean(item.show)) {
  315. nodeShow = item.show;
  316. }
  317. if (!nodeShow) return null;
  318. return (
  319. <span key={index} class={bem('action')}>
  320. {item.render(node)}
  321. </span>
  322. );
  323. });
  324. }
  325. const treeData = computed(() => {
  326. const data = cloneDeep(getTreeData.value);
  327. eachTree(data, (item, _parent) => {
  328. const searchText = searchState.searchText;
  329. const { highlight } = unref(props);
  330. const {
  331. title: titleField,
  332. key: keyField,
  333. children: childrenField,
  334. } = unref(getFieldNames);
  335. const icon = getIcon(item, item.icon);
  336. const title = get(item, titleField);
  337. const searchIdx = searchText ? title.indexOf(searchText) : -1;
  338. const isHighlight =
  339. searchState.startSearch && !isEmpty(searchText) && highlight && searchIdx !== -1;
  340. const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
  341. const titleDom = isHighlight ? (
  342. <span class={unref(getBindValues)?.blockNode ? `${bem('content')}` : ''}>
  343. <span>{title.substr(0, searchIdx)}</span>
  344. <span style={highlightStyle}>{searchText}</span>
  345. <span>{title.substr(searchIdx + (searchText as string).length)}</span>
  346. </span>
  347. ) : (
  348. title
  349. );
  350. const iconDom = icon ? (
  351. <TreeIcon icon={icon} />
  352. ) : slots.icon ? (
  353. <span class="mr-1">{getSlot(slots, 'icon')}</span>
  354. ) : null;
  355. item[titleField] = (
  356. <span
  357. class={`${bem('title')} pl-2`}
  358. onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
  359. >
  360. {slots?.title ? (
  361. <>
  362. {iconDom}
  363. {getSlot(slots, 'title', item)}
  364. </>
  365. ) : (
  366. <>
  367. {iconDom}
  368. {titleDom}
  369. <span class={bem('actions')}>{renderAction(item)}</span>
  370. </>
  371. )}
  372. </span>
  373. );
  374. return item;
  375. });
  376. return data;
  377. });
  378. expose(instance);
  379. return () => {
  380. const { title, helpMessage, toolbar, search, checkable } = props;
  381. const showTitle = title || toolbar || search || slots.headerTitle;
  382. const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
  383. return (
  384. <div class={[bem(), 'h-full', attrs.class]}>
  385. {showTitle && (
  386. <TreeHeader
  387. checkable={checkable}
  388. checkAll={checkAll}
  389. expandAll={expandAll}
  390. title={title}
  391. search={search}
  392. toolbar={toolbar}
  393. helpMessage={helpMessage}
  394. onStrictlyChange={onStrictlyChange}
  395. onSearch={handleSearch}
  396. searchText={searchState.searchText}
  397. >
  398. {extendSlots(slots)}
  399. </TreeHeader>
  400. )}
  401. <Spin
  402. wrapperClassName={unref(props.treeWrapperClassName)}
  403. spinning={unref(props.loading)}
  404. tip="加载中..."
  405. >
  406. <ScrollContainer style={scrollStyle} v-show={!unref(getNotFound)}>
  407. <Tree {...unref(getBindValues)} showIcon={false} treeData={treeData.value}>
  408. {extendSlots(slots, ['title'])}
  409. </Tree>
  410. </ScrollContainer>
  411. <Empty
  412. v-show={unref(getNotFound)}
  413. image={Empty.PRESENTED_IMAGE_SIMPLE}
  414. class="!mt-4"
  415. />
  416. </Spin>
  417. </div>
  418. );
  419. };
  420. },
  421. });
  422. </script>