Vue 评论@人功能实现
![](https://img2022.cnblogs.com/blog/1472677/202211/1472677-20221124111931649-70443040.png)
<template> <div class="content-container"> <el-dialog :title="title" :visible.sync="dialogVisible" width="540px" :append-to-body="true" :close-on-click-modal="false" :before-close="handleClose" custom-class="bpm-message-dialog" > <el-form ref="form" :rules="rules" :model="form" label-position="top" > <el-form-item label="内容" prop="note"> <div ref="editor" class="message" spellcheck="false" :contenteditable="true" @keyup="handkeKeyUp" @keydown="handleKeyDown" /> </el-form-item> <el-form-item label="附件"> <MyUpload ref="myUpload" v-model="form.fileList" type="img" :accept="['.png','.jpg','.jpeg','.gif']" /> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="handleClose">取 消</el-button> <el-button type="primary" @click="submit">确 定</el-button> </span> <AtDialog v-if="showDialog" ref="atDialog" :visible="showDialog" :position="position" :query-string="queryString" @onPickUser="handlePickUser" @onHide="handleHide" @onShow="handleShow" @close="showDialog = false" /> </el-dialog> </div> </template> <script> import MyUpload from '@/components/approveUpload/approve.vue' import AtDialog from './AtDialog' export default { name: 'RejectA', components: { MyUpload, AtDialog }, props: { title: { type: String, default: '留言' } }, data() { return { dialogVisible: false, form: { note: '', fileList: '' }, rules: { note: [{ required: true, message: '请输入备注信息', trigger: 'blur' }] }, fileList: [], allowSize: 50 * 1024, limit: 5, operateType: null, // 操作类型 node: '', // 获取到节点 user: '', // 选中项的内容 endIndex: '', // 光标最后停留位置 queryString: '', // 搜索值 showDialog: false, // 是否显示弹窗 position: { x: 0, y: 0 }// 弹窗显示位置 } }, computed: {}, watch: {}, created() {}, mounted() {}, methods: { // 限制输入框换行操作 handleTextareaKeydown() { const e = window.event || arguments[0] if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === 13) { e.returnValue = false return false } }, openDialog() { this.dialogVisible = true }, handleClose(done) { console.log('handleClose') this.$refs.form.resetFields() this.$set(this.form, 'fileList', null) this.$refs.myUpload.clearFileList() this.dialogVisible = false }, // 获取光标位置 getCursorIndex() { const selection = window.getSelection() return selection.focusOffset // 选择开始处 focusNode 的偏移量 }, // 获取节点 getRangeNode() { const selection = window.getSelection() return selection.focusNode // 选择的结束节点 }, // 弹窗出现的位置 getRangeRect() { const selection = window.getSelection() const range = selection.getRangeAt(0) // 是用于管理选择范围的通用对象 const rect = range.getClientRects()[0] // 择一些文本并将获得所选文本的范围 const LINE_HEIGHT = 30 return { x: rect.x, y: rect.y + LINE_HEIGHT } }, // 是否展示 @ showAt() { const node = this.getRangeNode() if (!node || node.nodeType !== Node.TEXT_NODE) return false const content = node.textContent || '' const regx = /@([^@\s]*)$/ const match = regx.exec(content.slice(0, this.getCursorIndex())) console.log('match', match) return match && match.length === 2 }, // 获取 @ 用户 getAtUser() { const content = this.getRangeNode().textContent || '' const regx = /@([^@\s]*)$/ const match = regx.exec(content.slice(0, this.getCursorIndex())) if (match && match.length === 2) { return match[1] } return undefined }, // 创建标签 createAtButton(user) { const btn = document.createElement('span') btn.style.display = 'inline-block' btn.dataset.user = JSON.stringify(user) btn.className = 'bpm-at-button' btn.contentEditable = 'false' btn.textContent = `@${user.name}` const wrapper = document.createElement('span') wrapper.style.display = 'inline-block' wrapper.contentEditable = 'false' const spaceElem = document.createElement('span') spaceElem.style.whiteSpace = 'pre' spaceElem.textContent = '\u200b' spaceElem.contentEditable = 'false' const clonedSpaceElem = spaceElem.cloneNode(true) wrapper.appendChild(spaceElem) wrapper.appendChild(btn) wrapper.appendChild(clonedSpaceElem) return wrapper }, replaceString(raw, replacer) { return raw.replace(/@([^@\s]*)$/, replacer) }, // 插入@标签 replaceAtUser(user) { const node = this.node if (node && user) { const content = node.textContent || '' const endIndex = this.endIndex const preSlice = this.replaceString(content.slice(0, endIndex), '') const restSlice = content.slice(endIndex) const parentNode = node.parentNode const nextNode = node.nextSibling const previousTextNode = new Text(preSlice) const nextTextNode = new Text('\u200b' + restSlice) // 添加 0 宽字符 const atButton = this.createAtButton(user) parentNode.removeChild(node) // 插在文本框中 if (nextNode) { parentNode.insertBefore(previousTextNode, nextNode) parentNode.insertBefore(atButton, nextNode) parentNode.insertBefore(nextTextNode, nextNode) } else { parentNode.appendChild(previousTextNode) parentNode.appendChild(atButton) parentNode.appendChild(nextTextNode) } // 重置光标的位置 const range = new Range() const selection = window.getSelection() range.setStart(nextTextNode, 0) range.setEnd(nextTextNode, 0) selection.removeAllRanges() selection.addRange(range) } }, // 键盘抬起事件 handkeKeyUp() { if (this.showAt()) { const node = this.getRangeNode() const endIndex = this.getCursorIndex() this.node = node this.endIndex = endIndex this.position = this.getRangeRect() this.queryString = this.getAtUser() || '' this.showDialog = true } else { this.showDialog = false } }, // 键盘按下事件 handleKeyDown(e) { if (this.showDialog) { if (e.code === 'ArrowUp' || e.code === 'ArrowDown' || e.code === 'Enter') { e.preventDefault() } } }, resetAtDialog() { this.queryString = '' this.$refs.index = -1 }, // 插入标签后隐藏选择框 handlePickUser(user) { this.replaceAtUser(user) this.user = user this.showDialog = false }, // 隐藏选择框 handleHide() { this.showDialog = false }, // 显示选择框 handleShow() { this.queryString = '' this.$refs.index = -1 this.showDialog = true }, submit() { switch (this.title) { case '驳回到发起人': this.operateType = 4 break case '驳回到指定节点': this.operateType = 5 break case '驳回到上一节点': this.operateType = 6 break case '同意': this.operateType = 7 break } this.$refs.form.validate((valid) => { if (valid) { // 判断是否存在文件上传 this.isUploading().then((res) => { if (res) { console.log('res', res) } else { console.log('') } }) } else { console.log('error submit!!') return false } }) } } } </script> <style lang="scss"> .bpm-message-dialog { .message { min-height: 96px; display: block; resize: vertical; padding: 5px 15px; line-height: 1.5; -webkit-box-sizing: border-box; box-sizing: border-box; width: 100%; font-size: inherit; color: #606266; background-color: #FFFFFF; background-image: none; border: 1px solid #DCDFE6; border-radius: 4px; -webkit-transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); } } </style> <style lang="scss" scoped> @import "~@/views/taskManage/css/dialog.scss"; </style>
<template>
<div
class="wrapper"
:style="{position:'fixed',top:position.y +'px',left:position.x+'px'}"
>
<div v-if="!mockList.length" class="empty">
<p>无搜索结果</p>
</div>
<div
v-for="(item,i) in mockList"
:key="item.id"
ref="usersRef"
class="item"
:class="{'active': i === index}"
@click="clickAt($event,item)"
@mouseenter="hoverAt(i)"
>
<div class="name">{{ item.name }}</div>
</div>
</div>
</template>
<script>
const mockData = [
{ name: 'HTML', id: 'HTML' },
{ name: 'CSS', id: 'CSS' },
{ name: 'Java', id: 'Java' },
{ name: 'JavaScript', id: 'JavaScript' }
]
export default {
name: 'AtDialog',
props: {
visible: {
type: Boolean
},
position: {
type: Object,
default: () => {}
},
queryString: {
type: String,
default: ''
}
},
data() {
return {
users: [],
index: -1,
mockList: mockData
}
},
watch: {
queryString(val) {
val ? this.mockList = mockData.filter(({ name }) => name.startsWith(val)) : this.mockList = mockData.slice(0)
}
},
mounted() {
document.addEventListener('keyup', this.keyDownHandler)
},
destroyed() {
document.removeEventListener('keyup', this.keyDownHandler)
},
methods: {
closeDialog() {
this.index = -1
this.$emit('close')
},
keyDownHandler(e) {
if (e.code === 'Escape') {
this.$emit('onHide')
return
}
// 键盘按下 => ↓
if (e.code === 'ArrowDown') {
if (this.index >= this.mockList.length - 1) {
this.index = 0
} else {
this.index = this.index + 1
}
}
// 键盘按下 => ↑
if (e.code === 'ArrowUp') {
if (this.index <= 0) {
this.index = this.mockList.length - 1
} else {
this.index = this.index - 1
}
}
// 键盘按下 => 回车
if (e.code === 'Enter') {
if (this.mockList.length) {
const user = {
name: this.mockList[this.index].name,
id: this.mockList[this.index].id
}
this.$emit('onPickUser', user)
this.index = -1
}
}
},
clickAt(e, item) {
const user = {
name: item.name,
id: item.id
}
this.$emit('onPickUser', user)
this.index = -1
},
hoverAt(index) {
this.index = index
}
}
}
</script>
<style scoped lang="scss">
.wrapper {
width: 238px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
box-sizing: border-box;
padding: 6px 0;
}
.empty{
font-size: 14px;
padding: 0 20px;
color: #999;
height: 24px;
line-height: 24px;
position: relative;
svg {
position: absolute;
width: 18px;
height: 18px;
right: 6px;
top: 4px;
cursor: pointer;
}
}
.item {
font-size: 14px;
padding: 0 20px;
line-height: 34px;
cursor: pointer;
color: #606266;
&.active {
background: #f5f7fa;
color: blue;
.id {
color: blue;
}
}
&:first-child {
border-radius: 5px 5px 0 0;
}
&:last-child {
border-radius: 0 0 5px 5px;
}
.id {
font-size: 12px;
color: rgb(83, 81, 81);
}
}
</style>