依赖
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"@wangeditor/plugin-mention": "^1.0.0",
RichEditor.vue
<template>
<div style="border: 1px solid #ccc; position: relative">
<Editor
style="height: 100px"
:defaultConfig="editorConfig"
v-model="valueHtml"
@onCreated="handleCreated"
@onChange="onChange"
/>
<mention-modal
v-if="isShowModal"
@hideMentionModal="hideMentionModal"
@insertMention="insertMention"
:position="position"
:list="list"
></mention-modal>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef, onBeforeUnmount, nextTick, watch } from 'vue';
import { Boot } from '@wangeditor/editor';
import { Editor } from '@wangeditor/editor-for-vue';
import mentionModule from '@wangeditor/plugin-mention';
import MentionModal from './MentionModal.vue';
// 注册插件
Boot.registerModule(mentionModule);
const props = withDefaults(
defineProps<{
content?: string;
list: any[];
}>(),
{
content: '',
},
);
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();
// const valueHtml = ref('<p>你好<span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="A张三" data-info="%7B%22id%22%3A%22a%22%7D">@A张三</span></p>')
const valueHtml = ref('');
const isShowModal = ref(false);
watch(
() => props.content,
(val: string) => {
nextTick(() => {
valueHtml.value = val;
});
},
);
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
const position = ref({
left: '15px',
top: '0px',
bottom: '0px',
});
const handleCreated = (editor: any) => {
editorRef.value = editor; // 记录 editor 实例,重要!
position.value = editor.getSelectionPosition();
};
const showMentionModal = () => {
// 对话框的定位是根据富文本框的光标位置来确定的
nextTick(() => {
const editor = editorRef.value;
console.log(editor.getSelectionPosition());
position.value = editor.getSelectionPosition();
});
isShowModal.value = true;
};
const hideMentionModal = () => {
isShowModal.value = false;
};
const editorConfig = {
placeholder: '@通知他人,添加评论',
EXTEND_CONF: {
mentionConfig: {
showModal: showMentionModal,
hideModal: hideMentionModal,
},
},
};
const onChange = (editor: any) => {
console.log('changed html', editor.getHtml());
console.log('changed content', editor.children);
};
const insertMention = (id: any, username: any) => {
const mentionNode = {
type: 'mention', // 必须是 'mention'
value: username,
info: { id, x: 1, y: 2 },
job: '123',
children: [{ text: '' }], // 必须有一个空 text 作为 children
};
const editor = editorRef.value;
if (editor) {
editor.restoreSelection(); // 恢复选区
editor.deleteBackward('character'); // 删除 '@'
console.log('node-', mentionNode);
editor.insertNode(mentionNode, { abc: 'def' }); // 插入 mention
editor.move(1); // 移动光标
}
};
function getAtJobs() {
return editorRef.value.children[0].children.filter((item: any) => item.type === 'mention').map((item: any) => item.info.id);
}
defineExpose({
valueHtml,
getAtJobs,
});
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
<style scoped>
.w-e-scroll {
max-height: 100px;
}
</style>
MentionModal.vue
<template>
<div id="mention-modal" :style="{ left, right, bottom }">
<el-input
id="mention-input"
v-model="searchVal"
ref="input"
@keyup="inputKeyupHandler"
onkeypress="if(event.keyCode === 13) return false"
placeholder="请输入用户名搜索"
/>
<el-scrollbar height="180px">
<ul id="mention-list">
<li v-for="item in searchedList" :key="item.id" @click="insertMentionHandler(item.id, item.username)">
{{ item.username }}
</li>
</ul>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue';
const props = defineProps<{
position: any;
list: any[];
}>();
const emit = defineEmits(['hideMentionModal', 'insertMention']);
// 定位信息
const top = computed(() => {
return props.position.top;
});
const bottom = computed(() => {
return props.position.bottom;
});
const left = computed(() => {
return props.position.left;
});
const right = computed(() => {
if (props.position.right) {
const right = +props.position.right.split('px')[0] - 180;
return right < 0 ? 0 : right + 'px';
}
return '';
});
// list 信息
const searchVal = ref('');
// const tempList = Array.from({ length: 20 }).map((_, index) => {
// return {
// id: index,
// username: '张三' + index,
// account: 'wp',
// };
// });
const list: any = ref(props.list);
// 根据 <input> value 筛选 list
const searchedList = computed(() => {
const searchValue = searchVal.value.trim().toLowerCase();
return list.value.filter((item: any) => {
const username = item.username.toLowerCase();
if (username.indexOf(searchValue) >= 0) {
return true;
}
return false;
});
});
const inputKeyupHandler = (event: any) => {
// esc - 隐藏 modal
if (event.key === 'Escape') {
emit('hideMentionModal');
}
// enter - 插入 mention node
if (event.key === 'Enter') {
// 插入第一个
const firstOne = searchedList.value[0];
if (firstOne) {
const { id, username } = firstOne;
insertMentionHandler(id, username);
}
}
};
const insertMentionHandler = (id: any, username: any) => {
emit('insertMention', id, username);
emit('hideMentionModal'); // 隐藏 modal
};
const input = ref();
onMounted(() => {
// 获取光标位置
// const domSelection = document.getSelection()
// const domRange = domSelection?.getRangeAt(0)
// if (domRange == null) return
// const rect = domRange.getBoundingClientRect()
// 定位 modal
// top.value = props.position.top
// left.value = props.position.left
// focus input
nextTick(() => {
input.value?.focus();
});
});
</script>
<style>
#mention-modal {
position: absolute;
bottom: -10px;
border: 1px solid #ccc;
background-color: #fff;
padding: 5px;
transition: all 0.3s;
}
#mention-modal input {
width: 150px;
outline: none;
}
#mention-modal ul {
padding: 0;
margin: 5px 0 0;
}
#mention-modal ul li {
list-style: none;
cursor: pointer;
padding: 5px 2px 5px 10px;
text-align: left;
}
#mention-modal ul li:hover {
background-color: #f1f1f1;
}
</style>