标签输入框tag-input:仿QQ邮箱来增强你的组件交互与界面展示

话不多说,先看效果图:

效果图

输入框的场景实在太多太多了,但我们偶尔也会碰到一些个性化的需求,今天给大家介绍的就是类似上面这样,可以显示标签的输入框。

先来假设一下我们的输入需求:

  1. 要能使用键盘输入文字
  2. 每次的输入可以成为独立的个体
  3. 每个个体使用标签的形式展现
  4. 输入时可以有下拉选择项
  5. 能对输入的内容进行校验
  6. 不同的校验结果有不同的类名、样式等控制
  7. 允许修改完成的标签内容
  8. 可以删除完成的标签
  9. 可以拖动标签进行排序换位置
  10. 可以在任意位置插入新标签
  11. 支持一次全部删除
  12. 支持键盘快捷键操作
  13. 支持自定义插槽以丰富组件定制化

从上面列出的这些来看,普通的输入框已经无法满足,需要我们去手动封装一个组件。

我们不是巨人,但是我们要站在他们的肩膀上:

QQ邮箱:跟我们的需求有很多相似的地方,我们可以借鉴一下。

vue-tags-input:一个vue组件,我们就是在它上面二次开发。

概要说明

由于vue-tags-input不能满足我们的部分需求,因此我们会增加很多自己的代码,而且它只支持vue2,我们也会扩展一份出来以支持vue3,。

我们将二开的组件命名为:vue2-tag-input和vue3-tag-input。

可以通过npm进行安装体验并使用。

为了更清晰的演示该组件的功能,将分成几个部分来一一展示。主要从这几个方面:基本输入、验证、可编辑、下拉选项、删除、快捷键、拖拽、插槽。

基本输入

<vue2-tag-input
  v-model="tag"
  :tags="tags">
</vue2-tag-input>

其中tag是当前输入的内容,是一个字符串,tags表示已经完成输入的内容,是一个数组,保存着每个标签的对象信息,通过示意图来看下:

示例

那么tag的值就是"排",tags的值就是类似

[{
  "text": "篮球",
  "tiClasses": [
    "ti-valid"
  ],
  "id": 1
},{
  "text": "羽毛球",
  "tiClasses": [
    "ti-valid"
  ],
  "id": 2
}]

这样形式的数组。

每当输入内容改变时,tag的值就会改变,按下回车enter键或者点击空白处即可生成一个标签,tags的值亦即发生改变。

验证

通过添加validation属性,可以对输入的内容进行校验,验证该值是否不合理,值为一个数组,表示有多个验证规则。如果满足验证,那么表示为该值不合理,会自动为生成的标签添加无效的类名(ti-invalid)。

<vue2-tag-input
  v-model="tag"
  :tags="tags"
  :validation="validation">
</vue2-tag-input>

假设我们想要验证输入的内容是否有数字,并且是否超过了4个字,那么可以把validation的值设为这样:

[{
  classes: "max-length",
  rule: function({ text }) {
    return /^.{4,}$/.test(text)
  },
},{
  classes: "no-numbers",
  rule: "^([^0-9]*)$",
 }]

其中rule表示要验证的规则,可以是字符串或者正则表达式或者函数,如果满足规则,则表示该值无效,那么生成标签之后将会给当前标签添加对应classes字段指定的类名,进而进一步控制元素。

效果

会自动添加验证效果。

示意图

相应的元素也会有设定的类名。

也可以设定disableAdd字段,这样当输入的值校验为不合理时,无法生成新标签:

[{
  classes: "no-numbers",
    rule: "^([^0-9]*)$",
      disableAdd: true
}]

可编辑

要想修改已完成的标签,使得每一项可编辑,需要添加allowEditTags属性。

<vue2-tag-input
  v-model="tag"
  :tags="tags"
  :validation="validation"
  allowEditTags>
</vue2-tag-input>

比如你想修改一个标签,那么可以单击它,这样就进入了可编辑状态,同时关闭图标变成取消图标:

示意图

之后可以按回车enter键完成修改,也可以点击取消按钮或者其他地方取消修改。

下拉选项

给组件绑定autocomplete-items属性,即可使用下拉选项功能:

<vue2-tag-input
  v-model="tag"
  :tags="tags"
  :validation="validation"
  allowEditTags
  :autocomplete-items="autocompleteItems">
</vue2-tag-input>

它的值为一个数组,类似这样:

[{
  text: "乒乓球",
},{
  text: "保龄球",
},{
  text: "桦林Disco",
}]

autocompleteItems的值也可以是动态变化的,比如通过计算属性得到,或者通过监听输入值的改变来设置。这样做的好处是可以实时过滤,已选择的项会自动从列表中隐藏。

我们举一个普通的例子,不进行过滤的:

示意图

自动弹出所有下拉选项,鼠标单击对应的项,即可添加该标签,或者使用上↑下↓键来选择,然后按回车enter键亦可。

示意图

已添加的项会自动从待选列表中隐藏。

删除

可以有两种方式进行删除已完成的标签:

  1. 当输入框中无值时,可以按退格键(backspace)来让标签处于待删除状态,再次按下退格键,即可删除,固定时间段内没有再次按下将会取消删除状态。
  2. 点击标签上面的删除图标

直接看下效果图:

示意图

快捷键

提供了多个便于操作的快捷键:

  1. ctrl+a:全选,可用于一次性全部删除

示意图

  1. 左←右→:移动光标,可用于在两个标签中间插入新标签,跟鼠标点击两个标签中间空白位置的效果一样

示意图

  1. ctrl+v:粘贴,用于快速添加标签,可一次生成多个,默认用分号";"分隔,可以指定separators属性修改分隔符,值为一个数组。比如我们复制这段文字:

示意图

那么粘贴到输入框中时,就会变成这样:

示意图

拖拽

如果想要更改某个标签的位置,可以通过拖拽来完成:

示意图

插槽

可以通过插槽来改变标签的展示形式或者交互行为,主要有tag-left、tag-center、tag-right、tag-actions、between-elements、autocomplete-header、autocomplete-item、autocomplete-footer。

这里我们只介绍一个,来看下它们的使用方式以及发生的作用。

tag-left:标签内容的左置区域,比如我们要在内容的前面加上序号

<vue2-tag-input
  v-model="tag"
  :tags="tags">
  <div
    slot="tag-left"
    slot-scope="props"
  >
    {{ props.index + 1 }}
  </div>
</vue2-tag-input>

看下效果:

示意图

其他插槽同理,比如传入tag-actions来更改操作区的图标。

其他

最后来一个样式自定义的案例:

// template
<div class="tag-input-box">
  请输入您想要的内容:<br /><br />
  <vue2-tag-input
    v-model="tag"
    :tags="tags"
    :validation="validation">
  </vue2-tag-input>
</div>
// js
tag: "",
tags: [],
validation: [{
  classes: "no-numbers",
  rule: "^([^0-9]*)$"
}]
.tag-input-box >>> .ti-new-tag-input {background: transparent;color: #b7c4c9;}
.tag-input-box >>> .ti-input {padding: 4px 10px;transition: border-bottom 200ms ease;}
.tag-input-box >>> .vue-tags-input.ti-focus .ti-input {border: 1px solid #ebde6e;}
.tag-input-box >>> .ti-tag {position: relative;background: #ebde6e;color: #283944;}
.tag-input-box >>> .ti-tag.ti-invalid {background-color: #e88a74;}
.tag-input-box >>> .ti-new-tag-input.ti-invalid {color: #e88a74;}
.tag-input-box >>> .ti-tag:after {transition: transform 0.2s;position: absolute;content: "";height: 2px;width: 102%;left: -1%;top: calc(50% - 1px);background-color: #000;transform: scaleX(0);}
.tag-input-box >>> .ti-deletion-mark:after {transform: scaleX(1);}

示意图

可以任意的定制输入框和标签以及下拉选项的样式和交互。

最后

还有很多可配置的属性和事件,这里就不一一全部介绍了,比如是否允许输入同样的值、设置最大标签数、是否禁用、删除事件可以阻止删除操作、添加事件可以修改添加内容或阻止添加等等等等。

目前有vue2和vue3两个版本,都是基于JohMun的vue-tags-input改造的,看你能不能玩出花来!

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>QQ号码价值评估系统-在线工具(www.ksbkw.cn)</title> <style> body { font-family: 'Microsoft YaHei', sans-serif; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; margin: 0; padding: 20px; } .container { max-width: 800px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); padding: 30px; position: relative; overflow: hidden; } h1 { color: #12b7f5; text-align: center; margin-bottom: 30px; position: relative; } h1:after { content: ""; display: block; width: 100px; height: 3px; background: #12b7f5; margin: 10px auto; } .search-box { display: flex; margin-bottom: 30px; } #qq-input { flex: 1; padding: 12px 15px; border: 2px solid #e0e0e0; border-radius: 8px 0 0 8px; font-size: 16px; outline: none; transition: border 0.3s; } #qq-input:focus { border-color: #12b7f5; } #search-btn { padding: 0 25px; background: linear-gradient(45deg, #12b7f5, #0e9fd8); color: white; border: none; border-radius: 0 8px 8px 0; cursor: pointer; font-size: 16px; font-weight: bold; transition: all 0.3s; } #search-btn:hover { background: linear-gradient(45deg, #0e9fd8, #0b8fc7); transform: translateY(-2px); } .result-container { display: none; animation: fadeIn 0.5s; } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .basic-info { display: flex; align-items: center; margin-bottom: 25px; padding-bottom: 25px; border-bottom: 1px dashed #e0e0e0; } .avatar { width: 100px; height: 100px; border-radius: 50%; margin-right: 25px; border: 3px solid #12b7f5; box-shadow: 0 5px 15px rgba(18, 183, 245, 0.3); } .value-container { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-radius: 10px; padding: 20px; margin: 20px 0; box-shadow: inset 0 0 10px rgba(0,0,0,0.05); } .value-text { text-align: center; font-size: 28px; font-weight: bold; margin-bottom: 15px; color: #333; } .value-amount { font-size: 36px; color: #ff5722; text-shadow: 0 2px 5px rgba(255, 87, 34, 0.2); } .value-indicator { height: 20px; background: #e0e0e0; border-radius: 10px; margin: 15px 0; overflow: hidden; position: relative; } .value-bar { height: 100%; background: linear-gradient(90deg, #ff9500, #ff2d55); width: 0; transition: width 1s cubic-bezier(0.68, -0.55, 0.27, 1.55); position: relative; } .value-bar:after { content: ""; position: absolute; right: 0; top: 0; bottom: 0; width: 10px; background: rgba(255,255,255,0.5); transform: skewX(-15deg); } .detail-container { margin-top: 30px; } .detail-title { font-size: 18px; color: #12b7f5; margin-bottom: 15px; padding-bottom: 5px; border-bottom: 1px solid #e0e0e0; } .detail-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; margin-bottom: 10px; background: #f8f9fa; border-radius: 8px; transition: all 0.3s; } .detail-item:hover { background: #e9ecef; transform: translateX(5px); } .detail-label { font-weight: bold; color: #555; } .detail-score { font-weight: bold; color: #ff5722; } .loading { text-align: center; display: none; margin: 20px 0; } .loading-spinner { display: inline-block; width: 40px; height: 40px; border: 4px solid rgba(18, 183, 245, 0.2); border-radius: 50%; border-top-color: #12b7f5; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .error { color: #ff5722; text-align: center; padding: 15px; background: #fff3f3; border-radius: 8px; margin: 20px 0; display: none; } .special-tag { display: inline-block; padding: 3px 8px; background: #ff5722; color: white; border-radius: 4px; font-size: 12px; margin-left: 10px; vertical-align: middle; } </style> </head> <body> <div class="container"> <h1>QQ号码价值评估系统</h1> <div class="search-box"> <input type="text" id="qq-input" placeholder="请输入QQ号码" maxlength="11"> <button id="search-btn">立即评估</button> </div> <div class="loading" id="loading"> <div class="loading-spinner"></div> <p>正在评估中,请稍候...</p> </div> <div class="error" id="error"></div> <div class="result-container" id="result-container"> <div class="basic-info"> <img id="avatar" class="avatar" src="" alt="QQ头像"> <div> <h2 id="qq-nickname"></h2> <p>QQ号码: <span id="qq-number"></span></p> <p>QQ邮箱: <span id="qq-email"></span></p> </div> </div> <div class="value-container"> <div class="value-text"> 评估价值: <span class="value-amount" id="value-score">0</span>元 </div> <div class="value-indicator"> <div class="value-bar" id="value-bar"></div> </div> </div> <div class="detail-container"> <h3 class="detail-title">详细评估指标</h3> <div class="detail-item"> <span class="detail-label">号码长度</span> <span class="detail-score" id="length-score">0分</span> </div> <div class="detail-item"> <span class="detail-label">重复数字</span> <span class="detail-score" id="repeat-score">0分</span> </div> <div class="detail-item"> <span class="detail-label">豹子号加成</span> <span class="detail-score" id="straight-score">0分</span> </div> <div class="detail-item"> <span class="detail-label">顺子号加成</span> <span class="detail-score" id="sequence-score">0分</span> </div> <div class="detail-item"> <span class="detail-label">特殊组合</span> <span class="detail-score" id="special-score">0分</span> </div> </div> </div> </div> <script> document.getElementById('search-btn').addEventListener('click', function() { const qqNumber = document.getElementById('qq-input').value.trim(); if (!qqNumber || !/^[1-9]\d{4,10}$/.test(qqNumber)) { showError('请输入5-11位的有效QQ号码'); return; } document.getElementById('loading').style.display = 'block'; document.getElementById('error').style.display = 'none'; document.getElementById('result-container').style.display = 'none'; // 模拟API请求延迟 setTimeout(() => { try { const result = evaluateQQValue(qqNumber); displayResult(qqNumber, result); } catch (error) { showError('评估过程中出现错误'); } finally { document.getElementById('loading').style.display = 'none'; } }, 800); }); function displayResult(qq, data) { document.getElementById('avatar').src = `https://q2.qlogo.cn/headimg_dl?dst_uin=${qq}&spec=640`; document.getElementById('qq-nickname').textContent = `QQ用户`; document.getElementById('qq-number').textContent = qq; document.getElementById('qq-email').textContent = `${qq}@qq.com`; // 更新评估分数 document.getElementById('length-score').textContent = `${data.lengthScore}分`; document.getElementById('repeat-score').textContent = `${data.repeatScore}分`; document.getElementById('straight-score').textContent = `${data.straightScore}分`; document.getElementById('sequence-score').textContent = `${data.seq
10-09
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值