Vue封裝組件系列文章javascript
根據產品原型實現一個級聯組件,下面看演示圖html
應用場景不少,如:後臺管理系統,旅遊系統,廣告投放系統,營銷系統...等,如今流行Vue
,React
,Anagular
三大框架,下面看看怎麼使用Vue
實現
產品經理的評審功能需求以下前端
...
表明過長,鼠標移上時顯示所有內容Vue.js 的核心包括一套「響應式系統」。vue
"響應式",開發思路跟Jquery的開發思路徹底不一樣。java
「響應式」,是指當數據改變後,Vue 會通知到使用該數據的代碼。例如,視圖渲染中使用了數據,數據改變後,視圖也會自動更新。ios
根據地區數據 JSON
能夠看出其結構git
[ { "value": "中國", "key": 1156, "id": 1156, "children": [ { "value": "北京市", "id": 10000, "key": 10000, "children": [] }, { "value": "河北省", "key": 200107, "id": 200107, "children": [ { "value": "石家莊", "key": 20010701, "id": 20010701 }, { "value": "唐山市", "key": 20010702, "id": 20010702, "children": [ { "value": "路南區", "key": 2001070201, "id": 2001070201, "children": [] } ] } ] } ]
中國github
xx省json
xx市segmentfault
xx市
這是一個循環嵌套的數據對象,而組件嵌套彷佛不能知足產品需求,若是使用數組來代替層級,彷佛能夠解決數據嵌套的問題
array => level 1 -> level 2 -> level 3 -> level 4
level 1 => current, children => level 2 (array)
level 2 => current, children => level 3 (array)
...
每一個level
都是一個總體,
有標題title
有全選 計算data中是否都選中select
子集的集合數據data
有當前選中current
標記當期層級 數組的索引level
首先定義個空的數組表明組件
const array = []
把數據處理成數組格式就能展開這個組件,那怎麼處理數據呢
初始化組件時不是全部都顯示,必須讓用戶選擇當前一個頂級大類
拿到全部頂級大類,並構建第一個元素
title = 省級
data = 頂級大類
current = 空
level = 1
select = false
array.push({title, select, data, current, level})
在選擇頂級大類時,給這個數組增長其一個子集元素
array.push({title, select, data, current, level})
...
依次類推
獲取組件的選擇結果,
能夠過濾數據的check 屬性獲得,
可以使用Vue的計算屬性得知隨時的結果
結果選擇框能夠直接綁定已選的計算組件,可構建結果UI
用來負責組件框架, 左右分欄,
左邊是選擇區域, 右邊是結果區域
這個是組件引用層,統一對外提供導入props 數據
和 導出的 emit 事件
組件須要作到徹底配置化,內部因此參數須要被抽象
更具層級平均分配空間,全部在橫向固定空間中,不能作過多的層級,太窄了無法顯示
由於須要循環顯示其層級,抽離層級爲佈局組件,佈局組件由 標題
和 滾動的選擇區域
組成
<Row> <Col :span="col" v-for="(box, idx) in resource" :key="idx"> <select-item :title="box.title"> <select-box v-model="box.current" :data="box.data" :level="box.level" @on-child="pushChild" @on-select="selectAll" /> </select-item> </Col> </Row>
在有選擇時才顯示,有標題欄顯示,結果區可統計結果個數,選擇項使用Tag標籤,支持快速刪除,創建縱向滾動條
可以使用佈局組件 與選擇區保持風格統一,
<Col span="7" offset="1"> <select-item v-if="resultLen && transfer" title="已選" clear @on-clear="$emit('on-clear', {list: data})"> <div v-for="item in result" :key="item.id" class="c-pop-tip"> <Tag :name="item.value" closable class="c-tag-item" @on-close="handleClose">{{item.value}}</Tag> </div> </select-item> </Col>
要兼容選擇區與結果區使用,因此統計個數得有開關控制,
邊框,顏色 UI 控制
全選狀態按鈕 CheckBox
搜索輸入框組件帶搜索按鈕
抽象 清空按鈕UI
抽象 統計個數UI
最關鍵的組件就算這個了
選擇項應該能夠類分紅兩種,
使用條件判斷便可實現分支顯示,可是用 CheckBox
組件,他自己有change功能,若是是v-model
綁定的,他的值改變,會讓主樹上通知到此次更新,
這針對於上面的第二種,在這層級沒有子級能夠完成他的工做,他的更新,他的父級能夠計算半選狀態,也能夠在父級計算選擇的個數,可是若是是有子級,這裏要響應他的全部子集也要選中,若是子集選中後,子集的全選也是選中狀態
在開發的過程當中,這裏的變化關係很複雜,不用圖形可解釋不清楚
點擊行能夠更改子集變化,
選中子集也要更改數據變化
v-model 綁定數據的好處是: 數據在內部發生了改變,而在原始端一樣改變了,只要使用就能夠了,
固然在使用上也有些不方便的地方,
props導入的數據,經過什麼props 屬性接收呢, value
... props: { value: { type: Array } } ...
在組件內部是不能Set 改變的,只能經過事件傳到父組件中來
經過什麼方法名來傳呢, input
(初級不少人不知道) this.$emit('input', val)
在初始化過程當中,構建第一層級組件的 title
data
current
level
假使省市json 數據爲 cityJson
構建第一層級的data
const data = this.cityJson.map(ret => { delete ret.children return ret })
當用戶選擇層級的 item
時觸發 動做新增層級數據
當用戶選中層級的 item
時觸發 動做新增層級數據 選中該層級下全部數據
selectAll ({level, check, cat}) { let index = level - 2 let current = index > -1 ? this.resource[index].current : '' cat && (current = cat) this.$emit('on-select', { check, current, list: this.data }) }
拋到根組件引用到處理,主要是循環當前層級的數據的check 屬性爲true
全選的checkbox 要屏蔽不能選擇,讓其選擇事件通信子組件中
搜索有兩種實現,一種是前端正則實現,這裏比較考驗前端的正則能力,還有優化循環速度
另外一種解法,就是經過後臺查詢結果,在根據結果篩選出數據顯示,不能直接使用後端數據,由於破壞了樹根數據,是無法計算選擇的,在搜索裏有清空功能,清空後的選擇搜索前的當前項,代碼以下
clearBox (level) { let current const index = level - 2 // 還原原來全部的data if (index > -1) { current = this.resource[index].current this.pushChild({ level: index + 1, current }) } else this.resource[0].data = this.data }
結果框的清空的邏輯相對比較簡單,只要把全部選擇的數據 check 屬性爲 false
固然也能夠用循環都設置一遍,但設置這裏都要使用$set 去更新數據
<select-item v-if="resultLen && transfer" title="已選" clear @on-clear="$emit('on-clear', {list: data})"> <div v-for="item in result" :key="item.id" class="c-pop-tip"> <Tag :name="item.value" closable class="c-tag-item" @on-close="handleClose">{{item.value}}</Tag> </div> </select-item>
事件是組件的關鍵的開發,事件的響應在引用的組件裏處理
貼上全部源代碼,不免裏面有些引用的文件,若是不能直接使用,請不要噴,由於這篇文章不是送個伸手黨的,是你有必定的基礎,想提高一下技能的你
<template> <div class="c-selecter"> <Row :gutter="12"> <Col span="16"> <Row> <Col :span="col" v-for="(box, idx) in resource" :key="idx"> <select-item :title="box.title"> <select-box v-model="box.current" :data="box.data" :level="box.level" @on-child="pushChild" @on-select="selectAll" /> </select-item> </Col> </Row> </Col> <Col span="7" offset="1"> <select-item v-if="resultLen && transfer" title="已選" clear @on-clear="$emit('on-clear', {list: data})"> <div v-for="item in result" :key="item.id" class="c-pop-tip"> <Tag :name="item.value" closable class="c-tag-item" @on-close="handleClose">{{item.value}}</Tag> </div> </select-item> </Col> </Row> </div> </template> <script> import SelectItem from './select-item.vue' import SelectBox from './select-box.vue' export default { name: 'selecter', components: { SelectItem, SelectBox }, props: { value: { type: Array }, title: { type: Array }, data: { type: Array }, transfer: { type: Boolean, default: true } }, data () { return { resource: [] } }, computed: { col () { return 24 / this.resource.length }, result () { return this.value }, resultLen () { return Boolean(this.value.length) } }, watch: { data (nVal) { if (nVal && nVal.length) this.updateResource() else this.resource = [] } }, methods: { updateResource () { this.resource = [] this.resource.push({ data: this.data, current: '', level: 1, title: this.title[0] }) }, handleClose (event, name) { this.$emit('on-delete', {list: this.data, name}) }, selectAll ({level, check, cat}) { let index = level - 2 let current = index > -1 ? this.resource[index].current : '' cat && (current = cat) this.$emit('on-select', { check, current, list: this.data }) }, pushChild (params) { const {item, level} = params const len = this.resource.length if (level <= len - 1) { this.resource.splice(level, len - level) } this.resource.push({ data: item.children, current: '', level: level + 1, title: this.title[level] || item.value }) this.resource[level - 1].current = item.value } }, created () { this.updateResource() } } </script> <style lang="stylus" scoped> @import "~assets/styles/mixin.styl" .c-pop-tip width 100% .c-tag-item width 90% margin 8px 8px 0 padding 2px 6px display block font-size 14px height 28px >>>span.ivu-tag-text $no-wrap() width calc(100% - 22px) display inline-block >>>.ivu-icon-ios-close top -8px </style>
<template> <div class="c-select-item"> <div class="c-header"> <span class="c-header-title">{{title}}</span> <span class="c-header-clear" v-if="clear" @click="$emit('on-clear')">清空所有</span> </div> <div class="c-selecter-content"> <slot></slot> </div> </div> </template> <script> export default { name: 'selectItem', props: { title: { type: String }, clear: { type: Boolean } } } </script> <style lang="stylus" scoped> @import "~assets/styles/mixin.styl" .c-select-item background-color #fff border solid 1px #dee4f5 .c-header padding 0 12px height 34px font-size 14px color #333 border-bottom solid 1px #dee4f5 background-color #fafbfe .c-header-title, .c-header-clear height 34px line-height 34px vertical-align middle .c-header-clear color #598fe6 float right cursor pointer .c-selecter-content $scroll() height 246px width 100% padding-bottom 8px </style>
<template> <div class="c-select-box"> <div class="c-check-all"> <div class="c-item-select c-cataract" @click="selectAll"></div> <Checkbox class="c-check-item" v-model="all">全選</Checkbox> </div> <div v-for="item in data" :key="item.id"> <div v-if="item.children && item.children.length" :class="itemClasses(item)" @click="$emit('on-child', {item, level})"> <Checkbox v-model="item.check" :indeterminate="itemIndeterminate(item)"></Checkbox> <span>{{item.value}}</span> <Icon type="ios-arrow-forward" class="c-check-arrow" size="14" color="#c1c1c1" /> <span class="c-item-checkbox c-cataract" @click="selectItem(item)"></span> </div> <Checkbox v-else class="c-check-item" v-model="item.check">{{item.value}}</Checkbox> </div> </div> </template> <script> const computeChild = (list, Vue) => { list.forEach(item => { if (item.children && item.children.length) { const child = item.children if (child.every(ret => ret.check)) Vue.$set(item, 'check', true) else Vue.$set(item, 'check', false) computeChild(child, Vue) } }) } export default { name: 'selectBox', props: { value: { type: [String, Number] }, data: { type: Array }, level: { type: Number } }, computed: { itemClasses () { return item => { const cls = ['c-check-item'] item.value === this.value && cls.push('active') return cls } }, all () { const len = this.data.filter(ret => ret.check).length return this.data.length === len } }, methods: { selectAll () { this.$emit('on-select', { check: !this.all, level: this.level }) }, selectItem (item) { this.$emit('on-select', { check: !item.check, level: this.level, cat: item.value }) }, itemIndeterminate (child) { const hasChild = (meta) => { return meta.children.reduce((sum, item) => { let foundChilds = [] if (item.check) sum.push(item) if (item.children) foundChilds = hasChild(item) return sum.concat(foundChilds) }, []) } const some = hasChild(child).length > 0 const every = child.children && child.children.every(ret => ret.check) return some && !every } }, watch: { data: { handler (nVal, oVal) { computeChild(nVal, this) }, deep: true } }, mounted () { computeChild(this.data, this) } } </script> <style lang="stylus" scoped> @import "~assets/styles/mixin.styl" .c-cataract display block position absolute top 0 left 0 z-index 8 cursor pointer .c-check-all width 100% height 36px position relative z-index 9 &:hover .c-check-item background-color #f8f8f8 .c-item-select width 100% height 100% .c-check-item margin 0 padding 0 12px display block position relative height 36px line-height 36px &:hover background-color #f8f8f8 &.active color #598fe6 background-color #f8f8f8 .c-check-arrow color #598fe6 !important .c-check-arrow float right margin-top 10px .c-item-checkbox width 36px height 36px .c-select-box >>>.ivu-checkbox-indeterminate .ivu-checkbox-inner background-color #6fb3fb border-color #6fb3fb </style>
在一個大分類的子分類裏選擇的分類,可是切到別的大類項,雖然結果框裏有選擇的分類,可是待選的框裏仍是不能顯示子集,需求上線後,客戶反應體驗很差,因此就研究了複選框的 半選
狀態,其實改起來很簡單,只要在計算屬性的加個布爾值顯示半選,布爾值就是該分類的data
裏是否有選中的項check = true
行內文本過長,換行顯示優化
由於分類的字數沒有限制,作前端其實不能相信用戶,同時也不能相信後端返回給的數據,也不能相信產品,在產品沒有碰到過字數限制的功能時候產生的問題時,都是期待着用戶是個正常的用戶的。
文本過長有兩種方式解決:
item
行的高度不使用line-height
的參數,用padding
作上下間隔後,讓文本自動換行 (這樣的問題是,右手邊圖標的居中問題,字數太多就會加高item
項,美觀度沒那麼統一)不少前端新人都接觸Vue一年、甚至兩年多才會使用像element ui
、iview
、vant
開源的UI基礎庫,但細心的你可能發現,這些只適合參照原型圖實現html編碼,但業務的層次抽離、邏輯的複用、組件化業務層方面都沒有手把手教咱們上路。
三大流行框架的核心是快速地組件化開發,而咱們只是簡單的在路由組件頁面堆積UI庫的組件嗎,顯然這不是咱們想要的高效開發。一個項目能夠大到100多個頁面,若是不抽離組件,重複工做量不可預估,效率更是談不上了。那麼如何像做者同樣能更深層次使用Vue呢,其實element ui的開源庫,每個組件的實現其實都是很基礎的方法實現的,假如你要實現這樣的基礎庫,你就會想辦法去看源代碼,看着看着你就學會了做者的不少思想,那還會有什麼的組件實現不了了?
師傅領進門,修行靠我的,人人都是咱們的老師。不知你是否同意...
以上,歡迎拍磚~
歡迎關注個人開源倉庫
GITHUB:xiejunping (Cabber) · GitHub
微信二維碼: 掃碼添加好友,交個朋友