背景
基于业务需求,产品提出输入交互,element-ui本身不能支持。完整交互如下视频。
IMG
大致描述:输入框可输入多条数据,超过一条,则以数量的 +n的形式显示,用户可以在输入框中输入,也可以点击右边的编辑小手,在展开的textarea中输入数据。数据以回车,中文逗号,英文逗号分隔(后续传给服务端时需要组成数组的形式。)
开发基本思路:
由于我们项目系统使用的elementui框架,所以为了保持风格统一,使用el-input作为载体,上面的输入框使用默认 type='text',下方的输入框使用 type='textarea'。
其中缩写的小方块用div元素包装,通过绝对定位 使其置于input输入框之上。
下面是html结构
- <div class="input-wrap" @click.stop>
- <el-input
- size="mini"
- id="tag-input"
- v-model="inputVal"
- :class="customClass"
- class="y-w-160"
- :placeholder="placeholder">
- el-input>
- <i class="el-icon-edit" @click="showTextArea = !showTextArea">i>
- <div class="tagcut" v-if="contentArr.length > 0">
- <span class="y-c-p el-tag el-tag--info el-tag--mini el-tag--light f-tag" @click="showTextArea = true">
- <span class="el-select__tags-text">{{ contentArr[0] }}span>
- <i class="y-bk-C0C4CC el-tag__close el-icon-close" @click="delContent">i>
- span>
- <span class="y-c-p el-tag el-tag--info el-tag--mini el-tag--light" @click="showTextArea = true" v-if="contentArr.length > 1">
- <span class="el-select__tags-text">+ {{ contentArr.length - 1 }}span>
- span>
- div>
- <transition name="text-area">
- <div class="textareabox" v-if="showTextArea">
- <el-input
- placeholder="请输入单号、最多20个,请用,或者换行隔开"
- class="t-setting"
- :rows="8"
- size="mini"
- resize="none"
- v-model="localText"
- type="textarea">
- el-input>
- div>
- transition>
- div>
有几个关键点这里提一下:
1. 当输入数据时,输入框中会生成tag(div包裹的内容),覆盖在input框之上,这个时候需要处理一下input框的光标位置,即padding-left的值,以免被tag框遮挡。
处理方式:监听textarea中的内容变化,当tag框有值时,获取tag框的宽度,动态设置input框的padding-left。代码如下
- watch: {
- localText(val) {
- // 处理输入的单号
- this.contentArr = val.replace(/\n/g, ',').replace(/,/g, ',').split(',').filter(item => item.trim());
- if(this.contentArr.length > this.maxLength) {
- this.$message.error('输入不能超过' + this.maxLength + '条');
- // this.contentArr中有重复数据
- let idx = 0; // 标记 this.contentArr[this.maxLength - 1] 这条数据的位置
- let maxLengthArr = this.contentArr.slice(0, this.maxLength);
- maxLengthArr.forEach((item, index) => {
- if(item === maxLengthArr[this.maxLength - 1]) {
- idx++;
- }
- })
- // 查找字符串中字符的位置做截取
- let indexOfNum = getIndexofNum(val, maxLengthArr[this.maxLength - 1], idx - 1);
- this.localText = val.slice(0, indexOfNum + maxLengthArr[this.maxLength - 1].length);
- }
- this.$nextTick(() => {
- let tagcutref = document.getElementsByClassName('tagcut');
- if(tagcutref[0]) {
- document.getElementById('tag-input').style.paddingLeft = tagcutref[0].offsetWidth + 2 + 'px';
- } else {
- document.getElementById('tag-input').style.paddingLeft = '10px';
- }
- })
- this.$emit('change', this.localText);
- }
- }
2. 该输入框中的内容通过v-model双向绑定。关键代码
- export default {
- props: {
- textareaContent: {
- type: String,
- required: true
- },
- },
- model: {
- prop: 'textareaContent',
- event: 'change', // 通过$emti('change'),修改textareaContent的值
- },
- watch: {
- inputVal(val) {
- // 监听 , ,
- if(val.indexOf(',') !== -1 || val.indexOf(',') !== -1) {
- let inputValArr = val.replace(/,/g, ',').split(',').filter(item => item.trim());
- inputValArr.forEach(item => {
- if(this.localText) {
- // 如果有值
- this.localText = this.localText + '\n' + item;
- } else {
- // 第一条数据不需要换行
- this.localText = this.localText + item;
- }
- this.$emit('change', this.localText); // 双向绑定输入框中的内容
- })
- this.inputVal = ''; // 输入框数据处理之后 置空
- }
- },
- }
- }
3. 在input框通过粘贴输入内容,复制的内容如果有换行符(\n),则会被转换成空格,不符合我们规定的数据输入格式(换行,中文逗号,英文逗号)。所以这里的处理方式是将input框的粘贴功能禁掉,然后监听粘贴的内容,对粘贴的内容进行处理,直接转换成对应tag框的形式展示在input框中。在textarea框中,则展示粘贴内容的原文本。
- <el-input
- size="mini"
- @blur="handleEnter"
- id="tag-input"
- v-model="inputVal"
- @paste.native.capture.prevent="handlePaste"
- :class="customClass"
- class="y-w-160"
- :placeholder="placeholder">
- el-input>
处理粘贴内容的时候有个比较麻烦的问题:如果粘贴的内容超过输入限制(这里默认是20条),则需要截取,截取之后需要保证复制的文本的格式(可能有中文逗号,英文逗号,换行符)。截取方法:找到第20条数据的下标位置。注意数据可能会有重复。
- // contentArr是数据格式化之后的数组,maxLength是最大可输入的数据长度,默认20。
- if(this.contentArr.length > this.maxLength) {
- this.$message.error('输入不能超过' + this.maxLength + '条');
- // this.contentArr中可能有重复数据
- let idx = 0; // 标记 this.contentArr[this.maxLength - 1] 这条数据在this.contentArr数组中是第几条数据
- let maxLengthArr = this.contentArr.slice(0, this.maxLength);
- maxLengthArr.forEach((item, index) => {
- if(item === maxLengthArr[this.maxLength - 1]) {
- idx++; // 如果有重复,标记是第几条重复数据,便于查找下标。
- }
- })
- // 查找字符串中字符的位置做截取
- let indexOfNum = getIndexofNum(val, maxLengthArr[this.maxLength - 1], idx);
- this.localText = val.slice(0, indexOfNum + maxLengthArr[this.maxLength - 1].length);
- }
-
-
- function getIndexofNum(str, cha, num) {
- let strn = ' ' + str + ' ';
- let x = -1;
- let i = 0;
- while(i < num) {
- x = strn.indexOf(cha, x + 1);
- if((/\n|\,|\,|\s/).test(strn[x - 1]) && (/\n|\,|\,|\s/).test(strn[x + 1])) {
- // 判断字符前面和字符后面是否有 (空格|,|,|换行符),前后都有,表示该字符完全匹配
- i++;
- }
- }
- return x;
- }
下面是完整代码:
- <template>
- <div class="input-wrap" @click.stop>
- <el-input
- size="mini"
- @blur="handleEnter"
- id="tag-input"
- @keyup.enter.native="handleEnter"
- v-model="inputVal"
- @paste.native.capture.prevent="handlePaste"
- :class="customClass"
- class="y-w-160"
- :placeholder="placeholder">
- el-input>
- <i class="el-icon-edit" @click="showTextArea = !showTextArea">i>
- <div class="tagcut" v-if="contentArr.length > 0">
- <span class="y-c-p el-tag el-tag--info el-tag--mini el-tag--light f-tag" @click="showTextArea = true">
- <span class="el-select__tags-text">{{ contentArr[0] }}span>
- <i class="y-bk-C0C4CC el-tag__close el-icon-close" @click="delContent">i>
- span>
- <span class="y-c-p el-tag el-tag--info el-tag--mini el-tag--light" @click="showTextArea = true" v-if="contentArr.length > 1">
- <span class="el-select__tags-text">+ {{ contentArr.length - 1 }}span>
- span>
- div>
- <transition name="text-area">
- <div class="textareabox" v-if="showTextArea">
- <el-input
- placeholder="请输入单号、最多20个,请用,或者换行隔开"
- class="t-setting"
- :rows="8"
- size="mini"
- resize="none"
- v-model="localText"
- type="textarea">
- el-input>
- div>
- transition>
- div>
- template>
- export default {
- props: {
- placeholder: {
- type: String,
- default: '运单号'
- },
- textareaContent: {
- type: String,
- required: true
- },
- // 最大可输入的单号数量
- maxLength: {
- type: Number,
- default: 20
- },
- customClass: {
- type: String
- }
- },
- model: {
- prop: 'textareaContent',
- event: 'change'
- },
- data() {
- return {
- contentArr: [],
- tagdetail: '',
- inputVal: '',
- showTextArea: false,
- localText: this.textareaContent
- }
- },
- methods: {
- getIndexofNum(str, cha, num) {
- let strn = ' ' + str + ' ';
- let x = -1;
- let i = 0;
- while(i < num) {
- x = strn.indexOf(cha, x + 1);
- if((/\n|\,|\,|\s/).test(strn[x - 1]) && (/\n|\,|\,|\s/).test(strn[x + 1])) {
- // 判断字符前面和字符后面是否有 (空格|,|,|换行符),前后都有,表示该字符完全匹配
- i++;
- }
- }
- return x;
- },
- delContent() {
- this.localText = this.textareaContent.slice(this.contentArr[0].length + 1);
- this.$emit('change', this.localText);
- },
- handleEnter() {
- // 输入框没有值 则不操作
- if(!this.inputVal.trim()) return;
- if(this.localText) {
- // 如果有值
- this.localText = this.localText + '\n' + this.inputVal;
- } else {
- // 第一条数据不需要换行
- this.localText = this.localText + this.inputVal;
- }
- this.$emit('change', this.localText);
- this.inputVal = '';
- },
- handlePaste(e) {
- if(e.clipboardData.getData('Text')) {
- if(this.localText) {
- // 如果有值
- this.localText = this.localText + '\n' + e.clipboardData.getData('Text');
- } else {
- // 第一条数据不需要换行
- this.localText = e.clipboardData.getData('Text');
- }
- }
- }
- },
- watch: {
- textareaContent(val) {
- // 这里设置,确保在父级页面重置textareaContent为空,localText的值可以响应
- this.localText = val;
- },
- localText(val) {
- // 处理输入的单号
- this.contentArr = val.replace(/\n/g, ',').replace(/,/g, ',').split(',').filter(item => item.trim());
- if(this.contentArr.length > this.maxLength) {
- this.$message.error('输入不能超过' + this.maxLength + '条');
- // this.contentArr中可能有重复数据
- let idx = 0; // 标记 this.contentArr[this.maxLength - 1] 这条数据在this.contentArr数组中是第几条数据
- let maxLengthArr = this.contentArr.slice(0, this.maxLength);
- maxLengthArr.forEach((item, index) => {
- if(item === maxLengthArr[this.maxLength - 1]) {
- idx++; // 如果有重复,标记是第几条重复数据,便于查找下标。
- }
- })
- // 查找字符串中字符的位置做截取
- let indexOfNum = this.getIndexofNum(val, maxLengthArr[this.maxLength - 1], idx);
- this.localText = val.slice(0, indexOfNum + maxLengthArr[this.maxLength - 1].length);
- }
- this.$nextTick(() => {
- let tagcutref = document.getElementsByClassName('tagcut');
- if(tagcutref[0]) {
- document.getElementById('tag-input').style.paddingLeft = tagcutref[0].offsetWidth + 2 + 'px';
- } else {
- document.getElementById('tag-input').style.paddingLeft = '10px';
- }
- })
- this.$emit('change', this.localText);
- },
- inputVal(val) {
- // 监听 , ,
- if(val.indexOf(',') !== -1 || val.indexOf(',') !== -1) {
- let inputValArr = val.replace(/,/g, ',').split(',').filter(item => item.trim());
- inputValArr.forEach(item => {
- if(this.localText) {
- // 如果有值
- this.localText = this.localText + '\n' + item;
- } else {
- // 第一条数据不需要换行
- this.localText = this.localText + item;
- }
- this.$emit('change', this.localText);
- })
- this.inputVal = ''; // 输入框数据处理之后 置空
- }
- },
- showTextArea(val) {
- if(val) {
- document.body.addEventListener('click', () => {
- this.showTextArea = false;
- })
- } else {
- document.body.removeEventListener('click', () => {});
- }
- },
- }
- }
- .input-wrap {
- display: inline-block;
- position: relative;
- .tagcut {
- position: absolute;
- left: 2px;
- bottom: 1px;
- }
- .textareabox {
- position: absolute;
- z-index: 1;
- .t-setting {
- width: 160px;
- }
- /deep/ .el-textarea__inner {
- border: 0;
- box-shadow: 0px 0px 5px 0px rgba(173,173,173,0.5);
- padding: 5px 10px;
- &::-webkit-scrollbar {
- width: 4px;
- }
- &::-webkit-scrollbar-thumb {
- border-radius: 2px;
- -webkit-box-shadow: inset 0 0 2px rgba(144,147,153,0.3);
- background-color:rgba(144,147,153,0.3);
- }
- }
- }
- }
- .text-area-enter-active{
- transition: opacity .5s;
- }
- .text-area-enter{
- opacity: 0;
- }
- .text-area-leave-active{
- transition: opacity .5s;
- }
- .text-area-leave-to{
- opacity: 0;
- }
- .el-icon-edit {
- position: absolute;
- right: 5px;
- color: #4c84ff;
- top: calc(50% - 7px);
- cursor: pointer;
- }
- .f-tag {
- .el-select__tags-text {
- max-width: 55px;
- display: inline-block;
- vertical-align: middle;
- }
- }
- .y-w-160 {
- width: 160px;
- }
- .y-c-p {
- cursor: pointer;
- }
- .y-bk-C0C4CC {
- background-color: #c0c4cc;
- }
README.md
- # 运单号/订单号输入组件 -- 使用方法 -- 参数&事件说明
- ## @param v-model 双向绑定 输入内容
- * 说明:输入框中的内容
- * 是否必传: 是
- * 值类型:String
- ## @param placeholder
- * 说明:输入框的placeholder
- * 是否必传:否
- * 默认值:运单号
- * 值类型:String
- ## @params customClass
- * 说明:自定义的className
- * 是否必传:否
- * 默认值:'''
- * 值类型:String
- ## @example
- ```html
- customClass="y-w-300"
- v-model="waybillOrder"
- placeholder="订单号 " />
功能不复杂, 主要在处理粘贴数据大于数据限制需要截取的时候, 当时思考的还算比较多。记录一下。