index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. <template>
  2. <div v-if="editable"
  3. v-clickoutside="handleClose"
  4. class="el-tree-select"
  5. @click="toggleTree">
  6. <div v-if="multiple"
  7. ref="tags"
  8. :style="{ 'max-width': inputWidth - 32 + 'px' }"
  9. class="el-tree-select__tags">
  10. <transition-group @after-leave="resetInputHeight">
  11. <template v-for="(item,i) in selected">
  12. <el-tag :key="item.value+i"
  13. :closable="!selectDisabled"
  14. :size="collapseTagSize"
  15. type="info"
  16. disable-transitions
  17. @close.stop="deleteTag(item)">
  18. <span class="el-tree-select__tags-text">{{ displayMode==="name"? item.label:item.path }}</span>
  19. </el-tag>
  20. </template>
  21. </transition-group>
  22. <input v-if="filterable"
  23. ref="input"
  24. v-model="query"
  25. :disabled="selectDisabled"
  26. :placeholder="currentPlaceholder"
  27. :autocomplete="false"
  28. :style="{ width: inputLength + 'px'}"
  29. type="text"
  30. class="el-tree-select__input"
  31. @focus="handleFocus"
  32. @keydown.esc.stop.prevent="visible = false"
  33. @keydown.delete="deletePrevTag"
  34. @input="e => handleQueryChange(e.target.value)">
  35. </div>
  36. <el-input ref="reference"
  37. v-model="selectedLabel"
  38. :disabled="selectDisabled"
  39. :readonly="selectReadonly"
  40. :validate-event="false"
  41. :size="selectSize"
  42. :class="{ 'is-focus': visible }"
  43. :placeholder="currentPlaceholder"
  44. type="text"
  45. @focus="handleFocus"
  46. @keyup.native="onInputChange"
  47. @mouseenter.native="inputHovering = true"
  48. @mouseleave.native="inputHovering = false">
  49. <template v-if="$slots.prefix"
  50. slot="prefix">
  51. <slot name="prefix" />
  52. </template>
  53. <i slot="suffix"
  54. :class="suffixIconClass"
  55. @click="handleIconClick" />
  56. </el-input>
  57. <transition name="el-zoom-in-top">
  58. <div v-show="visible"
  59. ref="popper"
  60. :style="{minWidth: inputWidth + 'px'}"
  61. class="el-tree-select-dropdown el-popper">
  62. <el-scrollbar wrap-class="el-tree-select-dropdown__wrap">
  63. <el-tree ref="tree"
  64. :data="data"
  65. :lazy="lazy"
  66. :load="load"
  67. :check-on-click-node="checkOnClickNode"
  68. :props="treeProps"
  69. :show-checkbox="showCheckbox"
  70. :expand-on-click-node="false"
  71. :check-strictly="checkStrictly"
  72. :filter-node-method="filterNodeMethod"
  73. :default-checked-keys="checkedKeys"
  74. :node-key="nodeKey"
  75. :empty-text="emptyText"
  76. :current-node-key="currentNodeKey"
  77. default-expand-all
  78. highlight-current
  79. @check="handleCheck"
  80. @node-click="handleNodeClick">
  81. <template v-slot="scope">
  82. <span class="el-tree-node__label">
  83. <i v-if="icon"
  84. :class="icon(scope.data)" /> {{ scope.node.label }}
  85. </span>
  86. </template>
  87. </el-tree>
  88. </el-scrollbar>
  89. </div>
  90. </transition>
  91. </div>
  92. <!--只读 文本样式-->
  93. <div v-else
  94. class="el-tree-select">
  95. <template v-if="$utils.isNotEmpty(selected)">
  96. <div v-if="multiple"
  97. class="el-tree-select__tags_readonly">
  98. <template v-for="(item,i) in selected">
  99. <el-tag v-if="$utils.isNotEmpty(item.value)"
  100. :key="item.value+i"
  101. type="info"
  102. disable-transitions
  103. class="el-tree-select__tags-text ibps-mr-5 ">
  104. <template v-if="displayMode === 'name'">
  105. {{ item.label }}
  106. </template>
  107. <template v-else>
  108. {{ item.path }}
  109. </template>
  110. </el-tag>
  111. </template>
  112. </div>
  113. <el-tag v-else
  114. type="info"
  115. disable-transitions>
  116. {{ selectedLabel }}
  117. </el-tag>
  118. </template>
  119. </div>
  120. </template>
  121. <script>
  122. // 参考 https://github.com/ElemeFE/element/blob/29e76cda035bb8a951e6792a33ba4ff5056515a0/packages/tree-select/src/main.vue
  123. // 可能下个版本出现,再进行修复
  124. // API https://deploy-preview-12104--element.netlify.com/#/zh-CN/component/tree-select
  125. // import ElInput from 'element-ui/packages/input'
  126. // import ElTree from 'element-ui/packages/tree/src/tree.vue'
  127. import Clickoutside from '@/plugins/element-ui/src/utils/clickoutside'
  128. import Popper from '@/plugins/element-ui/src/utils/vue-popper'
  129. import { valueEquals } from '@/plugins/element-ui/src/utils/util'
  130. import { addResizeListener, removeResizeListener } from '@/plugins/element-ui/src/utils/resize-event'
  131. import emitter from '@/plugins/element-ui/src/mixins/emitter'
  132. import PopupManager from '@/utils/popup'
  133. // TODO: 等 vue-popper 合并后,这里还需要做出调整
  134. const popperMixin = {
  135. props: {
  136. placement: {
  137. type: String,
  138. default: 'bottom-start'
  139. },
  140. appendToBody: false,
  141. arrowOffset: Popper.props.arrowOffset,
  142. offset: Popper.props.offset,
  143. boundariesPadding: Popper.props.boundariesPadding,
  144. popperOptions: Popper.props.popperOptions,
  145. visibleArrow: {
  146. type: Boolean,
  147. default: true
  148. }
  149. },
  150. methods: Popper.methods,
  151. data: Popper.data,
  152. beforeDestroy: Popper.beforeDestroy
  153. }
  154. const sizeMap = {
  155. 'medium': 36,
  156. 'small': 32,
  157. 'mini': 28
  158. }
  159. export default {
  160. name: 'ibps-tree-select',
  161. // components: {
  162. // ElInput,
  163. // ElTree
  164. // },
  165. directives: { Clickoutside },
  166. mixins: [popperMixin, emitter],
  167. provide() {
  168. return {
  169. elTreeSelect: this
  170. }
  171. },
  172. inject: {
  173. elForm: {
  174. default: ''
  175. },
  176. elFormItem: {
  177. default: ''
  178. }
  179. },
  180. props: {
  181. data: {
  182. type: Array,
  183. required: true
  184. },
  185. value: {
  186. type: [String, Number, Array, Object],
  187. default: ''
  188. },
  189. multiple: Boolean,
  190. disabled: Boolean,
  191. readonly: {
  192. type: Boolean,
  193. default: false
  194. },
  195. readonlyText: { // 只读样式 【text ,original】
  196. type: String,
  197. default: 'original',
  198. validator(val) {
  199. return ['original', 'text'].indexOf(val) > -1
  200. }
  201. },
  202. clearable: Boolean,
  203. size: {
  204. type: String,
  205. validator(val) {
  206. return ['medium', 'small', 'mini'].indexOf(val) > -1
  207. }
  208. },
  209. nodeKey: {
  210. type: String,
  211. default: 'id'
  212. },
  213. props: Object,
  214. placeholder: {
  215. type: String,
  216. default() {
  217. return '请选择'
  218. }
  219. },
  220. selectMode: { // 选值模式 leaf、any
  221. type: String,
  222. default: 'any',
  223. validator: function (value) {
  224. return ['any', 'leaf'].indexOf(value) !== -1
  225. }
  226. },
  227. displayMode: { // 显示模式 path 、name
  228. type: String,
  229. default: 'name',
  230. validator: function (value) {
  231. return ['name', 'path'].indexOf(value) !== -1
  232. }
  233. },
  234. separator: { // 树形选项分隔符
  235. type: String,
  236. default: '/'
  237. },
  238. warningText: {
  239. type: String,
  240. default: '请选择叶子节点'
  241. },
  242. allowSelection: Function, // 允许的节点
  243. lazy: Boolean,
  244. load: Function,
  245. showCheckbox: Boolean,
  246. checkStrictly: Boolean,
  247. filterable: Boolean,
  248. filterMethod: Function,
  249. emptyText: String,
  250. showCheckedStrategy: { // 显示多选按钮
  251. type: String,
  252. default: 'child',
  253. validator(val) {
  254. return ['all', 'parent', 'child'].indexOf(val) > -1
  255. }
  256. },
  257. icon: Function
  258. },
  259. data() {
  260. return {
  261. query: '',
  262. selectedLabel: '',
  263. inputLength: 0,
  264. visible: false,
  265. inputWidth: 0,
  266. inputHovering: false,
  267. treeVisibleOnFocus: false,
  268. selected: this.multiple ? [] : {},
  269. checkOnClickNode: false,
  270. treeProps: {
  271. label: 'name',
  272. children: 'children'
  273. }
  274. }
  275. },
  276. computed: {
  277. suffixIconClass() {
  278. let classes = ['el-tree-select__caret', 'el-input__icon']
  279. const criteria = this.clearable &&
  280. !this.selectDisabled &&
  281. this.inputHovering &&
  282. !this.multiple &&
  283. this.value !== undefined &&
  284. this.value !== null &&
  285. this.value !== ''
  286. if (criteria) {
  287. classes = [...classes, 'el-icon-circle-close', 'is-show-close']
  288. } else {
  289. classes.push('el-icon-arrow-down')
  290. if (this.visible) {
  291. classes.push('is-reverse')
  292. }
  293. }
  294. return classes
  295. },
  296. selectDisabled() {
  297. return this.disabled || (this.elForm || {}).disabled
  298. },
  299. selectSize() {
  300. return this.size || (this.elFormItem || {}).elFormItemSize || (this.$ELEMENT || {}).size
  301. },
  302. collapseTagSize() {
  303. return ['small', 'mini'].indexOf(this.selectSize) > -1
  304. ? 'mini'
  305. : 'small'
  306. },
  307. editable() {
  308. return !this.readonly || (this.readonly && this.readonlyText === 'original')
  309. },
  310. selectReadonly() {
  311. const isIE = !this.$isServer && !isNaN(Number(document.documentMode))
  312. return this.readonly || !this.filterable || this.multiple || (!isIE && !this.visible)
  313. },
  314. checkedKeys() {
  315. if (this.multiple && this.showCheckbox) {
  316. return this.value || []
  317. } else {
  318. return []
  319. }
  320. },
  321. currentNodeKey() {
  322. if (this.multiple) {
  323. return this.value && this.value.length > 0 ? this.value[0].value : ''
  324. } else {
  325. if (this.value && Array.isArray(this.value)) {
  326. return this.value[0] || ''
  327. } else {
  328. return this.value || ''
  329. }
  330. }
  331. },
  332. currentPlaceholder() {
  333. if (this.$utils.isEmpty(this.value)) {
  334. return this.placeholder
  335. } else {
  336. return ''
  337. }
  338. },
  339. labelKey() {
  340. return this.treeProps['label'] || 'name'
  341. }
  342. },
  343. watch: {
  344. visible(val) {
  345. if (val) {
  346. this.updatePopper()
  347. if (this.multiple && this.filterable) {
  348. this.$refs.input && this.$refs.input.focus()
  349. }
  350. this.$emit('focus', this)
  351. if (!this.multiple && this.filterable) {
  352. this.broadcast('ElInput', 'inputSelect')
  353. }
  354. } else {
  355. this.destroyPopper()
  356. this.$refs.input && this.$refs.input.blur()
  357. this.$emit('blur', this)
  358. if (!this.multiple) {
  359. this.setSelected()
  360. }
  361. if (this.filterable) {
  362. setTimeout(() => {
  363. this.handleQueryChange('')
  364. }, 100)
  365. }
  366. }
  367. },
  368. value(val, oldVal) {
  369. if (this.multiple) {
  370. this.resetInputHeight()
  371. }
  372. this.setSelected()
  373. if (!valueEquals(val, oldVal)) {
  374. this.dispatch('ElFormItem', 'el.form.change', val)
  375. }
  376. },
  377. data() {
  378. this.setSelected()
  379. },
  380. props: {
  381. handler(val, oldVal) {
  382. this.treeProps = val
  383. },
  384. deep: true,
  385. immediate: true
  386. }
  387. },
  388. mounted() {
  389. if (this.multiple && this.showCheckbox) {
  390. this.checkOnClickNode = true
  391. }
  392. if (this.editable) {
  393. this.referenceElm = this.$refs.reference.$el
  394. this.popperElm = this.$refs.popper
  395. this.inputWidth = this.$refs.reference.$el.getBoundingClientRect().width
  396. addResizeListener(this.$el, this.handleResize)
  397. this.setSelected()
  398. this.fixZIndex()
  399. }
  400. },
  401. beforeDestroy() {
  402. if (this.editable) {
  403. removeResizeListener(this.$el, this.handleResize)
  404. }
  405. },
  406. methods: {
  407. /**
  408. * zxh 修复zindex 不是最高的被遮住
  409. */
  410. fixZIndex() {
  411. PopupManager.getZIndex()
  412. },
  413. handleFocus(event) {
  414. this.treeVisibleOnFocus = true
  415. this.visible = true
  416. this.$emit('focus', event)
  417. },
  418. handleClose() {
  419. this.visible = false
  420. },
  421. toggleTree() {
  422. if (this.selectDisabled) return
  423. if (this.treeVisibleOnFocus) {
  424. this.treeVisibleOnFocus = false
  425. } else {
  426. this.visible = !this.visible
  427. }
  428. },
  429. handleIconClick(event) {
  430. if (this.suffixIconClass.indexOf('el-icon-circle-close') > -1) {
  431. event.stopPropagation()
  432. this.visible = false
  433. this.$emit('input', '')
  434. this.emitChange('')
  435. this.$emit('clear')
  436. this.selected = {}
  437. this.selectedLabel = ''
  438. }
  439. },
  440. emitChange(val) {
  441. if (!valueEquals(this.value, val)) {
  442. this.$emit('change', val, this.getNodeData(val))
  443. }
  444. },
  445. handleQueryChange(val) {
  446. this.$refs.tree && this.$refs.tree.filter(val)
  447. },
  448. handleNodeClick(data, node, tree) {
  449. if (this.showCheckbox) return
  450. if (typeof this.allowSelection === 'function' && !this.allowSelection(data, node, tree)) {
  451. return false
  452. }
  453. if ((this.selectMode === 'leaf' && !node.isLeaf) || (this.selectMode === 'leaf' && node.data.isLeaf === false)) {
  454. this.$message.warning(this.warningText)
  455. return false
  456. }
  457. this.setSelectedNode(data, node, tree)
  458. },
  459. setSelectedNode(data, node, tree) {
  460. const value = data[this.nodeKey]
  461. if (this.multiple) {
  462. const valueCopy = this.value.slice()
  463. const index = this.getValueIndex(valueCopy, value)
  464. if (index > -1) {
  465. valueCopy.splice(index, 1)
  466. } else {
  467. valueCopy.push(value)
  468. }
  469. this.$refs.input && this.$refs.input.focus()
  470. this.$emit('input', valueCopy)
  471. this.emitChange(valueCopy)
  472. } else {
  473. // if (value === this.value) {
  474. // value = ''
  475. // }
  476. this.$emit('input', value)
  477. this.emitChange(value)
  478. this.visible = false
  479. }
  480. },
  481. handleCheck(data, info) {
  482. const { checkedNodes } = info
  483. let nodes = []
  484. switch (this.showCheckedStrategy) {
  485. case 'parent':
  486. nodes = this.getTreeCheckedParentNodes()
  487. break
  488. case 'child':
  489. nodes = checkedNodes.filter(({ children }) => !(children && children.length))
  490. break
  491. default:
  492. nodes = checkedNodes
  493. break
  494. }
  495. const values = nodes.map((node) => node[this.nodeKey])
  496. if (this.filterable) this.$refs.input && this.$refs.input.focus()
  497. this.$emit('input', values)
  498. this.emitChange(values)
  499. },
  500. getTreeCheckedParentNodes() {
  501. const checkedNodes = []
  502. const traverse = (node) => {
  503. const childNodes = node.root ? node.root.childNodes : node.childNodes
  504. childNodes.forEach(child => {
  505. if (child.checked && !child.isLeaf) {
  506. checkedNodes.push({
  507. label: child.data.label,
  508. value: child.data.value
  509. })
  510. } else {
  511. traverse(child)
  512. }
  513. })
  514. }
  515. traverse(this.$refs.tree.store)
  516. return checkedNodes
  517. },
  518. getNodeData(value) {
  519. let node = null
  520. console.log('value',value)
  521. if (Array.isArray(this.data)) {
  522. const traverse = (arr, lablePrefix = []) => {
  523. for (let i = 0; i < arr.length; i++) {
  524. const child = arr[i]
  525. let labelData = child[this.labelKey]
  526. if (typeof this.labelKey !== 'string') {
  527. labelData = this.labelKey(child)
  528. }
  529. if (child[this.nodeKey] === value) {
  530. node = {
  531. label: labelData,
  532. value: child[this.nodeKey],
  533. path: [...lablePrefix, labelData].join(this.separator),
  534. data: child
  535. }
  536. break
  537. } else if (child.children && child.children.length > 0) {
  538. traverse(child.children, [...lablePrefix, labelData])
  539. }
  540. }
  541. }
  542. traverse(this.data)
  543. }
  544. return node
  545. },
  546. onInputChange() {
  547. if (this.filterable && this.query !== this.selectedLabel) {
  548. this.query = this.selectedLabel
  549. this.handleQueryChange(this.query)
  550. }
  551. },
  552. filterNodeMethod(value, data) {
  553. if (!value) return true
  554. this.$nextTick(this.updatePopper)
  555. if (typeof this.filterMethod === 'function') {
  556. return this.filterMethod(value, data)
  557. } else {
  558. if (data[this.labelKey]) {
  559. return data[this.labelKey].indexOf(value) !== -1
  560. } else {
  561. if (data.label) {
  562. return data.label.indexOf(value) !== -1
  563. } else {
  564. return true
  565. }
  566. }
  567. }
  568. },
  569. resetInputHeight() {
  570. this.$nextTick(() => {
  571. if (!this.$refs.reference || !this.$refs.tags) return
  572. const inputEl = this.$refs.reference.$refs.input
  573. const tags = this.$refs.tags
  574. let height = sizeMap[this.selectSize] || 40
  575. if (this.selected.length !== 0) {
  576. height = Math.max((tags.clientHeight + (tags.clientHeight > height ? 6 : 0)), height)
  577. }
  578. inputEl.style.height = `${height}px`
  579. if (this.visible) {
  580. this.updatePopper()
  581. }
  582. })
  583. },
  584. getValueIndex(arr = [], value) {
  585. let index = -1
  586. arr.some((item, i) => {
  587. if (item === value) {
  588. index = i
  589. return true
  590. } else {
  591. return false
  592. }
  593. })
  594. return index
  595. },
  596. deletePrevTag(e) {
  597. if (e.target.value.length <= 0) {
  598. this.selected.splice(-1, 1)
  599. const values = this.selected.map(({ value }) => value)
  600. this.$emit('input', values)
  601. this.emitChange(values)
  602. }
  603. },
  604. deleteTag(item) {
  605. this.selected = this.selected.filter(selectItem => selectItem !== item)
  606. const values = this.selected.map(({ value }) => value)
  607. this.$emit('input', values)
  608. this.emitChange(values)
  609. },
  610. resetInputWidth() {
  611. this.inputWidth = this.$refs.reference && this.$refs.reference ? this.$refs.reference.$el.getBoundingClientRect().width : 0
  612. },
  613. handleResize() {
  614. this.resetInputWidth()
  615. if (this.multiple) this.resetInputHeight()
  616. },
  617. setSelected() {
  618. if (this.multiple) {
  619. const result = []
  620. if (Array.isArray(this.value)) {
  621. this.value.forEach(value => {
  622. const node = this.getNodeData(value)
  623. if (node) result.push(node)
  624. })
  625. }
  626. this.selected = result
  627. if (result.length === 0) {
  628. this.selectedLabel = ''
  629. }
  630. this.$nextTick(this.resetInputHeight)
  631. } else {
  632. const node = this.getNodeData(this.value)
  633. if (node) {
  634. this.selected = node
  635. this.selectedLabel = this.displayMode === 'name' ? node.label : node.path
  636. if (this.filterable) this.query = this.selectedLabel
  637. } else {
  638. this.selected = {}
  639. this.selectedLabel = ''
  640. if (this.filterable) this.query = ''
  641. }
  642. }
  643. }
  644. }
  645. }
  646. </script>
  647. <style lang="scss">
  648. @import "~@/assets/styles/components/tree-select.scss";
  649. </style>