This commit is contained in:
cansnow
2025-12-23 00:18:46 +08:00
parent 59d1ba9a7e
commit f49f1f1ad1
54 changed files with 25338 additions and 333 deletions
@@ -1,193 +1,284 @@
<template>
<view class="editor_wrap">
<editor
:placeholder="placeholder"
id="editor2"
@ready="editorReady"
@focus="editorFocus"
@blur="editorBlur"
@input="editorInput" />
</view>
<div id="editor-container" :call="option" :change:call="editorModule.call"><!-- 编辑器 --></div>
</template>
<script>
import { forIn } from "lodash";
import {html2Text} from "@/util/common";
export default {
props: {
placeholder: {
type: String,
default: "",
},
},
data() {
return {
editorCtx: null,
lastStr: "",
isInsertingEmoji: false, // 标记是否正在插入表情
hasFocus: false, // 记录编辑器是否有焦点
};
timer:null,
option:null,
events:[],
}
},
methods: {
editorReady() {
uni
.createSelectorQuery()
.select("#editor2")
.context((res) => {
//this.$emit("ready", res);
this.editorCtx = res.context;
})
.exec();
insertText(text,successFn,errorFn){
this.addEvent('insertText',text);
},
editorFocus() {
this.hasFocus = true;
// 如果正在插入表情,不触发 focus 事件,并立即隐藏键盘
if (this.isInsertingEmoji) {
// #ifdef APP-PLUS || H5
uni.hideKeyboard();
// #endif
return;
}
this.$emit("focus");
insertImgEmoji(src,successFn,errorFn){
this.addEvent('insertImgEmoji',src);
},
editorBlur() {
this.hasFocus = false;
this.$emit("blur");
insertMention(username,userid){
this.addEvent('insertMention',{
username,
userid
});
},
clear(){
this.editorCtx.clear()
this.addEvent('clear');
},
insertText(text,successFn,errFn){
// 标记正在插入表情,阻止 focus 事件触发
this.isInsertingEmoji = true;
// 先隐藏键盘,避免插入时键盘弹出
// #ifdef APP-PLUS || H5
uni.hideKeyboard();
// #endif
// 如果编辑器当前有焦点,先让它失焦(通过点击外部区域)
// 但这种方式可能不太可靠,所以我们主要依赖 isInsertingEmoji 标志
// 使用 insertText 插入文本(这是最可靠的方法)
// 虽然会触发焦点,但我们已经通过 isInsertingEmoji 标志阻止了 focus 事件
this.editorCtx.insertText({
text: text,
success: (res) => {
successFn && successFn.call(this, [res]);
console.log("插入文字成功");
// 插入后立即隐藏键盘,防止键盘弹出
// #ifdef APP-PLUS || H5
// 使用多个延迟确保键盘被隐藏
setTimeout(() => {
uni.hideKeyboard();
}, 10);
setTimeout(() => {
uni.hideKeyboard();
}, 50);
setTimeout(() => {
uni.hideKeyboard();
}, 100);
// #endif
// 延迟重置标志,确保 focus 事件被完全忽略
setTimeout(() => {
this.isInsertingEmoji = false;
}, 300);
},
fail: (err) => {
errFn && errFn.call(this, [err]);
console.log("插入文字失败", err);
this.isInsertingEmoji = false;
}
});
blur(){
this.addEvent('blur');
},
delete(){
this.editorCtx.getContents({
success({html,text,delta}){
console.log(html,text,delta);
}
})
return ;
//setContents(OBJECT)
let emojiStr = this.editorCtx.getContents();
let emojiArr = [];
emojiStr = emojiStr.replace(/\[([^(\]|\[)]*)\]/g, function(item, index) {
emojiArr.unshift(item);
});
let sendStr ="";
if (emojiArr.length > 0) {
if (this.sendStr.endsWith(emojiArr[0])) {
this.sendStr = this.sendStr.replace(emojiArr[0], "");
focus(){
this.addEvent('focus');
},
setHtml(html){
this.addEvent('setHtml',html);
},
getText(){
this.addEvent('getText');
},
getHtml(){
console.log(this);
return 1;
this.addEvent('getHtml');
},
getSelectionPosition(){
this.addEvent('getSelectionPosition');
},
getParentNode(){
this.addEvent('getParentNode');
},
// 调用
call() {
if (this.timer) return;
// 消费事件队列(生产者/消费者机制)
this.timer = setInterval(() => {
if (this.events.length) {
this.option = this.events.shift();
console.log(this.option);
} else {
this.sendStr = this.sendStr.slice(0, this.sendStr.length - 1);
clearInterval(this.timer);
this.timer = null;
}
} else {
this.sendStr = this.sendStr.slice(0, this.sendStr.length - 1);
}, 10);
},
// 添加事件队列
addEvent(name, data) {
// #ifdef APP-PLUS
// tips:由于采用监听option改变来调用方法,
// 如果连续变化两次option,渲染层只会则监听到最后一次
// 导致调用丢失,所以采用事件队列形式解决,稍微延时10ms
// 等待渲染进程监听到option变化,在进行更改option
// 从性能上,几乎无感可以放心使用
const option = {
id: this.genId(),
name: `_${name}`,
data
};
this.events.push(option);
this.call();
// #endif
// #ifndef APP-PLUS
this[`_${name}`] && this[`_${name}`](data);
// #endif
},
genId() {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < 30; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
this.editorCtx.setContents({
html:sendStr
})
console.log('delete')
},
editorInput(e) {
let str = e.detail.html;
const oldArr = (this.lastStr ?? '').split("");
let contentStr = str;
oldArr.forEach((str) => {
contentStr = contentStr.replace(str, "");
});
contentStr = html2Text(contentStr);
this.$emit("input", e);
this.lastStr = e.detail.html;
return Date.now() + result;
},
// 开始拖拽地图
UserEvent(data) {
//console.log(data);
this.$emit('onUserEvent',data);
}
}
}
</script>
<script module="editorModule" lang="renderjs">
export default {
data() {
return {
_editorIns:null
}
},
};
mounted() {
this._initEditor();
},
methods: {
_initEditor() {
if (typeof window.wangEditor === 'function') {
this._initialize();
} else {
const script = document.createElement('script');
script.onload = this._initialize;
script.src = "static/wangeditor/index.js";
document.head.appendChild(script);
const link = document.createElement('link');
link.href = "static/wangeditor/style.css";
link.rel = "stylesheet";
document.head.appendChild(link);
}
},
// 创建地图
_initialize() {
const _this = this;
// 创建地图实例
const {createEditor} = window.wangEditor
const editor = createEditor({
selector: '#editor-container',
html: '',
config: {
placeholder: '',
maxLength:100,
hoverbarKeys:{
divider: {menuKeys: [],},
link: {menuKeys: [],},
image: {menuKeys: [],},
pre: {menuKeys: [],},
table: {menuKeys: [],},
text: {menuKeys: [],},
video: {menuKeys: [],},
},
onCreated(){
_this.$ownerInstance.callMethod('UserEvent',{
type:'ready'
});
},
//onDestroyed(){},
onFocus(){
_this.$ownerInstance.callMethod('UserEvent',{
type:'focus'
});
},
onBlur(){
_this.$ownerInstance.callMethod('UserEvent',{
type:'blur'
});
},
//onDestroyed(){},
onChange(editor) {
const html = editor.getHtml()
const text = editor.getText()
//console.log('editor content', html)
// 也可以同步到 <textarea>
_this.$ownerInstance.callMethod('UserEvent',{
type:'onChange',
html,
text,
});
},
}
});
editor.on('atevent',()=>{
_this.$ownerInstance.callMethod('UserEvent',{
type:'atevent'
});
})
this._editorIns = editor;
console.log(editor.insertText);
this._editorIns.insertText("text");
},
_insertText(text){
console.log('_insertText',text);
this._editorIns.insertText(text);
},
_insertNode(node){
this._editorIns.insertNode(node);
},
_insertImgEmoji(src){
this._insertNode({
type: 'image',
style:{width:'30px',height:'30px'},
src: src,
children: [{text: ''}],
});
},
_insertMention(data){
this._insertNode({
type: 'mention',
username: data.username,
userid: data.userid,
children: [{
text: ''
}],
});
},
_clear(){
this._editorIns.clear();
},
_delete(){
this._editorIns.deleteBackward();
},
_blur(){
this._editorIns.blur();
},
_focus(){
this._editorIns.focus();
},
_setHtml(html){
this._editorIns.setHtml(html);
},
_getText(){
return this._editorIns.getText();
},
_getHtml(){
return this._editorIns.getHtml();
},
_getSelectionPosition(){
return this._editorIns.getSelectionPosition();
},
_getParentNode(){
return this._editorIns.getParentNode();
},
// 通过监听call来调用渲染层方法
call(newValue, oldValue, ownerInstance, instance) {
if(!newValue){
return false;
}
if (this[newValue.name] && typeof this[newValue.name] === "function") {
this[newValue.name](newValue.data);
}
},
}
}
</script>
<style lang="scss" scoped>
.editor_wrap {
position: relative;
}
#editor2 {
background-color: #fff;
min-height: 30px;
max-height: 120px;
height: auto;
word-break: break-all;
}
::v-deep.ql-editor {
img {
vertical-align: sub !important;
}
p {
padding: 4px;
}
}
.canvas_container {
position: fixed;
bottom: -99px;
z-index: -100;
&_name {
max-width: 480rpx;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#atCanvas {
height: 20px;
}
.convas_container_name {
font-size: 16px !important;
.custom_editor {
::v-deep.w-e-text-container{
background: transparent;
[data-slate-editor]{
padding: 0;
}
p,
span{
height: 60rpx;
line-height: 60rpx;
}
p{
white-space: pre-wrap; /* 保留空格 */
margin: 0;
}
img{
}
span{
}
span[data-w-e-type="mention"]{
background-color: #ccc;
margin-right: 10px;
}
}
}
</style>