概要
开发任务系统中,业务需求:需要在任务描述、评论等地方支持@人员功能,可以将任务外部人员添加至当前任务中。
功能:1、支持输入@展开下拉,可通过鼠标点击或键盘上下移动 + 回车选中人员;
2、支持@后继续输入以搜索人员列表;
3、输入框@内容支持复制粘贴;
4、添加输入框最大输入限制;
整体架构流程
因时间紧迫,虽然看到了el-input支持Autocomplete属性,但是没空去研究它了,还是用div + contenteditable="true"开撸吧;
- 使用div(ref[customInput]),添加contenteditable="true"可编辑状态,得到一个自定义输入框;
- 使用el-popover,用于展示@人员列表;
- 监听customInput 的input事件,当输入为@时,保存当前输入框光标位置,打开el-popover;
- 打开el-popover后,继续监听customInput 的input事件,使用diff.diffChars对比输入差异,拿到差异输入进行@人员列表的搜索;
- 选中人时,为了方便,我用的方案是:拿到人员数据,生成一个span标签(给span添加contenteditable="false",不然插入的人员块也能被编辑),将人员的信息绑定在span标签上,我绑定的为data-id(工号)、data-realname(姓名);span标签创建好后,根据步骤3保存的光标位置,将span插入至customInput中,同时后移光标至插入的元素后;
- 为了实现复制粘贴功能,需要对customInput的复制粘贴事件(copy、paste)进行自定义;
- 组件内写了格式化数据的方法,通过changeChosen将处理好的@人员数组数据传递给父组件;
如业务需求包含重新编辑功能(如表单提交后需要二次编辑或修改),需要告知后端需额外保存一个存html代码的字段,因为当前组件的输入框内容,因为@人员块的缘故,只能通过html保持原本的正确格式,否则就是纯文本,通俗易懂就是两个字段分别存储customInput的textContent和innerHTML字段;编辑时只需用到保存的innerHTML字段,其余的textContent字段以及处理好的@人员数组,对于我们编辑而言,是没有用的。
技术细节
- 在监听input事件的同时,需要同时监听keydown,用来限制用户的输入;
- 插入span以及处理自定义粘贴事件时,需要注意光标位置;
- 还有el-popover的弹出位置,我这里是针对我们的业务需求进行的调整,有具体需要可以自己手动调整
小结
代码看起来很简单,但因为其中一些知识之前没有涉足过,一个小问题就耽误很久,三天的艰苦奋斗,中间每次觉得完美了,就会有莫名其妙的问题跳出来,暂时就这样了,目前用起来还行,就是用户体验方面需要稍微优化一下。(对了,还写了一个小程序的@组件,小程序是uniapp写的,有需要滴滴我),下边贴代码了,懒得整理了,复制粘贴拿走用吧。
如发现什么问题,或者有大哥给优化建议的,欢迎滴滴。
自定义输入框代码
<template>
<div class="custom-at-box">
<div class="custom-textarea-box">
<div
:class="[
'custom-textarea custom-scroll',
{ 'show-word-limit': showWordLimit && maxlength },
{ 'custom-textarea-disabled': disabled },
]"
:style="{ height: height }"
ref="customInput"
:contenteditable="!disabled"
:placeholder="placeholder"
@input="onInput($event)"
@keydown="onKeyDownInput($event)"
@paste="onPaste($event)"
@copy="onCopy($event)"
@click="showList(false)"
></div>
<div class="custom-at-limit" v-if="showWordLimit && maxlength">
{{ inputValueLen }}/{{ maxlength }}
</div>
</div>
<div :key="`customInput${taskPanelIsInFullScreen}`">
<el-popover
v-model="showPopover"
trigger="click"
class="custom-select-box"
ref="popoverRef"
:append-to-body="taskPanelPopoverAppendToBody"
@hide="hidePoppver"
:style="{ top: popoverOffset + 'px' }"
>
<div
class="custom-select-content custom-scroll"
ref="customSelectContent"
>
<div class="custom-select-empty load" v-if="searchOperatorLoad">
<i class="el-icon-loading"></i>
<span>加载中</span>
</div>
<div
class="custom-select-empty"
v-else-if="searchOperatorList.length === 0"
>
没有查询到该用户
</div>
<div
v-else
:class="[
'custom-select-item',
{ hoverItem: selectedIndex === index },
]"
v-for="(item, index) in searchOperatorList"
:key="item.employeeNo"
@click="handleClickOperatorItem(item)"
>
<div class="custom-select-item-content">
{{ item.realname }}({{ item.employeeNo }})
</div>
</div>
</div>
</el-popover>
</div>
</div>
</template>
<script>
const diff = require("diff");
import {
queryEmployeeByParam,
addRemark,
} from "@/api/recruitmentSystem/childUtils.js";
export default {
props: {
// 输入框placeholder
placeholder: {
type: String,
default: "请输入...",
},
// 是否显示输入字数统计
showWordLimit: {
type: Boolean,
default: true,
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
// 最大输入长度
maxlength: {
type: [Number, String],
default: "300",
},
// 输入框高度
height: {
type: String,
default: "100px",
},
setRefresh: {
type: Object,
default: () => {},
},
// 输入框输入的内容
value: {
type: String,
default: "",
},
},
data() {
return {
// 已输入内容的长度
inputValueLen: 0,
top: "",
left: "",
message: "",
startOffset: 0,
// @搜索人dom
searSpan: null,
// 筛选人数据
searchOperatorList: [],
// 筛选人数据加载状态
searchOperatorLoad: false,
// @插入位置
selectionIndex: 0,
// 当前编辑的dom
dom: null,
// 当前编辑dom的index
domIndex: 0,
// 当前编辑dom的childNodes的index
childDomIndex: 0,
// 编辑前dom内容
beforeDomVal: "",
// 筛选人选择框
showPopover: false,
// 筛选人选择框偏移量
popoverOffset: 0,
listInput: false,
listInputValue: "",
// 防抖
timer: null,
// 保存弹窗加载状态
addDataLoad: false,
// 鼠标选择人的索引
selectedIndex: 0,
};
},
mounted() {
this.setNativeInputValue();
},
computed: {
// 计算属性,用于同步父组件的数据
model: {
get() {
return this.value;
},
set(newValue) {
this.$emit("input", newValue);
if (this.$refs.customInput) {
this.$emit("inputText", this.$refs.customInput.textContent);
}
const nodeList = this.$refs.customInput.childNodes;
let list = [];
nodeList.forEach((e) => {
if(e.childNodes) {
e.childNodes.forEach(i => {
if (i.className === "active-text") {
list.push({
jobNumber: i.getAttribute("data-id"),
name: i.textContent.replace(/@/g, "").replace(/\s/g, ""),
});
}
})
}
if (e.className === "active-text") {
list.push({
jobNumber: e.getAttribute("data-id"),
name: e.textContent.replace(/@/g, "").replace(/\s/g, ""),
});
}
});
this.$emit("changeChosen", list);
},
},
taskPanelIsInFullScreen() {
return this.$store.getters.taskPanelIsInFullScreen;
},
taskPanelPopoverAppendToBody() {
return this.$store.getters.taskPanelPopoverAppendToBody;
},
},
methods: {
// 设置输入框的值
setNativeInputValue() {
if (this.$refs.customInput) {
if (this.value === this.$refs.customInput.innerHTML) return;
this.$refs.customInput.innerHTML = this.value;
this.inputValueLen = this.$refs.customInput.innerText.length;
}
},
// 筛选人弹窗数据选择
handleClickOperatorItem(item) {
this.addData(JSON.parse(JSON.stringify(item)));
this.$refs.customSelectContent.scrollTop = 0;
this.selectedIndex = 0;
this.showPopover = false;
this.listInput = false;
this.listInputValue = "";
},
// 艾特人弹窗关闭
hidePoppver() {
this.$refs.customSelectContent.scrollTop = 0;
this.selectedIndex = 0;
this.showPopover = false;
this.listInput = false;
this.listInputValue = "";
},
// 创建艾特需要插入的元素
createAtDom(item) {
// 先判断剩余输入长度是否能够完整插入元素
const dom = document.createElement("span");
dom.classList.add("active-text");
// 这里的contenteditable属性设置为false,删除时可以整块删除
dom.setAttribute("contenteditable", "false");
// 将id存储在dom元素的标签上,便于后续数据处理
dom.setAttribute("data-id", item.employeeNo);
dom.innerHTML = `@${item.realname} `;
return dom;
},
// 插入元素
addData(item) {
const spanElement = this.createAtDom(item);
const maxlength = Number(this.maxlength) || 300;
// 因为插入后需要删除之前输入的@,所以判断长度时需要减去这个1
if (maxlength - this.inputValueLen < spanElement.innerText.length - 1) {
this.$message("剩余字数不足");
return;
}
this.$refs.customInput.focus();
// 获取当前光标位置的范围
const selection = window.getSelection();
const range = selection.getRangeAt(0);
// 找到要插入的节点
const nodes = Array.from(this.$refs.customInput.childNodes);
let insertNode = "";
// 是否是子元素
let domIsCustomInputChild = true;
if (nodes[this.domIndex].nodeType === Node.TEXT_NODE) {
insertNode = nodes[this.domIndex];
} else {
const childNodeList = nodes[this.domIndex].childNodes;
insertNode = childNodeList[this.childDomIndex];
domIsCustomInputChild = false;
}
// 如果前一个节点是空的文本节点,@用户无法删除
// 添加判断条件:如果前一个节点是空的文本节点,则插入一个空的<span>节点
const html = insertNode.textContent;
// 左边的节点
const textLeft = document.createTextNode(
html.substring(0, this.selectionIndex - 1) + ""
);
const emptySpan = document.createElement("span");
// 如果找到了要插入的节点,则在其前面插入新节点
if (insertNode) {
if (!textLeft.textContent) {
if (domIsCustomInputChild) {
this.$refs.customInput.insertBefore(emptySpan, insertNode);
} else {
nodes[this.domIndex].insertBefore(emptySpan, insertNode);
}
}
insertNode.parentNode.insertBefore(spanElement, insertNode.nextSibling);
// 删除多余的@以及搜索条件
const textContent = insertNode.textContent.slice(
0,
-(1 + this.listInputValue.length)
);
if (!textContent && insertNode.nodeName === "#text") {
insertNode.remove();
} else {
insertNode.textContent = textContent;
}
} else {
// 如果未找到要插入的节点,则将新节点直接追加到末尾
this.$refs.customInput.appendChild(spanElement);
}
// 将光标移动到 span 元素之后
const nextNode = spanElement.nextSibling;
range.setStart(
nextNode || spanElement.parentNode,
nextNode ? 0 : spanElement.parentNode.childNodes.length
);
range.setEnd(
nextNode || spanElement.parentNode,
nextNode ? 0 : spanElement.parentNode.childNodes.length
);
selection.removeAllRanges();
selection.addRange(range);
this.model = this.$refs.customInput.innerHTML;
this.inputValueLen = this.$refs.customInput.innerText.length;
this.showList(false);
},
// 检查是否发生了全选操作
isSelectAll() {
const selection = window.getSelection();
return selection.toString() === this.$refs.customInput.innerText;
},
// 获取输入框是否选中文字
isSelect() {
try {
const selection = window.getSelection();
return selection.toString().length;
} catch (error) {
return 0;
}
},
// 输入事件
onKeyDownInput(event) {
// 获取当前输入框的长度
let currentLength = this.$refs.customInput.innerText.length;
// 获取最大输入长度限制
let maxLength = Number(this.maxlength) || 300;
// 如果按下的键是非控制键并且当前长度已经达到了最大长度限制
if (currentLength >= maxLength) {
// 获取按键的 keyCode
var keyCode = event.keyCode || event.which;
// 检查是否按下了 Ctrl 键
var ctrlKey = event.ctrlKey || event.metaKey; // metaKey 用于 macOS 上的 Command 键
// 允许的按键:Backspace(8)、Delete(46)、方向键和
var allowedKeys = [8, 46, 37, 38, 39, 40];
// 允许的按键 Ctrl+A、Ctrl+C、Ctrl+V
let allowedCtrlKey = [65, 67, 86]
// 检查按键是否在允许列表中并且没有执行选中操作
if (!allowedKeys.includes(keyCode) && !this.isSelect()) {
if((allowedCtrlKey.includes(keyCode) && ctrlKey)) {
return;
}
// 阻止默认行为
event.preventDefault();
return false;
}
}
if (this.showPopover) {
let listElement = this.$refs.customSelectContent;
let itemHeight = listElement.children[0].clientHeight;
if (event.key === "ArrowDown") {
// 防止光标移动
event.preventDefault();
// 移动选中索引
if (this.selectedIndex === this.searchOperatorList.length - 1) {
this.selectedIndex = 0; // 跳转到第一项
listElement.scrollTop = 0; // 滚动到列表顶部
} else {
this.selectedIndex++;
let itemBottom = (this.selectedIndex + 1) * itemHeight;
let scrollBottom = listElement.scrollTop + listElement.clientHeight;
if (itemBottom > scrollBottom) {
listElement.scrollTop += itemHeight;
}
}
} else if (event.key === "ArrowUp") {
event.preventDefault();
if (this.selectedIndex === 0) {
this.selectedIndex = this.searchOperatorList.length - 1; // 跳转到最后一项
listElement.scrollTop = listElement.scrollHeight; // 滚动到列表底部
} else {
this.selectedIndex--;
let itemTop = this.selectedIndex * itemHeight;
if (itemTop < listElement.scrollTop) {
listElement.scrollTop -= itemHeight;
}
}
} else if (event.key === "Enter") {
event.preventDefault();
if (!this.searchOperatorLoad) {
this.handleClickOperatorItem(
this.searchOperatorList[this.selectedIndex]
);
}
}
} else if (event.key === "Backspace" && this.isSelectAll()) {
// 如果执行了全选操作并删除,清空输入框内容
this.$refs.customInput.innerText = "";
this.model = this.$refs.customInput.innerHTML;
this.inputValueLen = 0;
}
},
// 监听输入事件
onInput(e) {
this.inputValueLen = this.$refs.customInput.innerText.length;
if (
["<div><br></div>", "<br>", "<span></span><br>"].includes(
this.$refs.customInput.innerHTML
)
) {
this.$refs.customInput.innerHTML = "";
this.inputValueLen = 0;
} else if (e.data === "@") {
// 保存焦点位置
this.saveIndex();
this.showList();
this.listInput = true;
} else if (this.showPopover) {
const diffResult = diff.diffChars(
this.beforeDomVal,
this.dom.textContent
);
let result = "";
// 遍历差异信息数组
for (let i = 0; i < diffResult.length; i++) {
const change = diffResult[i];
// 如果当前差异是添加或修改类型,则将其添加到结果字符串中
if (change.added) {
result += change.value;
} else if (change.removed && change.value === "@") {
this.showList(false);
this.listInputValue = "";
}
}
if (this.timer) {
clearTimeout(this.timer);
}
this.listInputValue = result;
this.timer = setTimeout(() => {
this.remoteMethod();
}, 300);
}
this.model = this.$refs.customInput.innerHTML;
},
onPaste(event) {
event.preventDefault();
// 获取剪贴板中的 HTML 和文本内容
const html = (event.clipboardData || window.clipboardData).getData(
"text/html"
);
const text = (event.clipboardData || window.clipboardData).getData(
"text/plain"
);
// 设置最大输入限制
const maxLength = Number(this.maxlength) || 300;
// 此时加个条件 看鼠标选中的文本长度,剩余可输入长度加上选中文本长度
const selection1 = window.getSelection();
const range1 = selection1.getRangeAt(0);
const clonedSelection = range1.cloneContents();
let selectTextLen = 0
if(clonedSelection.textContent && clonedSelection.textContent.length) {
selectTextLen = clonedSelection.textContent.length;
}
// 剩余可输入长度
const remainingLength = maxLength - this.inputValueLen + selectTextLen;
// 过滤掉不可见字符
const cleanText = text.replace(/\s/g, "");
// 创建一个临时 div 用于处理粘贴的 HTML 内容
const tempDiv = document.createElement("div");
tempDiv.innerHTML = html;
// 过滤掉不需要的内容,例如注释和换行符
const fragment = document.createDocumentFragment();
let totalLength = 0;
if (cleanText) {
if (remainingLength >= cleanText.length) {
fragment.appendChild(document.createTextNode(cleanText));
} else {
const truncatedText = cleanText.substr(0, remainingLength);
fragment.appendChild(document.createTextNode(truncatedText));
}
}else {
Array.from(tempDiv.childNodes).forEach((node) => {
const regex = /<span class="active-text" contenteditable="false" data-id="(\d+)">@([^<]+)<\/span>/g;
// 过滤注释和空白节点
if (
node.nodeType !== 8 &&
!(node.nodeType === 3 && !/\S/.test(node.textContent))
) {
const childText = node.textContent || "";
const childLength = childText.length;
const childHtml = node.outerHTML || node.innerHTML;
// 如果剩余空间足够,插入节点
if ((regex.exec(childHtml) !== null) && totalLength + childLength <= remainingLength) {
fragment.appendChild(node.cloneNode(true));
totalLength += childLength;
} else if (remainingLength - totalLength > 0) {
// 如果还有剩余长度,不插入节点,插入文本内容
const lastNodeLength = remainingLength - totalLength;
const truncatedText = childText.substr(0, lastNodeLength);
fragment.appendChild(document.createTextNode(truncatedText));
totalLength += truncatedText.length;
} else {
// 如果添加当前节点的内容会超出剩余可插入长度,则结束循环
return;
}
}
});
}
// 插入处理后的内容到光标位置
const selection = window.getSelection();
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(fragment);
// 更新输入框内容和长度
this.model = this.$refs.customInput.innerHTML;
this.inputValueLen = this.$refs.customInput.innerText.length;
// 设置光标位置为插入内容的后面一位
const newRange = document.createRange();
newRange.setStart(range.endContainer, range.endOffset);
newRange.collapse(true);
selection.removeAllRanges();
selection.addRange(newRange);
},
// 修改默认复制事件
onCopy(e) {
e.preventDefault();
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const clonedSelection = range.cloneContents();
// 检查复制的内容是否包含符合条件的元素
const hasActiveText =
clonedSelection.querySelector(
'.active-text[contenteditable="false"][data-id]'
) !== null;
const clipboardData = e.clipboardData || window.clipboardData;
if(hasActiveText) {
const div = document.createElement("div");
div.appendChild(clonedSelection);
const selectedHtml = div.innerHTML;
clipboardData.setData("text/html", selectedHtml);
}else {
clipboardData.setData("text/plain", clonedSelection.textContent || "");
}
},
// 保存焦点位置
async saveIndex() {
const selection = getSelection();
this.selectionIndex = selection.anchorOffset;
const nodeList = this.$refs.customInput.childNodes;
const range = selection.getRangeAt(0);
// 保存当前编辑的dom节点
for (const [index, value] of nodeList.entries()) {
// 这里第二个参数要配置成true,没配置有其他的一些小bug
// (range.startContainer.contains(value) && range.endContainer.contains(value)) 是为了处理兼容性问题
if (
selection.containsNode(value, true) ||
(range.startContainer.contains(value) &&
range.endContainer.contains(value))
) {
if (value.nodeType === Node.TEXT_NODE) {
this.dom = value;
this.beforeDomVal = value.textContent;
this.domIndex = index;
const selection = window.getSelection();
const range = selection.getRangeAt(0);
this.startOffset = range.startOffset - 1;
} else {
const childNodeList = value.childNodes;
for (const [childIndex, childValue] of childNodeList.entries()) {
if (selection.containsNode(childValue, true)) {
this.dom = value;
this.beforeDomVal = value.textContent;
this.domIndex = index;
this.childDomIndex = childIndex;
const selection = window.getSelection();
const range = selection.getRangeAt(0);
this.startOffset = range.startOffset - 1;
}
}
}
}
}
},
// 筛选人弹窗
showList(bool = true) {
this.showPopover = bool;
if (bool) {
const offset =
this.getCursorDistanceFromDivBottom(this.$refs.customInput) || -1;
if (offset < 0) {
this.popoverOffset = 0;
} else {
this.popoverOffset = -(offset - 1);
}
}
if (!bool) {
this.listInputValue = "";
this.remoteMethod();
}
},
// 获取光标位置
getCursorDistanceFromDivBottom(editableDiv) {
// 获取选区
const selection = window.getSelection();
// 获取选区的范围
const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
if (range) {
// 创建一个临时元素来标记范围的结束位置
const markerElement = document.createElement("span");
// 插入临时标记元素
range.insertNode(markerElement);
markerElement.appendChild(document.createTextNode("\u200B")); // 零宽空格
// 获取标记元素的位置信息
const markerOffsetTop = markerElement.offsetTop;
const markerHeight = markerElement.offsetHeight;
// 计算光标距离div底部的距离
const cursorDistanceFromBottom =
editableDiv.offsetHeight - (markerOffsetTop + markerHeight);
// 滚动条距顶部的高度
const scrollTop = editableDiv.scrollTop || 0;
// 移除临时标记元素
markerElement.parentNode.removeChild(markerElement);
// 返回光标距离底部的距离
return cursorDistanceFromBottom + scrollTop;
}
// 如果没有选区,则返回-1或者其他错误值
return -1;
},
// 搜索筛选人
async remoteMethod() {
let query = this.listInputValue;
this.searchOperatorLoad = true;
let params = {
keyword: query,
pageNo: 1,
pageSize: 500,
};
await queryEmployeeByParam(params)
.then((res) => {
this.searchOperatorList = res.list
.filter((i) => i.employeeStatusId === 1)
.map((e) => {
e.value = e.employeeNo + "_" + e.realname;
return e;
});
})
.catch(() => {});
this.searchOperatorLoad = false;
},
handleNameShift(item) {
const name = item.realname || "";
if (!name) return "--";
if (name.length > 1) {
return name.slice(0, 1);
} else {
return name;
}
},
// 按钮div点击 聚焦textarea
handleBtnBoxClick() {
this.$refs.customInput.focus();
},
// 获取@人的姓名
getInnerText() {
const customInput = this.$refs.customInput;
if (!customInput) return;
return customInput.innerText;
},
// 获取@人的工号
getJobId() {
const nodeList = this.$refs.customInput.childNodes;
let list = [];
nodeList.forEach((e) => {
if (e.className === "active-text") {
list.push(e.getAttribute("data-id"));
}
});
return list;
},
clearInput() {
this.$refs.customInput.innerText = "";
this.$refs.customInput.innerHTML = "";
this.inputValueLen = 0;
this.$emit("input", "");
this.$emit("inputText", "");
this.$emit("changeChosen", []);
},
},
};
</script>
<style lang="scss" scoped>
.custom-textarea-btn {
position: absolute;
bottom: 1px;
right: 4px;
left: 4px;
text-align: right;
// background: #fff;
padding-bottom: 3px;
.el-button {
font-size: 12px;
padding: 4px 10px;
}
}
.custom-textarea-box {
position: relative;
}
.custom-at-limit {
position: absolute;
right: 12px;
bottom: 4px;
font-size: 12px;
color: #999;
line-height: 12px;
}
::v-deep.custom-textarea {
width: 100%;
min-height: 50px;
max-height: 200px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #ffffff;
padding: 5px 15px;
color: #606266;
overflow-y: auto;
line-height: 20px;
font-size: 14px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
position: relative;
word-break: break-all;
&.show-word-limit {
padding-bottom: 16px;
}
&.custom-textarea-disabled {
cursor: not-allowed;
background-color: #f5f7fa;
border-color: #e4e7ed;
color: #c0c4cc;
}
&:focus {
border-color: #f98600 !important;
}
&:empty::before {
content: attr(placeholder);
font-size: 14px;
color: #c0c4cc;
}
.active-text {
color: #909399;
// padding: 2px 6px;
// background: #f4f4f5;
margin-right: 4px;
// border-radius: 4px;
// font-size: 12px;
}
// &:focus::before {
// content: "";
// }
}
::v-deep.custom-select-box {
position: relative;
.el-popover {
padding: 0;
top: 0;
box-shadow: 0 4px 8px 0 rgba(89, 88, 88, 0.8);
}
.custom-select-content {
width: 259px;
padding: 8px;
max-height: 260px;
overflow-y: auto;
}
.custom-select-item {
// font-size: 14px;
// padding: 0 20px;
// position: relative;
// height: 34px;
// line-height: 34px;
// box-sizing: border-box;
display: flex;
padding: 8px 12px;
border-bottom: 1px solid #ebebeb;
align-items: center;
color: #606266;
cursor: pointer;
&:last-child {
border-bottom: none;
}
.avatar-box {
flex-shrink: 0;
.custom-select-item-avatar {
width: 24px;
height: 24px;
background-color: #ffb803;
border-radius: 50%;
text-align: center;
line-height: 24px;
color: #ffffff;
}
}
.custom-select-item-content {
flex: 1;
padding-left: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
background-color: #f5f7fa;
}
&.hoverItem {
background-color: #dbdbdb;
}
}
.custom-select-empty {
padding: 10px 0;
text-align: center;
color: #999;
font-size: 14px;
&.load {
display: flex;
align-items: center;
justify-content: center;
}
}
.custom-scroll {
overflow: auto;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
border-radius: 8px;
background-color: #b4b9bf;
}
}
}
</style>
外部调用示例
<template>
<div>
<CustomInput
v-model="customInputHTML"
placeholder="这是一个支持@的输入框组件"
height="unset"
@inputText="handleChangeInputText"
@changeChosen="handleChangeChosen"
/>
</div>
</template>
<script>
import CustomInput from "@/components/CustomInput/index.vue";
export default {
components: { CustomInput },
data() {
return {
// 输入框的html代码
customInputHTML: "",
// 输入框的文本,可让后端使用,如不使用,也没啥用
customInputText: "",
// 输入框返回的@人员数据
customInputMentions: []
}
},
methods: {
// 获取输入框返回的文本
handleChangeInputText(val) {
this.customInputText = val;
},
// 获取输入框返回的文本
handleChangeChosen(val) {
this.customInputMentions = val;
},
}
}
</script>