vue3+elementUI 实现悬浮多行文本输入框

悬浮多行文本输入框 ------levitateMultipleInput

文档变更记录

日期 版本 变更说明
2023年10月26日 1.0 组件文档新增

版本支持

Vue3+

一、组件功能描述:

点击常规输入框 可以调出悬浮大文本域输入框,支持多行文本输入、支持从excel复制粘贴输入并自动切分为符合参数的逗号拼接字符串。

  • 组件图例

二、组件参数说明:

  1. props (属性)
参数名 参数类型 必填 默认值 说明
inputPrefix String 申请单号: 外部输入框 提示文本
inputPlaceholder string 请输入单号 外部输入框 占位文本Placeholder
popoverWidth number 300 如果需要结合 界面适应宽度的话,配合hook useGetElementWidthByClassName(className),参数class 是对应元素基坑的类名
showTextLength number 15 基础文本框展示的字符个数
useTextProcess boolean true 开启文本处理 , true : 判断长度限制 切割 \r \n 字符串 ,false :不做处理
overLength number 10 多行文本框最大上限,默认上限为 10个
textInputLength number 10 多行文本框显示行数 上限为 10个
inputInnerPlaceholder string 每行填写一条申请单号 悬浮输入框 占位文本
modelValue string 自定义V-model 绑定变量
  1. Emits (抛出事件)
事件名 事件参数 说明
update:modelValue textValue 自定义V-model 抛出事件
  1. Expose (对外暴露事件)

外部组件需要通过 $refs 来进行调用的方法

事件名 事件参数 说明
  1. 是否支持属性透传
不支持属性与事件透传至依赖组件

三、组件代码:

vue 复制代码
<template>
  <el-popover
    :visible="state.visible"
    placement="bottom-start"
    :width="props.popoverWidth"
  >
    <div>
      <el-input
        ref="inputTextAreaRef"
        v-model="textValue"
        type="textarea"
        :placeholder="inputInnerPlaceholder"
        resize="none"
        :autosize="{ minRows: props.textInputLength + 1, maxRows: props.textInputLength + 1 }"
        clearable
        class="textInputArea"
        @blur="blurInputTextArea"
        @input="showErrorTip = false"
        @clear="showErrorTip = false"
      />
      <div class="input-textarea-bottom-btn">
        <div>
          <p v-if="showErrorTip" class="over-length-error-tip">
            超出最大行数限制{{ props.overLength }}行
          </p>
        </div>
      </div>
    </div>
    <template #reference>
      <el-input
        ref="inputRef"
        v-model="omitText"
        class="base-input"
        :placeholder="props.inputPlaceholder"
        style="cursor: pointer"
        @focus="showLevitateWindow"
      >
        <template #prefix>
          {{ props.inputPrefix }}
        </template>
      </el-input>
    </template>
  </el-popover>
</template>
<script setup lang="ts">
import { computed } from 'vue'

const props = withDefaults(
  defineProps<{
    modelValue:string
    inputPrefix?: string
    inputPlaceholder?: string
    popoverWidth?: number
    showTextLength?:number
    useTextProcess?:boolean
    overLength?:number
    textInputLength?:number
    inputInnerPlaceholder?:string
    }>(),
  {
    inputPrefix: '申请单号:', // 外部输入框 提示文本
    inputPlaceholder: '请输入单号', // 外部输入框 占位文本
    inputInnerPlaceholder: '每行填写一条申请单号', // 悬浮输入框 占位文本
    popoverWidth: 300, // 如果需要结合 界面适应宽度的话,配合hook useGetElementWidthByClassName(className),参数class 是对应元素基坑的类名
    showTextLength: 15, // 基础文本框展示的字符个数
    overLength: 10, // 多行文本框最大上限,默认上限为 10个
    textInputLength: 10, // 多行文本框显示行数 上限为 10个
    useTextProcess: true // 开启文本处理 , 判断长度限制 切割 \r \n 字符串
  }
)

const emits = defineEmits(['update:modelValue'])

const inputTextAreaRef = ref<HTMLElement | null> (null)
const inputRef = ref<HTMLElement | null> (null)
const showErrorTip = ref<boolean> (false)
const state = reactive({
  visible: false
})

const omitText = computed(() => {
  return textValue.value.length > props.showTextLength ? textValue.value.substring(0, props.showTextLength) + '......' : textValue.value
})

const textValue = ref(props.modelValue)
watchEffect(() => {
  textValue.value = props.modelValue
})

// 聚焦的时候 展示悬浮输入框
function showLevitateWindow () {
  if (state.visible) {
    inputTextAreaRef.value?.focus()
    return
  }
  // 处理数据,如果包含  , 需要拆开
  const localValue = textValue.value
  if (localValue.indexOf(',') > -1) {
    textValue.value = localValue.split(',').join('\n')
    emits('update:modelValue', textValue.value)
  }
  state.visible = true
  nextTick(() => {
    inputTextAreaRef.value?.focus()
  })
}

// 悬浮输入失去焦点 传输数据给父组件
function blurInputTextArea () {
  if (props.useTextProcess) {
    const { overLength, val } = textProcessing(textValue.value)
    // textValue.value = val
    if (!overLength) { // 没有超长的 传递给父组件
      console.log('emit的数据', val)
      emits('update:modelValue', val)
      state.visible = false
    } else {
      showErrorTip.value = true // 展示错误信息
    }
  } else {
    emits('update:modelValue', textValue.value)
    state.visible = false
  }
}
// 文本处理方法,切割 \r \n 字符串
const textProcessing : (val: string) => { val:string, overLength:boolean } = (val) => {
  const splitText = val.split(/\r?\n/).filter(i => i !== '')
  const overLength = splitText.length > props.overLength // 最大长度
  return {
    val: splitText.join(','),
    overLength
  }
}
</script>
<style scoped lang="scss">
.input-textarea-bottom-btn{
  margin-top: 5px;
  display: flex;
  justify-content: space-between;
  align-content: center;
  .over-length-error-tip{
    color:#f56c6c;
    font-size: 12px;
    line-height: 24px;
  }
}
/* 隐藏浏览器默认滚动条 */
.textInputArea ::-webkit-scrollbar {
  width: 6px; /* 宽度 */
  height: 6px; /* 高度 */
}

/* 滚动条滑块 */
.textInputArea ::-webkit-scrollbar-thumb {
  background: #969696; /* 滑块颜色 */
  border-radius: 3px; /* 滑块圆角 */
}

/* 滚动条轨道 */
.textInputArea ::-webkit-scrollbar-track {
  background: #f0f0f0; /* 轨道颜色 */
  border-radius: 3px; /* 轨道圆角 */
}

/* 鼠标悬停在滚动条上时的滑块样式 */
.textInputArea ::-webkit-scrollbar-thumb:hover {
  background: #656565;
}
</style>

附带 Hook 函数

js 复制代码
import { ref, onMounted, onBeforeUnmount } from 'vue'
/**
 * @description 根据className 获取悬浮输入框的基础输入框宽度
 * @param className - 基础元素类名 string,默认 levitateBaseInput
 * @param paddingWidth - 边距宽度 number,默认 12
 * */
export default function useGetElementWidthByClassName (className = 'levitateBaseInput', paddingWidth = 12) {
  const elementWidth = ref(0)
  function getElementsClientWidth () {
    setTimeout(() => {
      const ele = document.getElementsByClassName(className)
      if (ele[0]) {
        elementWidth.value = ele[0].clientWidth - paddingWidth
      } else elementWidth.value = 0
    }, 400)
  }
  onMounted(() => {
    window.addEventListener('resize', getElementsClientWidth)
    getElementsClientWidth()
  })
  onBeforeUnmount(() => {
    window.removeEventListener('resize', getElementsClientWidth)
  })
  return elementWidth
}

四、组件使用例子:

js 复制代码
<HomeSearchItem ref="inputElementRef" prop="MultiAccounts" class="levitateBaseInput">
  <levitate-multiple-input
    v-model="queryParams.MultiAccounts"
    :popover-width="levitateWidth"
    input-prefix="多账户查询:"
    input-placeholder=""
    input-inner-placeholder="每行输入一个账户,最多20行,Meta账户可不加act_前缀"
    :over-length="20"
  />
</HomeSearchItem>

const queryParams = reactive({
    MultiAccounts:''
})
const levitateWidth = useGetElementWidthByClassName()

五、其他注意事项:

1、本组件主要是用于解决从Excel 复制粘贴多行文本,并需要单行截取的问题。

2、props中的popoverWidth是用于设置弹出层的宽度,在搭配外层formItem使用时候,可以使用配套hook函数useGetElementWidthByClassName(默认获取类名为levitateBaseInput的元素的宽度,可以通过参数修改)获取外层宽度使弹出层宽度匹配外层输入框,否则使用固定宽度300。

3、组件设计:在点击外层输入框的时候,当悬浮框没有展开的时候,则显示悬浮框并自动focus到大文本域输入框(如果v-model的数据包含 , 则需要进行拆分并加入\n 确保可以在文本域输入框正常分行展示),当悬浮框是展开的状态则默认focus文本域输入框防止失去焦点(再次点击外层小输入框的时候,防止丢失焦点)。当文本域失去焦点则进行文本处理,见第四点。

js 复制代码
/*
 <el-popover
  :visible="state.visible">
  .....................省略其他代码.....................
  </el-popover>
*/  
// 聚焦的时候 展示悬浮输入框
function showLevitateWindow () {
  if (state.visible) {
    inputTextAreaRef.value?.focus()
    return
  }
  // 处理数据,如果包含  , 需要拆开, localValue  为v-model传入数据的本地副本
  const localValue = textValue.value
  if (localValue.indexOf(',') > -1) {
    textValue.value = localValue.split(',').join('\n')
    emits('update:modelValue', textValue.value)
  }
  state.visible = true
  // 在悬浮输入框展示后立刻聚焦
  nextTick(() => {
    inputTextAreaRef.value?.focus()
  })
}

4、有关文本处理的方法,props中的useTextProcess是文本处理配置,如果设置为true,即对输入的多行文本进行文本处理 切割 \r \n 字符串并返回切割后拼接 , 后的字符串;如果是false则不对文本处理。会在悬浮框失去焦点的时候触发blurInputTextArea 方法进而在useTextProcess为true的情况下调用textProcessing 方法进行处理。当前版本在false的情况下不处理超长情况。

在true的情况如果文本超出行数限制,则会将showErrorTip.value = true // 展示错误信息,

js 复制代码
// 悬浮输入失去焦点 传输数据给父组件。
function blurInputTextArea () {
  if (props.useTextProcess) {
    const { overLength, val } = textProcessing(textValue.value)
    // textValue.value = val
    if (!overLength) { // 没有超长的 传递给父组件
      console.log('emit的数据', val)
      emits('update:modelValue', val)
      state.visible = false
    } else {
      showErrorTip.value = true // 展示错误信息
    }
  } else {
    emits('update:modelValue', textValue.value)
    state.visible = false
  }
}
// 文本处理方法,切割 \r \n 字符串
const textProcessing : (val: string) => { val:string, overLength:boolean } = (val) => {
  const splitText = val.split(/\r?\n/).filter(i => i !== '')
  const overLength = splitText.length > props.overLength // 最大长度
  return {
    val: splitText.join(','),
    overLength
  }
}
相关推荐
吕彬-前端21 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱23 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai32 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb