vue2+vue-quill-editor

351 阅读9分钟

编辑器的功能,主要的需求是。编辑器做标注功能。由于是vue2项目,所以用的编辑器是 vue-quill-editor,这个插件不进行维护了,但是正常使用还是可以的,主要是会涉及到一个报错 [Deprecation] Listener added for a ‘DOMNodeInserted’ mutation event. Support for this event type has been removed, and this event will no longer be fired. See for more information. 这个就是quill的插件报错,去找到 _this.domNode.addEventListener('DOMNodeInserted', function () { }); 修改为:new MutationObserver(() => {}).observe(_this.domNode, { childList: true }); 这样报错就没有了。

直接上代码吧:

npm install vue-quill-editor --save

main.js
import Vue from 'vue'
import App from './App.vue'

import VueQuillEditor from 'vue-quill-editor'
// require styles
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
Vue.use(VueQuillEditor, /* { default global options } */)
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
// 组件中使用
<template>
    <div class="container">
        <quill-editor
                            ref="myQuillEditor"
                            v-model="content"
                            :options="editorOption"
                            @blur="onEditorBlur($event)"
                            @focus="onEditorFocus($event)"
                            @change="onEditorChange($event)"
                            style="height: 600px"
                        ></quill-editor>
        </el-row>
        <el-dialog :visible.sync="dialogVisible" title="添加标注" :before-close="cancelAnnotation">
            <el-form ref="markForm" :model="markForm" label-width="100px">
                <el-form-item label="标注词:">
                    <el-input disabled v-model="markForm.keyworkd"></el-input>
                </el-form-item>
                <el-form-item label="建议词:">
                    <el-input v-model="markForm.suggest"></el-input>
                </el-form-item>
                <el-form-item label="问题描述:">
                    <el-input type="textarea" v-model="markForm.problemDescription"></el-input>
                </el-form-item>
                <!-- 其他表单项 -->
            </el-form>
            <span slot="footer" class="dialog-footer">
    <el-button size="mini" @click="cancelAnnotation">取消</el-button>
    <el-button size="mini" type="primary" @click="addAnnotation">确定</el-button>
  </span>
        </el-dialog>
        <el-button
            v-if="isAddButton"
            class="add-button"
            ref="addButton"
            type="primary"
            @click="addButton"
            size="mini"
        >添加标注</el-button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            content: '<h3 style="text-align: start;">故事背景</h3><p style="text-align: start;">在遥远的艾斯特拉王国,时间并非一成不变,而是由古老传说中的“时光之钥”所掌控。这把神秘的钥匙,剧说是创世之初遗落的碎片,拥有逆转时光、重塑历史的无上力量。然而,这股力量若落入邪恶之手,将引发不可预知的灾难。数千年来,时光之钥被分为数块碎片,散落于世界的各个角落,守护着世界的平衡与秩序。</p><h3 style="text-align: start;">主要人物</h3><ul><li style="text-align: start;">艾莉娅:一位拥有敏锐直觉与温柔心灵的年轻女学者,对历史与神话充满无限好奇。她欧染间发现了一本古老的手稿,里面记载了时光之钥的传说,从此踏上了寻找钥匙碎片的征途。</li><li style="text-align: start;">莱恩:前王室骑士团成员,因一个误会而被流放。他武艺高强,性格孤傲但内心深处渴望救赎。艾莉娅的出现让他重新找到了战斗的意义,决定加入她的行列,保护她免受一切威胁。</li><li style="text-align: start;">凯尔:来自异世界的旅人,拥有操控元素的神奇能力。他因一次意外穿越到艾斯特拉,被艾莉娅和莱恩的勇气与决心所吸引,决定帮助他们寻找时光之钥碎片,同时寻找回到自己世界的方法。</li></ul><h3 style="text-align: start;">故事梗概</h3><h4 style="text-align: start;">第一章:启程与谜团</h4><p style="text-indent: 2em; text-align: start;">艾莉娅在一次图书馆的深夜探险中,意外发现了一哥尘封的手稿,上面详细记载了时光之钥的传说及其碎片的线索。意识到这份力量的重要性,她决定踏上寻找碎片的旅程。在旅途中,她先后遇到了莱恩和凯尔,三人因各自的信念与目的而结伴。他们首先来到了古老的遗迹——遗忘之城,那里隐藏着第一块时光之钥碎片的线索。通过解开一系列复杂的机关与谜题,他们成功获得了第一块碎片,但也因此引起了暗中窥视的邪恶势力的注意。</p><h4 style="text-align: start;">第二章:挑战与试炼</h4><p style="text-indent: 2em; text-align: start;">随着旅程的深入,三人遭遇了越来越多的挑战与危险。他们不仅要面对自然界的考研,如穿越迷雾森林、攀登险峻的山峰,还要应对来自邪恶势力的追击与阻挠。在一次激烈的战斗中,莱恩为了保护艾莉娅身受重伤,这让他深刻反思自己过去的错误与选择。而凯尔则利用自己的元素之力,在关键时刻扭转战局,展现了非凡的能力与智慧。通过这些经历,三人之间的友谊与信任日益加深,彼此成为了不可分割的伙伴。</p><h4 style="text-align: start;">第三章:真相与抉择</h4><p style="text-align: start;"> &nbsp; &nbsp; &nbsp; 在接近最后一块时光之钥碎片的过程中,他们逐渐揭开了时光之钥背后隐藏的真相。原来,时光之钥的力量并非无限制地操控时间,而是引导人们正视历史、珍惜现在、展望未来。邪恶势力之所以渴望得到它,是为了改变对他们不利的过去,从而逃避应有的惩罚与责任。面对这一残酷的真相,艾莉娅、莱恩和凯尔必须做出抉择:是继续追寻力量,还是用它来维护世界的平衡与正义?</p><h4 style="text-align: start;">第四章:决战与牺牲</h4><p style="text-align: start;">在最终的决战中,三人与邪恶势力展开了激烈的交锋。邪恶势力的首领试图利用时光之钥的力量改变历史,但艾莉娅等人坚决反对。在一场惊心动魄的对决后,凯尔利用自己的特殊能力,将时光之钥的碎片重新组合,释放出了一股强大的力量。然而,这股力量并非用于战斗,而是用来揭示真相、唤醒人们的良知与勇气。最终,邪恶势力在众人的联合反抗下彻底覆灭,时光之钥也化为了点点星光,融入了大地之中,象征着时间的不可逆转与生命的无限可能。</p><h4 style="text-align: start;">第五章:和平与新生</h4><p style="text-align: start;">战争结束后,艾斯特拉王国迎来了久违的和平与繁荣。艾莉娅、莱恩和凯尔也因为他们的英勇与智慧而被誉为英雄。但对他们而言,更重要的是这段旅程中收获的友谊、成长与自我认知。艾莉娅继续她的学术研究,致力于传承与发扬古代文化的精髓;莱恩则选择留在王城,用自己的剑与勇气保护这片土地与人民;而凯尔则在找到返回自己世界的方法后,带着对艾斯特拉的深深眷恋与祝福离开了。</p><p style="text-align: start;">《时光之钥》不仅是一部关于冒险与战痘的故事,更是一次心灵的洗礼与成长的旅程。它教会我们珍惜时间、勇于探索、团结合作、坚守正义,并在反思与成长中不断前行。在这个充满奇迹与未知的世界里,每个人都是自己命运的主宰者,而真正的力量则源自内心的信念与减持。</p>',
            programIdeologicalConnotation: '这是思想内涵',
            dialogVisible: false,
            isAddButton: false,
            quillReady: false, // 新增标志位,表示 Quill 编辑器是否已准备好
            editorOption: {
                // 占位配置
                placeholder: '',
                modules: {
                    toolbar:{
                        container: [
                            ['bold', 'italic', 'underline', 'strike'], //加粗,斜体,下划线,删除线
                            [{ header: 1 }, { header: 2 }], // 标题
                            [{ list: 'ordered' }, { list: 'bullet' }], // 列表
                            [{ indent: '-1' }, { indent: '+1' }], // 缩进
                            [{ color: [] }, { background: [] }], // 字体颜色,字体背景颜色
                            [{ align: [] }], // 对齐方式
                            ['clean'], // 清除文本格式
                        ]
                    }
                }
            },
            psoAmendmentRecords: [
                {
                    "id": 1,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 2,
                    "outPoint": 4,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "背景",
                },
                {
                    "id": 2,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 6,
                    "outPoint": 8,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "遥远",
                },
                {
                    "id": 3,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 220,
                    "outPoint": 222,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "此踏",
                },
                {
                    "id": 4,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 300,
                    "outPoint": 302,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "她的",
                },
                {
                    "id": 5,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 500,
                    "outPoint": 502,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "遇到",
                },
                {
                    "id": 6,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 1000,
                    "outPoint": 1002,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "决战",
                },
                {
                    "id": 7,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 2001,
                    "outPoint": 2003,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "旅途",
                },
                {
                    "id": 8,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 2501,
                    "outPoint": 2503,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "牺牲",
                },
                {
                    "id": 9,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 2799,
                    "outPoint": 2802,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "艾丽娅",
                }
            ],
            psoAiaResult: [
                {
                    "id": 1,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 2,
                    "outPoint": 4,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "背景",
                },
                {
                    "id": 2,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 10,
                    "outPoint": 12,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "背景",
                },
                {
                    "id": 3,
                    "contentId": 1,
                    "videoId": null,
                    "inPoint": 2600,
                    "outPoint": 2602,
                    "problemDescription": "用词不当",
                    "imageUrl": null,
                    "suggest": "测试",
                    "auditor": "zenglingqi",
                    "auditType": 1,
                    "auditStage": 1,
                    "deleted": 0,
                    "createTime":"2024-09-04 10:17:15",
                    "updateTime":"2024-09-04 10:17:15",
                    "keyworkd": "背景",
                },

            ], // 故事梗概机审结果
            picAmendmentRecords: [], // 思想内涵修改建议
            picAiaResult: [], // 思想内涵机审结果
            activeName: 'first',
            storyActiveName: 'first',
            connotationActiveName: 'third',
            markForm: {
                keyworkd: '', // 关键词
                suggest: '', // 建议词
                problemDescription: '', // 问题描述
                inPoint: '', // 关键词索引开始位置
                outPoint: '' // 关键词索引结束位置
            },
        };
    },
    created () {
        // 选中取消时, 隐藏按钮
        console.log('-------------123')
        // document.addEventListener('selectionchange', () => {
        //     if(this.$refs.myQuillEditor === undefined) {
        //         this.isAddButton = false
        //     } else {
        //         let quill = null;
        //         if(this.activeName === 'first') {
        //             // 故事梗概
        //             quill = this.$refs.myQuillEditor.quill
        //         } else if(this.activeName === 'second') {
        //             quill = this.$refs.connotationEditor.quill
        //         }
        //         const range = quill.getSelection()
        //         if (this.isAddButton && range.length === 0) {
        //             this.isAddButton = false
        //         }
        //     }
        // })
    },
    mounted() {
        this.setEditorContent();
        console.log('mounted-----')
        // this.$nextTick(() => {
        //     this.quillReady = true; // 在下一个 tick 设置为 true,确保 Quill 实例已挂载
        //     // this.$refs.myQuillEditor.quill.root.addEventListener('mouseup', this.mouseup);
        //     this.$refs.myQuillEditor.quill.root.addEventListener('mouseup', this.mouseup);
        //     this.$refs.connotationEditor.quill.root.addEventListener('mouseup', this.mouseup);
        // });
    },
    beforeDestroy() {
        if (this.quillReady && this.$refs.myQuillEditor) {
            this.$refs.myQuillEditor.quill.root.removeEventListener('mouseup', this.mouseup);
        }
        if (this.quillReady && this.$refs.connotationEditor) {
            this.$refs.connotationEditor.quill.root.removeEventListener('mouseup', this.mouseup);
        }
    },
    methods: {
        onEditorBlur(editor) {
            console.log('editor blur!', editor)
        },
        onEditorFocus(editor) {
            console.log('editor focus!', editor)
        },
        onEditorChange({ quill, html, text }) {
            // 这里可以获取到编辑器的内容、HTML和纯文本
            // quill 是编辑器实例
            // html 是编辑器内容的HTML
            // text 是编辑器内容的纯文本
            // console.log('editor content changed!', quill, html, text);
            // 根据需要处理你的逻辑
            // 例如,检查是否需要显示添加按钮
            const range = quill.getSelection();
            if (range && range.length === 0) {
                this.isAddButton = false;
            }
        },
        // 根据修改建议去设置标注的
        setEditorContent() {
            let quill = null;
            if(this.activeName === 'first') {
                // 故事梗概
                quill = this.$refs.myQuillEditor.quill
            } else if(this.activeName === 'second') {
                quill = this.$refs.connotationEditor.quill
            }
            this.psoAiaResult.forEach((record => {
                const startIndex = record.inPoint
                const selectLength = record.keyworkd.length;
                // quill.setSelection(startIndex, selectLength)
                quill.formatText(startIndex, selectLength, {
                    'bold': true,
                    'color': 'red'
                });
            }))
            this.psoAmendmentRecords.forEach((record => {
                const startIndex = record.inPoint
                const selectLength = record.keyworkd.length;
                // quill.setSelection(startIndex, selectLength)
                quill.formatText(startIndex, selectLength, {
                    'bold': true,
                    'color': 'red'
                });
            }))
        },
        // mouseup (e) {
        //     if (!this.quillReady) return; // 如果 Quill 编辑器未准备好,直接返回
        //     // console.log('鼠标事件')
        //     // 未选中
        //     let quill = null;
        //     if(this.activeName === 'first') {
        //         quill = this.$refs.myQuillEditor.quill;
        //     } else {
        //         quill = this.$refs.connotationEditor.quill;
        //     }
        //     const range = quill.getSelection();
        //     if(range.length === 0) return
        //     // 插入交互
        //     this.insetAffix(e)
        // },
        // insetAffix(e) {
        //     this.isAddButton = true
        //     this.$nextTick(() => {
        //         const { clientX, clientY } = e
        //         const addButton = this.$refs.addButton.$el;
        //         const offset = 5
        //         addButton.style.cssText = `top: ${clientY + offset}px; left: ${clientX + offset}px; z-index: 10`
        //     })
        // },
        // addButton() {
        //     if (!this.quillReady) return; // 如果 Quill 编辑器未准备好,直接返回
        //     // 添加标注弹出层
        //     this.dialogVisible = true
        //     this.markForm = {
        //         keyworkd: '', // 关键词
        //         suggest: '', // 建议词
        //         problemDescription: '', // 问题描述
        //         inPoint: '', // 关键词索引开始位置
        //         outPoint: '' // 关键词的索引结束位置
        //     }
        //     // 需要拿到选中的文字
        //     let quill = null;
        //     if(this.activeName === 'first') {
        //         quill = this.$refs.myQuillEditor.quill;
        //     } else {
        //         quill = this.$refs.connotationEditor.quill;
        //     }
        //     // const quill = this.$refs.myQuillEditor.quill
        //     const range = quill.getSelection()
        //     const text = quill.getText(range.index, range.length); // 选中的文字
        //     console.log(range,text)
        //     this.markForm.keyworkd = text;
        //     this.markForm.inPoint = range.index
        //     this.markForm.outPoint = range.index + range.length
        // },
        // addAnnotation() {
        //     this.dialogVisible = false;
        //     // 此时应该取消选区
        //     let quill = null;
        //     if(this.activeName === 'first') {
        //         quill = this.$refs.myQuillEditor.quill;
        //     } else {
        //         quill = this.$refs.connotationEditor.quill;
        //     }
        //     // const quill = this.$refs.myQuillEditor.quill
        //     const startIndex = this.markForm.inPoint
        //     const selectLength = this.markForm.keyworkd.length;
        //     // quill.setSelection(startIndex, selectLength)
        //     quill.formatText(startIndex, selectLength, {
        //         'bold': true,
        //         'color': 'red'
        //     });
        //     this.isAddButton = false
        // },
        // cancelAnnotation() {
        //     this.dialogVisible = false
        //     this.isAddButton = false
        // },
        // 定位
        storyOutlineLocation(row, event, column) {
            // console.log('row',row)
            let quill = null;
            if(this.activeName === 'first') {
                quill = this.$refs.myQuillEditor.quill;
            } else {
                quill = this.$refs.connotationEditor.quill;
            }
            // const quill = this.$refs.myQuillEditor.quill
            // 设置光标位置到指定位置
            // quill.setSelection(row.inPoint, row.keyworkd.length);
            // 获取当前光标位置相对于编辑器容器的位置
            const bounds = quill.getBounds(row.inPoint);

            // 获取编辑器的滚动容器
            const scrollContainer = quill.scrollingContainer;

            // 计算需要滚动的偏移量
            const scrollTop = scrollContainer.scrollTop;
            const scrollLeft = scrollContainer.scrollLeft;

            // 考虑编辑器的滚动容器和内容的位置
            const padding = 10; // 偏移量,可根据需要调整
            const targetTop = bounds.top + scrollTop - padding;
            const targetLeft = bounds.left + scrollLeft;

            // 滚动到指定位置
            scrollContainer.scrollTop = targetTop;
            scrollContainer.scrollLeft = targetLeft;
        },
        editClick(row) {
            console.log('编辑人审建议',row)
            this.dialogVisible = true;
            this.markForm.keyworkd = row.keyworkd;
            this.markForm.inPoint = row.inPoint;
            this.markForm.suggest = row.suggest;
            this.markForm.problemDescription = row.problemDescription;
        }
    },
    computed: {
        editor() {
            if(this.activeName === 'first') {
                return this.$refs.myQuillEditor.quill
            } else {
                return this.$refs.connotationEditor.quill
            }
        }
    },
}
</script>
<style scoped>
.ql-editor {
    height: 300px !important;
}
.add-button {
    position: fixed;
    top: 0;
    left: 0;
    z-index: -1;
}
.container {
    display: flex;
}
.annotation-button {
    position: absolute;
    padding: 5px 10px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    z-index: 1000; /* 确保按钮在其他内容之上 */
}
</style>