Vue 评论@人功能实现
<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>

浙公网安备 33010602011771号