cl-tree.uvue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. <template>
  2. <view class="cl-tree">
  3. <cl-tree-item
  4. v-for="item in data"
  5. :key="item.id"
  6. :item="item"
  7. :level="0"
  8. :pt="pt"
  9. ></cl-tree-item>
  10. </view>
  11. </template>
  12. <script lang="ts" setup>
  13. import { computed, watch, ref, type PropType } from "vue";
  14. import type { ClTreeItem, ClTreeNodeInfo } from "../../types";
  15. defineOptions({
  16. name: "cl-tree"
  17. });
  18. const props = defineProps({
  19. pt: {
  20. type: Object as PropType<any>,
  21. default: () => ({})
  22. },
  23. // 树形结构数据
  24. list: {
  25. type: Array as PropType<ClTreeItem[]>,
  26. default: () => []
  27. },
  28. // 节点图标
  29. icon: {
  30. type: String,
  31. default: "arrow-right-s-fill"
  32. },
  33. // 展开图标
  34. expandIcon: {
  35. type: String,
  36. default: "arrow-down-s-fill"
  37. },
  38. // 是否显示复选框
  39. showCheckbox: {
  40. type: Boolean,
  41. default: false
  42. },
  43. // 是否严格的遵循父子不互相关联
  44. checkStrictly: {
  45. type: Boolean,
  46. default: false
  47. }
  48. });
  49. // 树数据
  50. const data = ref<ClTreeItem[]>(props.list);
  51. /**
  52. * 优化的节点搜索算法,使用Map缓存提升查找性能
  53. * 创建节点索引映射,O(1)时间复杂度查找节点
  54. */
  55. const nodeMap = computed(() => {
  56. // 创建一个Map用于存储节点信息,key为节点id,value为节点信息对象
  57. const map = new Map<string | number, ClTreeNodeInfo>();
  58. // 递归遍历所有节点,构建节点与父节点的映射关系
  59. function buildMap(nodes: ClTreeItem[], parent: ClTreeItem | null = null): void {
  60. for (let i: number = 0; i < nodes.length; i++) {
  61. const node = nodes[i]; // 当前节点
  62. // 将当前节点的信息存入map,包含节点本身、父节点、在父节点中的索引
  63. map.set(node.id, { node, parent, index: i } as ClTreeNodeInfo);
  64. // 如果当前节点有子节点,则递归处理子节点
  65. if (node.children != null && node.children.length > 0) {
  66. buildMap(node.children, node);
  67. }
  68. }
  69. }
  70. // 从根节点开始构建映射
  71. buildMap(data.value);
  72. return map; // 返回构建好的Map
  73. });
  74. /**
  75. * 根据key查找节点信息
  76. * @param key 节点id
  77. * @returns 节点信息对象或null
  78. */
  79. function findNodeInfo(key: string | number): ClTreeNodeInfo | null {
  80. const result = nodeMap.value.get(key); // 从Map中查找节点信息
  81. return result != null ? result : null; // 找到则返回,否则返回null
  82. }
  83. /**
  84. * 获取指定节点的所有祖先节点
  85. * @param key 节点id
  86. * @returns 祖先节点数组(从根到最近父节点顺序)
  87. */
  88. function getAncestors(key: string | number): ClTreeItem[] {
  89. const result: ClTreeItem[] = []; // 用于存储祖先节点
  90. let nodeInfo = findNodeInfo(key); // 当前节点信息
  91. // 循环查找父节点,直到根节点
  92. while (nodeInfo != null && nodeInfo.parent != null) {
  93. result.unshift(nodeInfo.parent); // 将父节点插入到数组前面
  94. nodeInfo = findNodeInfo(nodeInfo.parent.id); // 查找父节点信息
  95. }
  96. return result; // 返回祖先节点数组
  97. }
  98. /**
  99. * 更新所有节点的选中状态(用于批量操作后的状态同步)
  100. */
  101. function updateAllCheckStates(): void {
  102. // 递归更新每个节点的选中和半选状态
  103. function updateNodeStates(nodes: ClTreeItem[]): void {
  104. for (let i: number = 0; i < nodes.length; i++) {
  105. const node = nodes[i]; // 当前节点
  106. const children = node.children != null ? node.children : []; // 子节点数组
  107. if (children.length == 0) {
  108. // 叶子节点,重置半选状态
  109. node.isHalfChecked = false;
  110. continue; // 跳过后续处理
  111. }
  112. // 先递归处理子节点
  113. updateNodeStates(children);
  114. // 统计子节点的选中和半选数量
  115. let checkedCount = 0; // 选中数量
  116. let halfCheckedCount = 0; // 半选数量
  117. for (let j = 0; j < children.length; j++) {
  118. if (children[j].isChecked == true) {
  119. checkedCount++;
  120. } else if (children[j].isHalfChecked == true) {
  121. halfCheckedCount++;
  122. }
  123. }
  124. // 根据子节点状态更新当前节点状态
  125. if (checkedCount == children.length) {
  126. // 全部选中
  127. node.isChecked = true;
  128. node.isHalfChecked = false;
  129. } else if (checkedCount > 0 || halfCheckedCount > 0) {
  130. // 部分选中或有半选
  131. node.isChecked = false;
  132. node.isHalfChecked = true;
  133. } else {
  134. // 全部未选中
  135. node.isChecked = false;
  136. node.isHalfChecked = false;
  137. }
  138. }
  139. }
  140. // 从根节点开始递归更新
  141. updateNodeStates(data.value);
  142. }
  143. /**
  144. * 更新指定节点的所有祖先节点的选中状态
  145. * @param key 节点id
  146. */
  147. function updateAncestorsCheckState(key: string | number): void {
  148. const ancestors = getAncestors(key); // 获取所有祖先节点
  149. // 从最近的父节点开始向上更新
  150. for (let i = ancestors.length - 1; i >= 0; i--) {
  151. const ancestor = ancestors[i]; // 当前祖先节点
  152. const children = ancestor.children != null ? ancestor.children : ([] as ClTreeItem[]); // 子节点数组
  153. if (children.length == 0) continue; // 没有子节点则跳过
  154. let checkedCount = 0; // 选中数量
  155. let halfCheckedCount = 0; // 半选数量
  156. // 统计子节点的选中和半选数量
  157. for (let j = 0; j < children.length; j++) {
  158. if (children[j].isChecked == true) {
  159. checkedCount++;
  160. } else if (children[j].isHalfChecked == true) {
  161. halfCheckedCount++;
  162. }
  163. }
  164. // 根据子节点状态更新当前祖先节点状态
  165. if (checkedCount == children.length) {
  166. // 全部选中
  167. ancestor.isChecked = true;
  168. ancestor.isHalfChecked = false;
  169. } else if (checkedCount > 0 || halfCheckedCount > 0) {
  170. // 部分选中或有半选
  171. ancestor.isChecked = false;
  172. ancestor.isHalfChecked = true;
  173. } else {
  174. // 全部未选中
  175. ancestor.isChecked = false;
  176. ancestor.isHalfChecked = false;
  177. }
  178. }
  179. }
  180. /**
  181. * 获取指定节点的所有子孙节点
  182. * 优化:使用队列实现广度优先遍历,避免递归栈溢出
  183. * @param key 节点id
  184. * @returns 子孙节点数组
  185. */
  186. function getDescendants(key: string | number): ClTreeItem[] {
  187. const nodeInfo = findNodeInfo(key); // 查找节点信息
  188. if (nodeInfo == null || nodeInfo.node.children == null) {
  189. return []; // 节点不存在或无子节点,返回空数组
  190. }
  191. // 存储所有子孙节点
  192. const result: ClTreeItem[] = [];
  193. // 队列用于广度优先遍历
  194. const queue: ClTreeItem[] = [];
  195. // 将子节点添加到队列中
  196. for (let i = 0; i < nodeInfo.node.children.length; i++) {
  197. queue.push(nodeInfo.node.children[i]);
  198. }
  199. // 广度优先遍历所有子孙节点
  200. while (queue.length > 0) {
  201. const node = queue.shift(); // 取出队首节点
  202. if (node == null) break; // 队列为空则结束
  203. result.push(node); // 将当前节点加入结果数组
  204. // 如果有子节点,继续加入队列
  205. if (node.children != null && node.children.length > 0) {
  206. for (let i = 0; i < node.children.length; i++) {
  207. queue.push(node.children[i]);
  208. }
  209. }
  210. }
  211. return result; // 返回所有子孙节点
  212. }
  213. /**
  214. * 清空所有节点的选中状态
  215. */
  216. function clearChecked(): void {
  217. // 遍历所有节点,将 isChecked 和 isHalfChecked 设为 false
  218. nodeMap.value.forEach((info: ClTreeNodeInfo) => {
  219. info.node.isChecked = false;
  220. info.node.isHalfChecked = false;
  221. });
  222. }
  223. /**
  224. * 设置指定节点的选中状态
  225. * @param key 节点id
  226. * @param flag 是否选中
  227. */
  228. function setChecked(key: string | number, flag: boolean): void {
  229. const nodeInfo = findNodeInfo(key); // 查找节点信息
  230. if (nodeInfo == null) return; // 节点不存在则返回
  231. // 设置当前节点选中状态
  232. nodeInfo.node.isChecked = flag;
  233. // 非严格模式下处理父子联动
  234. if (!props.checkStrictly) {
  235. // 设置所有子孙节点的选中状态
  236. const descendants = getDescendants(key);
  237. for (let i = 0; i < descendants.length; i++) {
  238. descendants[i].isChecked = flag;
  239. }
  240. // 更新所有祖先节点的状态
  241. updateAncestorsCheckState(key);
  242. }
  243. }
  244. /**
  245. * 批量设置节点选中状态
  246. * @param keys 需要设置为选中的节点id数组
  247. */
  248. function setCheckedKeys(keys: (string | number)[]): void {
  249. // 遍历所有需要选中的节点
  250. for (let i = 0; i < keys.length; i++) {
  251. const key: string | number = keys[i];
  252. const nodeInfo = findNodeInfo(key); // 查找节点信息
  253. if (nodeInfo != null) {
  254. nodeInfo.node.isChecked = true; // 设置为选中
  255. // 非严格模式下同时设置所有子孙节点为选中状态
  256. if (!props.checkStrictly) {
  257. const descendants = getDescendants(key);
  258. for (let j = 0; j < descendants.length; j++) {
  259. descendants[j].isChecked = true;
  260. }
  261. }
  262. }
  263. }
  264. // 非严格模式下更新所有相关节点的状态
  265. if (!props.checkStrictly) {
  266. updateAllCheckStates();
  267. }
  268. }
  269. /**
  270. * 获取所有选中节点的keys
  271. * @returns 选中节点id数组
  272. */
  273. function getCheckedKeys(): (string | number)[] {
  274. const result: (string | number)[] = []; // 存储选中节点id
  275. /**
  276. * 递归收集所有选中节点的id
  277. * @param nodes 当前遍历的节点数组
  278. */
  279. function collectCheckedKeys(nodes: ClTreeItem[]): void {
  280. for (let i = 0; i < nodes.length; i++) {
  281. const node = nodes[i];
  282. if (node.isChecked == true) {
  283. result.push(node.id); // 收集选中节点id
  284. }
  285. if (node.children != null) {
  286. collectCheckedKeys(node.children); // 递归处理子节点
  287. }
  288. }
  289. }
  290. collectCheckedKeys(data.value); // 从根节点开始收集
  291. return result; // 返回所有选中节点id
  292. }
  293. /**
  294. * 获取所有半选中节点的keys
  295. * @returns 半选节点id数组
  296. */
  297. function getHalfCheckedKeys(): (string | number)[] {
  298. const result: (string | number)[] = []; // 存储半选节点id
  299. /**
  300. * 递归收集所有半选节点的id
  301. * @param nodes 当前遍历的节点数组
  302. */
  303. function collectHalfCheckedKeys(nodes: ClTreeItem[]): void {
  304. for (let i = 0; i < nodes.length; i++) {
  305. const node = nodes[i];
  306. if (node.isHalfChecked == true) {
  307. result.push(node.id); // 收集半选节点id
  308. }
  309. if (node.children != null) {
  310. collectHalfCheckedKeys(node.children); // 递归处理子节点
  311. }
  312. }
  313. }
  314. collectHalfCheckedKeys(data.value); // 从根节点开始收集
  315. return result; // 返回所有半选节点id
  316. }
  317. /**
  318. * 设置指定节点的展开状态
  319. * @param key 节点id
  320. * @param flag 是否展开
  321. */
  322. function setExpanded(key: string | number, flag: boolean): void {
  323. const nodeInfo = findNodeInfo(key); // 查找节点信息
  324. if (nodeInfo == null) return; // 节点不存在则返回
  325. nodeInfo.node.isExpand = flag; // 设置节点的展开状态
  326. }
  327. /**
  328. * 批量设置节点展开状态
  329. * @param keys 需要展开的节点id数组
  330. */
  331. function setExpandedKeys(keys: (string | number)[]): void {
  332. // 先重置所有节点为收起状态
  333. nodeMap.value.forEach((info: ClTreeNodeInfo) => {
  334. info.node.isExpand = false;
  335. });
  336. // 设置指定节点为展开状态
  337. for (let i = 0; i < keys.length; i++) {
  338. const nodeInfo = findNodeInfo(keys[i]);
  339. if (nodeInfo != null) {
  340. nodeInfo.node.isExpand = true;
  341. }
  342. }
  343. }
  344. /**
  345. * 获取所有展开节点的keys
  346. * @returns 展开节点id数组
  347. */
  348. function getExpandedKeys(): (string | number)[] {
  349. const result: (string | number)[] = []; // 存储展开节点id
  350. /**
  351. * 递归收集所有展开节点的id
  352. * @param nodes 当前遍历的节点数组
  353. */
  354. function collectExpandedKeys(nodes: ClTreeItem[]): void {
  355. for (let i = 0; i < nodes.length; i++) {
  356. const node = nodes[i];
  357. if (node.isExpand == true) {
  358. result.push(node.id); // 收集展开节点id
  359. }
  360. if (node.children != null) {
  361. collectExpandedKeys(node.children); // 递归处理子节点
  362. }
  363. }
  364. }
  365. collectExpandedKeys(data.value); // 从根节点开始收集
  366. return result; // 返回所有展开节点id
  367. }
  368. /**
  369. * 展开所有节点
  370. */
  371. function expandAll(): void {
  372. // 遍历所有节点,如果有子节点则设置为展开
  373. nodeMap.value.forEach((info: ClTreeNodeInfo) => {
  374. if (info.node.children != null && info.node.children.length > 0) {
  375. info.node.isExpand = true;
  376. }
  377. });
  378. }
  379. /**
  380. * 收起所有节点
  381. */
  382. function collapseAll(): void {
  383. // 遍历所有节点,将isExpand设为false
  384. nodeMap.value.forEach((info: ClTreeNodeInfo) => {
  385. info.node.isExpand = false;
  386. });
  387. }
  388. // 监听props.list变化,同步到内部数据
  389. watch(
  390. computed(() => props.list),
  391. (val: ClTreeItem[]) => {
  392. data.value = val;
  393. },
  394. { immediate: true, deep: true }
  395. );
  396. // 监听树数据变化,自动更新选中状态
  397. watch(
  398. data,
  399. () => {
  400. if (!props.checkStrictly) {
  401. updateAllCheckStates();
  402. }
  403. },
  404. { deep: true }
  405. );
  406. defineExpose({
  407. icon: computed(() => props.icon),
  408. expandIcon: computed(() => props.expandIcon),
  409. showCheckbox: computed(() => props.showCheckbox),
  410. checkStrictly: computed(() => props.checkStrictly),
  411. clearChecked,
  412. setChecked,
  413. setCheckedKeys,
  414. getCheckedKeys,
  415. getHalfCheckedKeys,
  416. setExpanded,
  417. setExpandedKeys,
  418. getExpandedKeys,
  419. expandAll,
  420. collapseAll
  421. });
  422. </script>