Tree.vue 13 KB

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