index.vue 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  1. <template>
  2. <div class="task-select-tree" :style="{ width: componentWidth }">
  3. <el-popover
  4. ref="popover"
  5. placement="bottom-start"
  6. :width="popoverComputedWidth"
  7. trigger="click"
  8. v-model="popoverVisible"
  9. popper-class="task-select-tree-popper"
  10. :disabled="disabled"
  11. @show="onPopoverShow"
  12. >
  13. <div class="popover-content-wrapper">
  14. <el-input
  15. v-if="filterable"
  16. placeholder="输入关键字过滤"
  17. v-model="filterText"
  18. size="mini"
  19. clearable
  20. class="filter-input"
  21. @input="handleFilterDebounced"
  22. >
  23. </el-input>
  24. <div class="tree-container" :style="{ height: treeHeight + 'px' }">
  25. <div
  26. ref="virtualList"
  27. class="virtual-tree-list"
  28. @scroll="handleScroll"
  29. :style="{ height: treeHeight + 'px', overflow: 'auto' }"
  30. >
  31. <div
  32. class="virtual-content"
  33. :style="{
  34. height: totalHeight + 'px',
  35. position: 'relative',
  36. paddingTop: offsetY + 'px'
  37. }"
  38. >
  39. <div
  40. v-for="(item, index) in visibleItems"
  41. :key="`${item.key}-${item.level}-${index}`"
  42. :style="{
  43. height: itemHeight + 'px',
  44. paddingLeft: (item.level * indentSize) + 'px',
  45. display: 'flex',
  46. alignItems: 'center',
  47. position: 'relative'
  48. }"
  49. :class="[
  50. 'virtual-tree-node',
  51. {
  52. 'is-leaf': item.isLeaf,
  53. 'is-checked': checkedKeysSet.has(item.key) && (item.isLeaf || parentSelectable || !checkStrictly),
  54. 'is-disabled': item.nodeDisabled || checkboxDisabled(item) // Combined disabled state for row styling
  55. }
  56. ]"
  57. @click="handleNodeClick(item)"
  58. >
  59. <div class="node-expand-container">
  60. <i
  61. v-if="!item.isLeaf"
  62. :class="[
  63. 'node-expand-icon',
  64. item.expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'
  65. ]"
  66. @click.stop="toggleNodeExpand(item)"
  67. ></i>
  68. <span v-else class="node-expand-placeholder"></span>
  69. </div>
  70. <el-checkbox
  71. :value="checkedKeysSet.has(item.key)"
  72. :disabled="checkboxDisabled(item)"
  73. @change="handleCheckboxChange(item, $event)"
  74. @click.stop
  75. class="node-checkbox"
  76. ></el-checkbox>
  77. <span class="node-label" :title="item.label">{{ item.label }}</span>
  78. <span v-if="item.isLeaf && showNodeDetail" class="node-detail-text">
  79. (ID: {{ item.originalData.qwUserId || item.originalData.id }}{{ item.originalData.taskExecDate ? ' ' + item.originalData.taskExecDate : '' }})
  80. </span>
  81. </div>
  82. </div>
  83. </div>
  84. </div>
  85. <div class="popover-footer">
  86. <span class="selected-count">已选择: {{ displaySelectedCount }}</span>
  87. <el-button size="mini" @click="handleClearInPopover">清空</el-button>
  88. <el-button
  89. size="mini"
  90. :type="isAllCurrentResultsSelected ? 'warning' : 'info'"
  91. @click="handleSelectAllToggle"
  92. >
  93. {{ isAllCurrentResultsSelected ? '取消全选' : '全选' }}
  94. </el-button>
  95. <el-button type="primary" size="mini" @click="handleConfirm">确定</el-button>
  96. </div>
  97. </div>
  98. <div
  99. slot="reference"
  100. :class="['select-trigger-wrapper', { 'is-disabled': disabled, 'is-active': popoverVisible }]"
  101. @mouseenter="inputHovering = true"
  102. @mouseleave="inputHovering = false"
  103. >
  104. <div class="tags-or-value-container">
  105. <template v-if="multiple && displaySelectedNodes.length > 0">
  106. <el-tag
  107. v-for="node in displaySelectedNodes"
  108. :key="node.key"
  109. type="info"
  110. size="small"
  111. closable
  112. :disable-transitions="true"
  113. @close.stop="removeTag(node)"
  114. class="trigger-tag"
  115. >
  116. {{ node.label }}
  117. </el-tag>
  118. <el-tag
  119. v-if="displaySelectedCount > maxDisplayTags && displaySelectedNodes.length === maxDisplayTags"
  120. type="info"
  121. size="small"
  122. class="trigger-tag"
  123. >
  124. + {{ displaySelectedCount - maxDisplayTags }}
  125. </el-tag>
  126. </template>
  127. <span v-else-if="!multiple && displaySelectedCount > 0" class="single-value-display">
  128. {{ singleSelectedLabel }}
  129. </span>
  130. <span v-else class="placeholder-text">{{ placeholder }}</span>
  131. </div>
  132. <span class="icons-container">
  133. <i
  134. v-if="showClearIcon"
  135. class="el-icon-circle-close clear-icon"
  136. @click.stop="handleClearOnTrigger"
  137. ></i>
  138. <i :class="['el-icon-arrow-up', 'arrow-icon', { 'is-reverse': !popoverVisible }]"></i>
  139. </span>
  140. </div>
  141. </el-popover>
  142. </div>
  143. </template>
  144. <script>
  145. // 防抖函数
  146. function debounce(func, wait) {
  147. let timeout;
  148. return function executedFunction(...args) {
  149. const later = () => {
  150. clearTimeout(timeout);
  151. func(...args);
  152. };
  153. clearTimeout(timeout);
  154. timeout = setTimeout(later, wait);
  155. };
  156. }
  157. export default {
  158. name: 'OptimizedSelectTree',
  159. props: {
  160. value: {
  161. type: Array,
  162. default: () => []
  163. },
  164. rawData: {
  165. type: Array,
  166. required: true
  167. },
  168. placeholder: {
  169. type: String,
  170. default: '请选择'
  171. },
  172. multiple: {
  173. type: Boolean,
  174. default: false
  175. },
  176. disabled: { // Global disabled for the component
  177. type: Boolean,
  178. default: false
  179. },
  180. checkStrictly: {
  181. type: Boolean,
  182. default: false // Important for parent-child linkage
  183. },
  184. filterable: {
  185. type: Boolean,
  186. default: true
  187. },
  188. maxDisplayTags: {
  189. type: Number,
  190. default: 2
  191. },
  192. componentWidth: {
  193. type: String,
  194. default: '100%'
  195. },
  196. popoverWidth: {
  197. type: [String, Number],
  198. default: 'auto'
  199. },
  200. treeProps: {
  201. type: Object,
  202. default: () => ({
  203. children: 'children',
  204. label: 'label',
  205. isLeaf: 'isLeaf',
  206. disabled: 'disabled' // Key for disabled state in rawData
  207. })
  208. },
  209. nodeKey: {
  210. type: String,
  211. default: 'id'
  212. },
  213. defaultExpandAll: {
  214. type: Boolean,
  215. default: false
  216. },
  217. showNodeDetail: {
  218. type: Boolean,
  219. default: true
  220. },
  221. treeHeight: {
  222. type: Number,
  223. default: 280
  224. },
  225. returnLeafOnly: {
  226. // This prop's effect on output is now overridden.
  227. // It might still be used by consumers for other logic if they rely on its original meaning.
  228. type: Boolean,
  229. default: true
  230. },
  231. parentSelectable: {
  232. type: Boolean,
  233. default: false
  234. }
  235. },
  236. data() {
  237. return {
  238. popoverVisible: false,
  239. filterText: '',
  240. inputHovering: false,
  241. triggerElRect: null,
  242. itemHeight: 36,
  243. indentSize: 24,
  244. startIndex: 0,
  245. endIndex: 0,
  246. scrollTop: 0,
  247. visibleItemCount: 0,
  248. flattenedNodes: [],
  249. nodeMap: new Map(),
  250. checkedKeysSet: new Set(),
  251. expandedKeysSet: new Set(),
  252. updateTimer: null
  253. };
  254. },
  255. computed: {
  256. popoverComputedWidth() {
  257. if (this.popoverWidth === 'auto') {
  258. return this.triggerElRect ? this.triggerElRect.width : 300;
  259. }
  260. return parseInt(this.popoverWidth, 10);
  261. },
  262. totalHeight() {
  263. return this.visibleNodes.length * this.itemHeight;
  264. },
  265. offsetY() {
  266. return this.startIndex * this.itemHeight;
  267. },
  268. visibleNodes() {
  269. let nodes = this.flattenedNodes;
  270. if (this.filterText.trim()) {
  271. nodes = this.applyFilter(this.flattenedNodes);
  272. }
  273. const visible = [];
  274. for (const node of nodes) {
  275. if (this.isNodeCurrentlyVisible(node)) {
  276. visible.push(node);
  277. }
  278. }
  279. return visible;
  280. },
  281. visibleItems() {
  282. const items = this.visibleNodes.slice(this.startIndex, this.endIndex);
  283. return items.map(node => ({
  284. ...node,
  285. expanded: this.expandedKeysSet.has(node.key)
  286. }));
  287. },
  288. // Actual count of selected leaf items
  289. displaySelectedCount() {
  290. let leafCount = 0;
  291. this.checkedKeysSet.forEach(key => {
  292. const node = this.nodeMap.get(key);
  293. if (node && node.isLeaf) {
  294. leafCount++;
  295. }
  296. });
  297. return leafCount;
  298. },
  299. displaySelectedNodes() {
  300. if (!this.multiple) return [];
  301. const resultNodes = [];
  302. Array.from(this.checkedKeysSet)
  303. .map(key => this.nodeMap.get(key))
  304. .filter(Boolean)
  305. .forEach(node => {
  306. // Always display tags for leaf nodes that are selected.
  307. if (node.isLeaf) {
  308. resultNodes.push(node);
  309. }
  310. });
  311. return resultNodes.slice(0, this.maxDisplayTags);
  312. },
  313. singleSelectedLabel() {
  314. if (this.multiple || this.value.length === 0) return '';
  315. // 'value' prop should only contain leaf keys due to emitChange modification.
  316. // For single select, value should have at most one key.
  317. const selectedKey = String(this.value[0]);
  318. const node = this.nodeMap.get(selectedKey);
  319. // We expect 'value' to contain a leaf key.
  320. return node ? node.label : '';
  321. },
  322. showClearIcon() {
  323. return (
  324. !this.disabled &&
  325. this.inputHovering &&
  326. this.displaySelectedCount > 0 && // Based on leaf count now
  327. !this.multiple
  328. );
  329. },
  330. // 计算当前搜索结果中的所有叶子节点
  331. currentResultLeafNodes() {
  332. return this.visibleNodes.filter(node => node.isLeaf && !this.checkboxDisabled(node));
  333. },
  334. // 判断当前搜索结果是否全部选中
  335. isAllCurrentResultsSelected() {
  336. const leafNodes = this.currentResultLeafNodes;
  337. if (leafNodes.length === 0) return false;
  338. return leafNodes.every(node => this.checkedKeysSet.has(node.key));
  339. }
  340. },
  341. watch: {
  342. rawData: {
  343. handler(newData) {
  344. this.processRawData(newData);
  345. this.syncCheckedKeysFromValue();
  346. },
  347. immediate: true
  348. },
  349. value: {
  350. handler() {
  351. this.syncCheckedKeysFromValue();
  352. },
  353. deep: true,
  354. },
  355. filterText: {
  356. handler() {
  357. this.resetScroll();
  358. }
  359. },
  360. popoverVisible(isVisible) {
  361. if (isVisible) {
  362. this.$nextTick(() => {
  363. this.calculateVisibleRange();
  364. });
  365. }
  366. }
  367. },
  368. created() {
  369. this.handleFilterDebounced = debounce(this.filterTextChange, 300);
  370. this.visibleItemCount = Math.ceil(this.treeHeight / this.itemHeight) + 5;
  371. },
  372. methods: {
  373. filterTextChange() {
  374. this.resetScroll();
  375. },
  376. processRawData(data) {
  377. if (!Array.isArray(data)) {
  378. this.flattenedNodes = [];
  379. this.nodeMap.clear();
  380. return;
  381. }
  382. const flattened = [];
  383. const nodeMap = new Map();
  384. const expandedKeys = new Set();
  385. const processNode = (node, level = 0, parentKey = null) => {
  386. const children = node[this.treeProps.children] || [];
  387. const hasChildren = children.length > 0;
  388. const key = node[this.nodeKey];
  389. if (key === undefined || key === null) {
  390. console.warn('Node lacks a unique key:', node);
  391. return;
  392. }
  393. const strKey = String(key);
  394. let label = node[this.treeProps.label];
  395. if (typeof label !== 'string' || label.trim() === '') {
  396. label = node.name || node.title || node.text || `Node-${strKey}`;
  397. }
  398. const nodeDisabledByData = !!node[this.treeProps.disabled];
  399. const processedNode = {
  400. key: strKey,
  401. label: String(label).trim(),
  402. level,
  403. isLeaf: !hasChildren,
  404. nodeDisabled: nodeDisabledByData,
  405. parentKey: parentKey ? String(parentKey) : null,
  406. childrenKeys: hasChildren ? children.map(child => String(child[this.nodeKey])).filter(k => k !== undefined && k !== null) : [],
  407. originalData: node,
  408. hasChildren
  409. };
  410. flattened.push(processedNode);
  411. nodeMap.set(processedNode.key, processedNode);
  412. if (this.defaultExpandAll && hasChildren && !nodeDisabledByData) {
  413. expandedKeys.add(processedNode.key);
  414. }
  415. if (hasChildren) {
  416. children.forEach(child => processNode(child, level + 1, strKey));
  417. }
  418. };
  419. data.forEach(node => processNode(node));
  420. this.flattenedNodes = flattened;
  421. this.nodeMap = nodeMap;
  422. this.expandedKeysSet = new Set(expandedKeys);
  423. },
  424. isNodeCurrentlyVisible(node, currentExpandedSet = this.expandedKeysSet) {
  425. if (node.level === 0) return true;
  426. let current = node;
  427. while (current.parentKey) {
  428. const parent = this.nodeMap.get(current.parentKey);
  429. if (!parent || !currentExpandedSet.has(parent.key)) {
  430. return false;
  431. }
  432. current = parent;
  433. }
  434. return true;
  435. },
  436. applyFilter(nodesToFilter) {
  437. const searchText = this.filterText.toLowerCase().trim();
  438. if (!searchText) return nodesToFilter;
  439. const matchedNodeKeys = new Set();
  440. const newExpandedKeys = new Set(this.expandedKeysSet);
  441. nodesToFilter.forEach(node => {
  442. if (node.label.toLowerCase().includes(searchText)) {
  443. matchedNodeKeys.add(node.key);
  444. let parentKey = node.parentKey;
  445. while (parentKey) {
  446. const parentNode = this.nodeMap.get(parentKey);
  447. if (parentNode) {
  448. newExpandedKeys.add(parentKey);
  449. parentKey = parentNode.parentKey;
  450. } else {
  451. break;
  452. }
  453. }
  454. }
  455. });
  456. const filteredResult = nodesToFilter.filter(node => {
  457. return this.hasMatchInSubtreeOrSelf(node, searchText);
  458. });
  459. this.expandedKeysSet = newExpandedKeys;
  460. return filteredResult;
  461. },
  462. hasMatchInSubtreeOrSelf(node, searchText) {
  463. if (node.label.toLowerCase().includes(searchText)) {
  464. return true;
  465. }
  466. if (!node.isLeaf) {
  467. for (const childKey of node.childrenKeys) {
  468. const childNode = this.nodeMap.get(childKey);
  469. if (childNode && this.hasMatchInSubtreeOrSelf(childNode, searchText)) {
  470. return true;
  471. }
  472. }
  473. }
  474. return false;
  475. },
  476. checkboxDisabled(item) {
  477. if (item.nodeDisabled) return true;
  478. if (this.checkStrictly && !item.isLeaf && !this.parentSelectable) {
  479. return true;
  480. }
  481. return false;
  482. },
  483. calculateVisibleRange() {
  484. const containerHeight = this.treeHeight;
  485. const totalItems = this.visibleNodes.length;
  486. const newStartIndex = Math.floor(this.scrollTop / this.itemHeight);
  487. const visibleCountInViewport = Math.ceil(containerHeight / this.itemHeight);
  488. const buffer = 5;
  489. this.startIndex = Math.max(0, newStartIndex - buffer);
  490. this.endIndex = Math.min(totalItems, newStartIndex + visibleCountInViewport + buffer);
  491. },
  492. handleScroll(e) {
  493. const newScrollTop = e.target.scrollTop;
  494. if (newScrollTop !== this.scrollTop) {
  495. this.scrollTop = newScrollTop;
  496. if (!this.scrollRAF) {
  497. this.scrollRAF = requestAnimationFrame(() => {
  498. this.calculateVisibleRange();
  499. this.scrollRAF = null;
  500. });
  501. }
  502. }
  503. },
  504. resetScroll() {
  505. this.scrollTop = 0;
  506. if (this.$refs.virtualList) {
  507. this.$refs.virtualList.scrollTop = 0;
  508. }
  509. this.$nextTick(() => {
  510. this.calculateVisibleRange();
  511. });
  512. },
  513. toggleNodeExpand(node) {
  514. if (node.isLeaf || node.nodeDisabled) return;
  515. const key = node.key;
  516. if (this.expandedKeysSet.has(key)) {
  517. this.expandedKeysSet.delete(key);
  518. } else {
  519. this.expandedKeysSet.add(key);
  520. }
  521. this.expandedKeysSet = new Set(this.expandedKeysSet);
  522. this.$nextTick(() => this.calculateVisibleRange());
  523. },
  524. handleNodeClick(node) {
  525. if (node.nodeDisabled) return;
  526. if (!node.isLeaf && !this.parentSelectable) {
  527. this.toggleNodeExpand(node);
  528. return;
  529. }
  530. if (!node.isLeaf) {
  531. this.toggleNodeExpand(node);
  532. return;
  533. }
  534. if (!this.checkboxDisabled(node)) {
  535. const isCurrentlyChecked = this.checkedKeysSet.has(node.key);
  536. this.handleCheckboxChange(node, !isCurrentlyChecked);
  537. }
  538. },
  539. handleCheckboxChange(node, checked) {
  540. if (this.checkboxDisabled(node)) return;
  541. const newCheckedKeys = new Set(this.checkedKeysSet);
  542. const canNodeItselfBeInSet = node.isLeaf || this.parentSelectable || !this.checkStrictly;
  543. if (checked) {
  544. if (!this.multiple) {
  545. newCheckedKeys.clear();
  546. }
  547. if (canNodeItselfBeInSet) {
  548. newCheckedKeys.add(node.key);
  549. }
  550. if (!this.checkStrictly) {
  551. this.selectAllChildren(node, newCheckedKeys);
  552. this.checkParentSelection(node, newCheckedKeys);
  553. }
  554. } else {
  555. if (canNodeItselfBeInSet) {
  556. newCheckedKeys.delete(node.key);
  557. }
  558. if (!this.checkStrictly) {
  559. this.unselectAllChildren(node, newCheckedKeys);
  560. this.uncheckParentSelection(node, newCheckedKeys);
  561. }
  562. }
  563. this.checkedKeysSet = newCheckedKeys;
  564. this.emitChange();
  565. },
  566. selectAllChildren(node, checkedKeys) {
  567. if (node.childrenKeys && node.childrenKeys.length > 0) {
  568. node.childrenKeys.forEach(childKey => {
  569. const childNode = this.nodeMap.get(childKey);
  570. if (childNode && !childNode.nodeDisabled) {
  571. const canChildBeInSet = childNode.isLeaf || this.parentSelectable || !this.checkStrictly;
  572. if(canChildBeInSet){
  573. checkedKeys.add(childKey);
  574. }
  575. if (!childNode.isLeaf) {
  576. this.selectAllChildren(childNode, checkedKeys);
  577. }
  578. }
  579. });
  580. }
  581. },
  582. unselectAllChildren(node, checkedKeys) {
  583. if (node.childrenKeys && node.childrenKeys.length > 0) {
  584. node.childrenKeys.forEach(childKey => {
  585. const childNode = this.nodeMap.get(childKey);
  586. if (childNode) {
  587. const canChildBeInSet = childNode.isLeaf || this.parentSelectable || !this.checkStrictly;
  588. if(canChildBeInSet){
  589. checkedKeys.delete(childKey);
  590. }
  591. if (!childNode.isLeaf) {
  592. this.unselectAllChildren(childNode, checkedKeys);
  593. }
  594. }
  595. });
  596. }
  597. },
  598. checkParentSelection(node, checkedKeys) {
  599. if (!node.parentKey || this.checkStrictly) return;
  600. const parentNode = this.nodeMap.get(node.parentKey);
  601. if (parentNode && !parentNode.nodeDisabled && (this.parentSelectable || !this.checkStrictly)) {
  602. const allChildrenChecked = parentNode.childrenKeys.every(childKey => {
  603. const child = this.nodeMap.get(childKey);
  604. return !child || child.nodeDisabled || checkedKeys.has(childKey);
  605. });
  606. if (allChildrenChecked) {
  607. checkedKeys.add(parentNode.key);
  608. this.checkParentSelection(parentNode, checkedKeys);
  609. }
  610. }
  611. },
  612. uncheckParentSelection(node, checkedKeys) {
  613. if (!node.parentKey || this.checkStrictly) return;
  614. const parentNode = this.nodeMap.get(node.parentKey);
  615. if (parentNode && (this.parentSelectable || !this.checkStrictly)) {
  616. if (checkedKeys.has(parentNode.key)) {
  617. checkedKeys.delete(parentNode.key);
  618. this.uncheckParentSelection(parentNode, checkedKeys);
  619. }
  620. }
  621. },
  622. emitChange() {
  623. let finalKeys = [];
  624. const currentInternalCheckedKeys = Array.from(this.checkedKeysSet);
  625. // ALWAYS return only leaf nodes in the emitted value.
  626. currentInternalCheckedKeys.forEach(key => {
  627. const node = this.nodeMap.get(key);
  628. if (node && node.isLeaf) {
  629. finalKeys.push(key);
  630. }
  631. });
  632. finalKeys = [...new Set(finalKeys)]; // Deduplicate
  633. const finalNodes = finalKeys.map(k => this.nodeMap.get(k)).filter(Boolean);
  634. this.$emit('input', finalKeys);
  635. this.$emit('change', finalKeys, finalNodes);
  636. },
  637. syncCheckedKeysFromValue() {
  638. const newCheckedKeys = new Set();
  639. if (Array.isArray(this.value)) {
  640. this.value.forEach(key => {
  641. const strKey = String(key);
  642. const node = this.nodeMap.get(strKey);
  643. if (node) {
  644. // Value should ideally contain leaf keys if controlled by this component.
  645. // If an external value has a parent key, handle it for internal consistency.
  646. newCheckedKeys.add(strKey);
  647. if (!node.isLeaf && !this.checkStrictly) {
  648. this.selectAllChildren(node, newCheckedKeys);
  649. this.checkParentSelection(node, newCheckedKeys);
  650. } else if (node.isLeaf && !this.checkStrictly) {
  651. // If a leaf is added from value, ensure its parent's state is updated
  652. this.checkParentSelection(node, newCheckedKeys);
  653. }
  654. }
  655. });
  656. }
  657. this.checkedKeysSet = newCheckedKeys;
  658. if(this.popoverVisible) {
  659. this.$nextTick(() => this.calculateVisibleRange());
  660. }
  661. },
  662. handleConfirm() {
  663. this.emitChange(); // emitChange now handles filtering for leaf nodes
  664. this.popoverVisible = false;
  665. },
  666. handleClearInPopover() {
  667. this.checkedKeysSet.clear();
  668. this.emitChange();
  669. },
  670. handleClearOnTrigger() {
  671. if (this.disabled) return;
  672. this.checkedKeysSet.clear();
  673. this.emitChange();
  674. this.popoverVisible = false;
  675. },
  676. // 新增:处理全选/取消全选功能
  677. handleSelectAllToggle() {
  678. if (this.disabled) return;
  679. const leafNodes = this.currentResultLeafNodes;
  680. if (leafNodes.length === 0) return;
  681. const newCheckedKeys = new Set(this.checkedKeysSet);
  682. if (this.isAllCurrentResultsSelected) {
  683. // 取消全选:移除当前搜索结果中的所有叶子节点
  684. leafNodes.forEach(node => {
  685. if (!this.checkStrictly) {
  686. // 如果不是严格模式,需要处理父子节点关系
  687. this.handleCheckboxChange(node, false);
  688. } else {
  689. // 严格模式下直接移除
  690. newCheckedKeys.delete(node.key);
  691. }
  692. });
  693. if (this.checkStrictly) {
  694. this.checkedKeysSet = newCheckedKeys;
  695. this.emitChange();
  696. }
  697. } else {
  698. // 全选:选择当前搜索结果中的所有叶子节点
  699. if (!this.multiple) {
  700. // 单选模式下,只选择第一个叶子节点
  701. const firstLeaf = leafNodes[0];
  702. if (firstLeaf) {
  703. this.handleCheckboxChange(firstLeaf, true);
  704. }
  705. } else {
  706. // 多选模式下,选择所有叶子节点
  707. leafNodes.forEach(node => {
  708. if (!this.checkStrictly) {
  709. // 如果不是严格模式,需要处理父子节点关系
  710. if (!this.checkedKeysSet.has(node.key)) {
  711. this.handleCheckboxChange(node, true);
  712. }
  713. } else {
  714. // 严格模式下直接添加
  715. newCheckedKeys.add(node.key);
  716. }
  717. });
  718. if (this.checkStrictly) {
  719. this.checkedKeysSet = newCheckedKeys;
  720. this.emitChange();
  721. }
  722. }
  723. }
  724. },
  725. removeTag(nodeToRemove) {
  726. if (this.disabled || this.checkboxDisabled(nodeToRemove)) return;
  727. // nodeToRemove here will be a leaf node because displaySelectedNodes only gives leaf nodes
  728. this.handleCheckboxChange(nodeToRemove, false);
  729. },
  730. onPopoverShow() {
  731. if (this.$refs.popover && this.$refs.popover.$refs.reference) {
  732. this.triggerElRect = this.$refs.popover.$refs.reference.getBoundingClientRect();
  733. }
  734. this.$nextTick(() => {
  735. this.resetScroll();
  736. if (this.filterable && this.$refs.popover && this.$refs.popover.$el) {
  737. const inputEl = this.$refs.popover.$el.querySelector('.filter-input input');
  738. if (inputEl) inputEl.focus();
  739. }
  740. });
  741. }
  742. },
  743. beforeDestroy() {
  744. if (this.updateTimer) clearTimeout(this.updateTimer);
  745. if (this.scrollRAF) cancelAnimationFrame(this.scrollRAF);
  746. }
  747. };
  748. </script>
  749. <style scoped>
  750. .task-select-tree {
  751. display: inline-block;
  752. position: relative;
  753. font-size: 14px;
  754. }
  755. .select-trigger-wrapper {
  756. background-color: #fff;
  757. border-radius: 4px;
  758. border: 1px solid #dcdfe6;
  759. box-sizing: border-box;
  760. color: #606266;
  761. display: flex;
  762. align-items: center;
  763. min-height: 32px; /* Element UI default input height */
  764. padding: 0 30px 0 10px;
  765. position: relative;
  766. transition: border-color .2s cubic-bezier(.645,.045,.355,1);
  767. width: 100%;
  768. cursor: pointer;
  769. overflow: hidden; /* To contain tags */
  770. }
  771. .select-trigger-wrapper.is-disabled {
  772. background-color: #f5f7fa;
  773. border-color: #e4e7ed;
  774. color: #c0c4cc;
  775. cursor: not-allowed;
  776. }
  777. .select-trigger-wrapper:hover:not(.is-disabled) {
  778. border-color: #c0c4cc;
  779. }
  780. .select-trigger-wrapper.is-active:not(.is-disabled) {
  781. border-color: #409eff;
  782. }
  783. .tags-or-value-container {
  784. flex-grow: 1;
  785. display: flex;
  786. flex-wrap: wrap; /* Allow tags to wrap */
  787. gap: 4px; /* Space between tags */
  788. align-items: center;
  789. overflow: hidden; /* Hide overflowed tags/text */
  790. padding: 2px 0; /* Minimal padding for tags */
  791. min-height: 28px; /* Ensure container has some height */
  792. }
  793. .single-value-display,
  794. .placeholder-text {
  795. overflow: hidden;
  796. text-overflow: ellipsis;
  797. white-space: nowrap;
  798. line-height: 28px; /* Align with tags/input field */
  799. }
  800. .single-value-display {
  801. color: #606266;
  802. }
  803. .placeholder-text {
  804. color: #c0c4cc;
  805. }
  806. .icons-container {
  807. position: absolute;
  808. right: 8px;
  809. top: 50%;
  810. transform: translateY(-50%);
  811. display: flex;
  812. align-items: center;
  813. color: #c0c4cc;
  814. height: 100%;
  815. }
  816. .clear-icon {
  817. cursor: pointer;
  818. margin-right: 5px;
  819. display: none; /* Hidden by default */
  820. font-size: 14px;
  821. }
  822. .select-trigger-wrapper:hover .clear-icon {
  823. display: inline-block; /* Show on hover */
  824. }
  825. .select-trigger-wrapper.is-disabled .clear-icon {
  826. display: none !important; /* Never show if component is disabled */
  827. }
  828. .arrow-icon {
  829. transition: transform .3s;
  830. font-size: 14px;
  831. }
  832. .arrow-icon.is-reverse {
  833. transform: rotate(180deg);
  834. }
  835. .tree-container {
  836. border: 1px solid #ebeef5;
  837. border-radius: 4px;
  838. background-color: #fff;
  839. margin-top: 8px; /* If filter input is present */
  840. }
  841. .virtual-tree-list {
  842. position: relative; /* For absolute positioning of virtual-content if needed */
  843. }
  844. .virtual-content {
  845. width: 100%; /* Ensure full width */
  846. }
  847. .virtual-tree-node {
  848. box-sizing: border-box;
  849. cursor: pointer;
  850. user-select: none;
  851. border-bottom: 1px solid #f5f7fa; /* Lighter border */
  852. transition: background-color 0.2s;
  853. min-height: 36px; /* Ensure consistency with itemHeight */
  854. }
  855. .virtual-tree-node:last-child {
  856. border-bottom: none;
  857. }
  858. .virtual-tree-node:hover:not(.is-disabled) {
  859. background-color: #f5f7fa;
  860. }
  861. .virtual-tree-node.is-checked:not(.is-disabled) {
  862. /* color: #409eff; */
  863. }
  864. .virtual-tree-node.is-checked .node-label {
  865. /* font-weight: bold; */
  866. }
  867. .virtual-tree-node.is-disabled {
  868. color: #c0c4cc;
  869. cursor: not-allowed;
  870. background-color: #f5f7fa;
  871. }
  872. .virtual-tree-node.is-disabled .node-label {
  873. color: #c0c4cc;
  874. }
  875. .node-expand-container {
  876. width: 20px;
  877. height: 100%;
  878. display: flex;
  879. align-items: center;
  880. justify-content: center;
  881. flex-shrink: 0;
  882. }
  883. .node-expand-icon {
  884. cursor: pointer;
  885. color: #c0c4cc;
  886. transition: transform 0.2s, color 0.2s;
  887. font-size: 12px;
  888. }
  889. .node-expand-icon:hover {
  890. color: #409eff;
  891. }
  892. .node-expand-placeholder {
  893. width: 12px;
  894. height: 12px;
  895. display: block;
  896. }
  897. .node-checkbox {
  898. margin: 0 8px 0 4px;
  899. flex-shrink: 0;
  900. }
  901. .node-label {
  902. flex: 1;
  903. text-overflow: ellipsis;
  904. white-space: nowrap;
  905. line-height: 1.4;
  906. font-weight: normal;
  907. padding-right: 5px;
  908. }
  909. .node-detail-text {
  910. font-size: 12px;
  911. color: #909399;
  912. margin-left: 8px;
  913. flex-shrink: 0;
  914. white-space: nowrap;
  915. }
  916. .popover-content-wrapper {
  917. max-width: 100%;
  918. }
  919. .popover-footer {
  920. display: flex;
  921. justify-content: space-between;
  922. align-items: center;
  923. padding-top: 10px;
  924. border-top: 1px solid #ebeef5;
  925. margin-top: 10px;
  926. }
  927. .selected-count {
  928. font-size: 12px;
  929. color: #909399;
  930. }
  931. .filter-input {
  932. margin-bottom: 8px;
  933. }
  934. .trigger-tag {
  935. margin: 1px 0;
  936. max-width: 150px;
  937. }
  938. .trigger-tag >>> .el-tag__content {
  939. overflow: hidden;
  940. text-overflow: ellipsis;
  941. white-space: nowrap;
  942. }
  943. </style>
  944. <style>
  945. /* Global styles for popper, if needed */
  946. .task-select-tree-popper.el-popover {
  947. padding: 12px;
  948. }
  949. .task-select-tree-popper .el-checkbox__input.is-disabled .el-checkbox__inner {
  950. background-color: #edf2fc;
  951. border-color: #dcdfe6;
  952. cursor: not-allowed;
  953. }
  954. .task-select-tree-popper .el-checkbox__input.is-checked .el-checkbox__inner {
  955. background-color: #409eff;
  956. border-color: #409eff;
  957. }
  958. .task-select-tree-popper .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner {
  959. background-color: #f2f6fc;
  960. border-color: #dcdfe6;
  961. }
  962. </style>