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
微信二维码: 扫码添加好友,交个朋友