Tree.vue 12 KB

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