V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
xiaoyan2017
V2EX  ›  推广

electron-vue 仿微信聊天编辑器|表情|截图功能分享

  •  
  •   xiaoyan2017 · 2020-01-07 15:58:46 +08:00 · 2105 次点击
    这是一个创建于 945 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近一直在学习 electron,基于 Electron-vue 实现 div 可编辑 contenteditable 插入表情及截图功能。

    使用 electron-vue 脚手架工具

    https://github.com/SimulatedGREG/electron-vue

    为了避免使用 vue 手动建立起 electron 应用程序。electron-vue 充分利用 vue-cli 作为脚手架工具,加上拥有 vue-loader 的 webpack、electron-packager 或是 electron-builder,以及一些最常用的插件,如 vue-router、vuex 等等。

    如何实现微信客户端,在编辑器光标处插入表情?通常有以下两种实现思路:

    • 通过 input 或 textarea 实现,在文本框插入[笑脸]、(:12 表情符标签,展示的时候解析标签就行

    通过如下代码就行实现效果

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8">
            <title></title>
            <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
        </head>
        <body>
            <div class="container">
                <div class="row">
                    <div class="col col-sm-12">
                        <button class="btn btn-success" data-emoj="[笑脸]">[笑脸]</button>
                        <button class="btn btn-success" data-emoj="[奋斗]">[奋斗]</button>
                        <button class="btn btn-success" data-emoj="[:17]">[:17]</button>
                    </div>
                    <div class="col col-sm-12">
                        <textarea class="form-control" id="content" rows="10"></textarea>
                    </div>
                </div>
            </div>
         
            <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
            <script>
                (function ($) {
                    $.fn.extend({
                        insertEmojAtCaret: function (myValue) {
                            var $t = $(this)[0];
                            if (document.selection) {
                                this.focus();
                                sel = document.selection.createRange();
                                sel.text = myValue;
                                this.focus();
                            } else if ($t.selectionStart || $t.selectionStart == '0') {
                                var startPos = $t.selectionStart;
                                var endPos = $t.selectionEnd;
                                var scrollTop = $t.scrollTop;
                                $t.value = $t.value.substring(0, startPos) + myValue + $t.value.substring(endPos, $t.value.length);
                                this.focus();
                                $t.selectionStart = startPos + myValue.length;
                                $t.selectionEnd = startPos + myValue.length;
                                $t.scrollTop = scrollTop;
                            } else {
                                this.value += myValue;
                                this.focus();
                            }
                        }
                    });
                })(jQuery);
                     
                $("button").on("click", function() {
                    $("#content").insertEmojAtCaret($(this).attr("data-emoj"));
                });
            </script>
        </body>
    </html>
    

    可是上面方法只能插入表情符,而且还需要解析处理,有些麻烦,如果能通过直接插入表情图片实现就更好,如是就想到了 html5 中 div 有可编辑功能 contenteditable="true"

    • div 可编辑属性(设置 contenteditable="true" 实现)

    运用 div 模拟 设置 contenteditable="true" 实现富文本编辑器效果,这种方法可以在可编辑区插入表情图片,由于在 vue 中 div 不能绑定 v-model,需要另外处理。

    如上图:就是通过设置 div 的 contenteditable="true" 来实现的效果就比较完美。

    实现思路:

    新建一个可编辑组件,chatInput.vue ,通过监听数据变化并返回父组件。

    1、调用 chatInput.vue 子组件,并给其绑定 v-model

    <template>
        ...
        <chatInput ref="chatInput" v-model="editorText" />
    </template>
    
    ...
    
    export default {
        data () {
            return {
                editorText: '',
                
                ...
            }
        },
        ...
    }
    

    2、v-model 中传入的值 editorText 在子组件 prop 中获取并监听变化

    export default {
        props: {
            value: { type: String, default: '' }
        },
        data () {
            return {
                editorText: this.value,
                ...
            }
        },
        watch: {
            value() {
                ...
            }
        },
    }
    

    3、通过监听获取到的 prop 值,并将该值赋值给子组件中的 v-html 参数,双向绑定就实现

    vue 中,contenteditable=true 的组件作为文本输入框,在非手动输入值后, 如点击表情后,光标会丢失,可通过如下代码实现将光标定位到最后

    function setLastAtCaret(obj) {
    	console.log(obj)
    	console.log(window.getSelection)
    	console.log(document.selection)
    	if (window.getSelection) { //ie11 10 9 ff safari
    		obj.focus(); //解决 ff 不获取焦点无法定位问题
    		var range = window.getSelection(); //创建 range
    		range.selectAllChildren(obj); //range 选择 obj 下所有子内容
    		range.collapseToEnd(); //光标移至最后
    	} else if (document.selection) { //ie10 9 8 7 6 5
    		var range = document.selection.createRange(); //创建选择对象
    		//var range = document.body.createTextRange();
    		range.moveToElementText(obj); //range 定位到 obj
    		range.collapse(false); //光标移至最后
    		range.select();
    	}
    }
    

    上面这种方法,每次都是定位到输入最后,也不是完美,最后通过下面这种方法即可完美解决了 vue 中 div 可编辑框光标处插入表情效果

    /**
     * 光标处插入内容
     * @param html 需要插入的内容
     */
    insertHtmlAtCaret(html) {
    	let sel, range;
    	if(!this.$refs.editor.childNodes.length) {
    		this.$refs.editor.focus()
    	}
    	if (window.getSelection) {
    		// IE9 and non-IE
    		sel = window.getSelection();
    
    		if (sel.getRangeAt && sel.rangeCount) {
    			range = sel.getRangeAt(0);
    			range.deleteContents();
    			let el = document.createElement("div");
    			el.appendChild(html)
    			var frag = document.createDocumentFragment(), node, lastNode;
    			while ((node = el.firstChild)) {
    				lastNode = frag.appendChild(node);
    			}
    			range.insertNode(frag);
    			if (lastNode) {
    				range = range.cloneRange();
    				range.setStartAfter(lastNode);
    				range.collapse(true);
    				sel.removeAllRanges();
    				sel.addRange(range);
    			}
    		}
    	} else if (document.selection && document.selection.type != "Control") {
    		// IE < 9
    		document.selection.createRange().pasteHTML(html);
    	}
    	
    	this.handleInput()
    }
    

    新建 chatInput.vue 组件

    <!-- vue 实现 contenteditable 功能 -->
    
    <template>
        <div 
            ref="editor"
            class="editor"
            contenteditable="true"
            v-html="editorText"
            @input="handleInput"
            @focus="handleFocus"
            @blur="handleBlur">
        </div>
    </template>
    
    <script>
        export default {
            props: {
                value: { type: String, default: '' }
            },
            data () {
                return {
                    editorText: this.value,
                    isChange: true,
                }
            },
            watch: {
                value() {
                    if(this.isChange) {
                        this.editorText = this.value
                    }
                }
            },
            methods: {
                handleInput() {
                    this.$emit('input', this.$el.innerHTML)
                },
                // 清空编辑器
                handleClear() {
                    this.$refs.editor.innerHTML = ''
                    this.$refs.editor.focus()
                },
                
                // 获取焦点
                handleFocus() {
                    this.isChange = false
                    this.$emit('focusFn')
                },
                // 失去焦点
                handleBlur() {
                    this.isChange = true
                    this.$emit('blurFn')
                },
                
    
                /**
                 * 光标处插入内容
                 * @param html 需要插入的内容
                 */
                insertHtmlAtCaret(html) {
                    let sel, range;
                    if(!this.$refs.editor.childNodes.length) {
                        this.$refs.editor.focus()
                    }
                    if (window.getSelection) {
                        // IE9 and non-IE
                        sel = window.getSelection();
    
                        if (sel.getRangeAt && sel.rangeCount) {
                            range = sel.getRangeAt(0);
                            range.deleteContents();
                            let el = document.createElement("div");
                            el.appendChild(html)
                            var frag = document.createDocumentFragment(), node, lastNode;
                            while ((node = el.firstChild)) {
                                lastNode = frag.appendChild(node);
                            }
                            range.insertNode(frag);
                            if (lastNode) {
                                range = range.cloneRange();
                                range.setStartAfter(lastNode);
                                range.collapse(true);
                                sel.removeAllRanges();
                                sel.addRange(range);
                            }
                        }
                    } else if (document.selection && document.selection.type != "Control") {
                        // IE < 9
                        document.selection.createRange().pasteHTML(html);
                    }
                    
                    this.handleInput()
                }
            }
        }
    </script>
    
    <style>
    
    </style>
    

    electron-vue 实现截图功能

    nodejs 中通过的 execFile 方法去执行 exe 文件,exe 调用同级目录下的 dll,调出截图工具

    将 exe 和 dll 文件打包到 static 目录下,通过绝对路径去执行。exe 和 dll 可网上找。

    screenShot() {
    	return new Promise((resolve) => {
    		const { execFile } = require('child_process')
    		var screenWin = execFile('./static/PrintScr.exe')
    		screenWin.on('exit', function(code) {
    			let pngs = require('electron').clipboard.readImage().toPNG()
    			let imgData = new Buffer.from(pngs, 'base64')
    			let imgs = 'data:image/png;base64,' + btoa(new Uint8Array(imgData).reduce((data, byte) => data + String.fromCharCode(byte), ''))
    			resolve(imgs)
    		})
    	})
    },
    

    okay~~ 以上就是基于 electron+vue 实现聊天编辑器插入表情、截图小功能分享。

    最后附上 uniapp 案例项目:uniapp+vue+nvue 仿抖音短视频|直播 app

    作者:xiaoyan2017
    链接: https://juejin.im/post/5e141ffc5188253a57049a63
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    3 条回复    2020-01-07 16:36:23 +08:00
    haocity
        1
    haocity  
       2020-01-07 16:30:08 +08:00
    不错 感谢分享 昨天才遇到的问题 今天就解决了
    聊天框插入表情光标移动到最后
    本来是这样写的
    ```javascript
    let range = window.getSelection().getRangeAt(0)
    range.setStartAfter(that.$refs.editor.lastChild);
    range.setEndAfter(that.$refs.editor.lastChild)
    ```
    结果 android 上面没问题 ios 光标雷打不动
    SSREX
        2
    SSREX  
       2020-01-07 16:30:12 +08:00 via iPhone
    大佬!
    Lucoie
        3
    Lucoie  
       2020-01-07 16:36:23 +08:00
    有手机端模拟的吗?
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2185 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 35ms · UTC 16:17 · PVG 00:17 · LAX 09:17 · JFK 12:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.