
需求描述:
1.需要单选多选人员
2.@输入弹出人员选择组件
3.点击@弹出人员选择组件
4.@XX 需要组合一起删除
5.可以输入其他文本,文本中间需要可以插入@XX
<template>
<div class="progress">
<div class="list">
<div class="description pointer">
<el-dropdown trigger="click" @command="onDropClick">
<span class="el-dropdown-link">
{{activeLabel}}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="item" v-for="item in dropOptions" :key="item.label">{{ item.label }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="msg-content" v-if="logList.length>0">
<div class="item flex" v-for="item in logList">
<div class="flex flex1">
<xr-avatar
:name="item.createUserIdText"
:size="45"
key="userName"/>
<div class="ml10">
<div>
<span class="color_99">{{item.createUserIdText}}</span>
<span class="color_99 ml10">{{ item.typeText }}</span>
</div>
<div class="message mt5">
<p class="color_txt">{{item.title}}</p>
<p class="flex-col bor-top" v-if="item.hasContent">
<span class="mt8">状态
<span>{{ item.startStatus == 1 ? '正常' :( item.startStatus == 2 ? '风险' : '逾期') }}</span>
<span v-if="item.endStatus && item.endStatus!='null'">- {{ item.endStatus == 1 ? '正常' :( item.endStatus == 2 ? '风险' : '逾期') }}</span>
</span>
<span class="mt8">进度 {{item.startProgress}}% - {{item.endProgress}}%
<span> <i class="el-icon-caret-top"></i>{{item.differ}}%</span>
</span>
<span class="mt8">说明 {{ item.remarks!='null' ? item.remarks : '-' }}</span>
</p>
</div>
</div>
</div>
<div class="color_99">{{ filterTime(item.createTime) }}</div>
</div>
</div>
<div v-else class="msg-content">
<xr-empty :top="250" :width="150"></xr-empty>
</div>
</div>
<div class="send-box">
<div class="choose-box" v-if="showChoose">
<div class="user-list">
<select-employee
:radio="false"
v-model="users"
closeDep
@select="userChange(arguments[0])"
>
</select-employee>
<div class="list-btn">
<el-button type="primary" plain size="mini" @click="showChoose = false">取消</el-button>
<el-button type="primary" size="mini" @click="handleSelectUser">确定</el-button>
</div>
</div>
</div>
<div
ref="editor"
class="send-content"
spellcheck="false"
:contenteditable="true"
@keyup="handkeKeyUp"
@input="saveCursorPosition"
@click="saveCursorPosition"
@keydown="handleKeyDown"
/>
<div class="send-txt com-flex">
<span class="icon">
<i @click="chooseOwner">@</i>
<i class="el-icon-paperclip ml5"></i>
</span>
<el-button type="primary" v-debounce='confirm'>发送</el-button>
</div>
</div>
</div>
</template>
<script>
import { userListAPI } from '@/api/common'
import { getOkrLog, saveOkrLog } from '@/api/task'
import SelectEmployee from '@/components/SelectEmployee/main'
import moment from 'moment'
export default{
components:{
SelectEmployee
},
data(){
return {
dropOptions:[
{ label:'所有记录', num:6, type:'' },
{ label:'目标更新进度', num:6, type:1 },
{ label:'评论', num:6, type:3 },
{ label:'操作日志', num:6, type:4 },
{ label:'附件', num:6, type:5 },
],
activeLabel:'所有记录',
activeNum:6,
showChoose:false,
ownerId:'',
userList:null,
content:'',
logType:'',
logList:[],
objectiveItemId:'',
currentMention: '',
attachId:[],
users:[],
userNames:[],
queryString:'',
endIndex: '',
node: '',
savedRange:''
}
},
props:{
detail:{
type:Object,
default:()=>{}
}
},
created(){
this.getUserList()
this.getLog()
this.$bus.on('getLog', () => {
this.getLog()
})
this.detail.krList && this.detail.krList.map((item,i)=>{
let obj = {
label:`KR${i+1}更新进度`,
type:2,
...item
}
this.dropOptions.push(obj)
})
},
computed: {
},
methods:{
async chooseOwner(){
this.$refs.editor.focus()
this.restoreCursorPosition()
const node = this.getRangeNode()
console.log(node)
const endIndex = this.getCursorIndex()
this.node = node
this.endIndex = endIndex
this.queryString = this.getAtUser() || ''
this.showChoose = true
},
saveCursorPosition() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
this.savedRange = selection.getRangeAt(0);
console.log(this.savedRange,'11111111111111')
}
},
restoreCursorPosition() {
if (this.savedRange) {
const selection = window.getSelection();
selection.removeAllRanges();
console.log(this.savedRange,'55555555555555')
selection.addRange(this.savedRange);
}
},
handkeKeyUp() {
if (this.showAt()) {
const node = this.getRangeNode()
const endIndex = this.getCursorIndex()
this.node = node
this.endIndex = endIndex
this.queryString = this.getAtUser() || ''
this.showChoose = true
} else {
this.showChoose = false
}
if (this.$refs.editor.innerText.length > 100) {
this.$refs.editor.innerText = this.$refs.editor.innerText.substr(
0,
100
)
document.execCommand('selectAll', false, null)
document.getSelection().collapseToEnd()
}
},
getCursorIndex() {
const selection = window.getSelection()
return selection.focusOffset
},
getRangeNode() {
const selection = window.getSelection()
return selection.focusNode
},
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
},
handleKeyDown(e) {
if (this.showChoose) {
if (e.code === 'ArrowUp' ||
e.code === 'ArrowDown' ||
e.code === 'Enter') {
e.preventDefault()
}
}
},
handleSelectUser() {
this.replaceAtUser()
this.showChoose = false
},
replaceAtUser() {
const node = this.node
if (node && this.users.length>0) {
const content = node.textContent || ''
const endIndex = this.endIndex
const preSlice = this.replaceString(content.slice(0, endIndex), '')
const restSlice = content.slice(endIndex)
const parentNode = node.nodeType == 3 ? node.parentNode : node
const nextNode = node.nodeType == 3 ? node.nextSibling : ''
const previousTextNode = new Text(preSlice)
const nextTextNode = new Text('\u200b' + restSlice)
const atButton = this.createAtButtonUsers()
node.nodeType == 3 && 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)
}
},
createAtButtonUsers(){
const btns = this.users.map(user=>{
return this.createAtButton(user)
})
let fragment = document.createDocumentFragment();
btns.map(item=>{
fragment.appendChild(item);
})
return fragment
},
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.authName}`
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) {
console.log(raw,'rrrrrrrrrrrrr')
return raw.replace(/@([^@\s]*)$/, replacer)
},
userChange(value){
this.users = value
},
filterTime(time){
return moment(time).format('MM/DD hh:mm')
},
confirm(){
const dom = document.querySelectorAll('.bpm-at-button')
console.log(typeof dom,'ddddd')
let domUser = []
dom.forEach(item=>{
const user = JSON.parse(item.dataset.user)
domUser.push(user)
})
let params = {
id: this.detail.id,
objectiveId: this.detail.id,
objectiveItemId: this.objectiveItemId,
type:this.logType,
title: this.content,
notifyUserIds:domUser.map(item=>item.userId),
attachId:this.attachId
}
saveOkrLog(params).then(res=>{
if(res.code !=0 ){
this.$message.error('请重试')
}else{
this.content = ''
this.getLog()
}
})
},
getLog(){
let params = {
id:this.detail.id,
objectiveId: this.detail.id,
objectiveItemId: this.objectiveItemId,
type:this.logType,
}
getOkrLog(params).then(res=>{
let data = res.data || []
this.logList = data.map(item=>{
let content = (item.type!=4 && JSON.parse(item.content)) || false
return {
...item,
hasContent:item.content&&item.type!=4 ? true:false,
startStatus: (item.type!=4 && content&& content.status && content.status.toString().includes(',') ? content.status.split(',')[0] : content.status) || '',
endStatus:(item.type!=4 && content&& content.status &&content.status.toString().includes(',') ? content.status.split(',')[1] : content.status) || '',
startProgress: item.type!=4 && content && Math.round(content.progress.split(',')[0]*100),
endProgress: item.type!=4 && content && Math.round(content.progress.split(',')[1]*100),
differ:item.type!=4 && content && Math.round(content.progress.split(',')[1]*100) - Math.round(content.progress.split(',')[0]*100),
remarks:content && content.remarks
}
})
})
},
getUserList(){
userListAPI({page:1,limit:500}).then(res=>{
this.userList = res.data || []
})
},
onDropClick(command) {
this.activeLabel = command.label
this.activeNum = command.num
this.logType = command.type
if(command.type == 2){
this.objectiveItemId = command.id
}else{
this.objectiveItemId = ''
}
this.getLog()
},
}
}
</script>
<style lang="scss" scoped>
.progress{
.msg-content{
height:calc(100vh - 210px);
overflow-y:auto;
}
.ml10{
margin-left:10px;
}
.mt5{
margin-top:5px;
}
.ml5{
margin-left:5px;
}
.mr5{
margin-right:5px;
}
.mt10{
margin-top: 10px;
}
.pd20{
padding-bottom:20px;
}
.com-flex{
display: flex;
align-items: center;
justify-content: space-between;
}
.start-flex{
display: flex;
align-items: center;
}
.flex-end {
align-items: flex-end;
}
.flex{
display: flex;
}
.flex1 {
flex:1
}
.color_99{
color:#999;
}
.flex-col{
display: flex;
flex-direction: column;
}
.item{
padding:10px;
}
.color_txt{
color:#94ACC3;
padding-bottom:10px;
}
.bor-top{
border-top:1px solid #A5CFF3;
}
.message {
padding:10px;
background: #C9E6FF;
border:1px solid #A5CFF3;
border-radius: 0 8px 8px 8px;
width:285px;
}
.description{
padding:10px;
}
.send-box{
position: absolute;
bottom: 0;
width:460px;
margin:10px;
padding:2px;
border-radius: 4px;
border:1px solid #dddddd;
:deep(.el-textarea__inner){
border:none
}
.cusor{
animation: blink 1s linear infinite;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0; }
100% { opacity: 1; }
}
.send-content{
height:50px;
padding:5px;
}
.choose-box{
padding: 10px 0 0 10px;
position: relative;
.user-list{
width:450px;
position: absolute;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
bottom:calc(100% + 10px);
border-radius: 6px;
left:5px;
background:#ffffff;
.title{
padding:10px 20px;
border-bottom: 1px solid #ededed;
.type{
color:#48a1f0
}
}
.mr10{
margin-right:10px;
}
.content{
display: flex;
flex-direction: column;
align-items: center;
width:100%;
height:300px;
overflow-y: auto;
}
.content-item{
display: flex;
align-items: center;
width:100%;
padding:3px 20px;
.label{
margin-left:5px;
}
}
}
.list-btn{
padding:5px 20px;
text-align: right
}
}
.send-txt{
text-align: right;
.icon{
color:#999;
cursor: pointer;
}
}
}
.empty-box.msg-content{
margin-top:200px;
display:flex;
flex-direction:column;
align-items:center;
img{
width:200px;
height:auto;
}
}
}
</style>
参考:vue实现(@、At、艾特)demo - 掘金 (juejin.cn)