vue 编辑器+使用场景+问题解决

发布时间 2023-12-03 20:53:45作者: 朱呀朱~

vue 编辑器组件

  • 添加依赖

    "dependencies": {
        "@codemirror/autocomplete": "^6.4.2",
        "@codemirror/commands": "^6.2.1",
        "@codemirror/lang-javascript": "^6.0.2",
        "@codemirror/lang-sql": "^6.0.0",
        "@codemirror/language": "^6.6.0",
        "@codemirror/lint": "^6.2.0",
        "@codemirror/search": "^6.2.3",
        "@codemirror/state": "^6.2.0",
        "@codemirror/view": "^6.11.2",
        "codemirror": "^5.65.15"
    },
    
  • 导入组件:

    const scCodeEditor = defineAsyncComponent(() => import("../codeEdit.vue"))
    
    export default {
      components: {
        scCodeEditor
      },
    // ......
    
  • 使用标签:<sc-code-editor></sc-code-editor>

使用场景

<el-dialog v-model="showDatasetDialog" title="数据编辑">
    <sc-code-editor v-model="editingData" :height="400"></sc-code-editor>
    <div style="display: flex; justify-content: center; align-items: center;">
        <span style="margin: auto">
            <el-button type="primary" @click="saveDataset" size="default">确认</el-button>
            <el-button @click="cancelDatasetEdit" size="default">取消</el-button>
        </span>
    </div>
</el-dialog>
  • 是在下拉的标签 <el-collapse-item>v-for 循环显示多条数据中,内含一个触发弹窗的按钮:

    <el-form-item label="y轴数据:">
        <el-button @click="editDataset(seriesItem)" size="default">编辑</el-button>
    </el-form-item>
    
  • 编辑按钮

    // 编辑数据
    editDataset(seriesItem) {
      this.showDatasetDialog = true; // 打开弹窗
      nextTick(() => {
        this.editingData = JSON.stringify(seriesItem.data);
        this.newSeriesItem = seriesItem
      })
    },
    
  • 保存按钮

    // 保存的确定按钮
    saveDataset() {
      this.newSeriesItem.data = JSON.parse(this.editingData);
      this.showDatasetDialog = false;
      this.editingData = '';
    },
    
    
  • 取消按钮

    // 取消按钮
    cancelDatasetEdit() {
      this.editingData = '';
      this.showDatasetDialog = false;
    },
    

出现问题并解决

  1. 传到编辑器中的文本不对,不是想要的样式

    • 看编辑器组件内 props 接收的是什么类型,JSON.stringify 返回的是 JSON 字符串,而 JSON.parse 返回的是一个对象,要在合适的地方恰当转换
    • this.editingData 在定义时置空不是 null,而应该是 “ ”
  2. 编辑器弹窗点击右上角的叉号关闭不是立刻关,更像是点一下叉号连着关了多个弹窗

    • 这个问题可能与 v-model 绑定和 v-for 循环结合使用时的共享状态有关,一个方法是尝试为每个弹窗使用不同的状态,而不是在循环中共享相同的 showDatasetDialog
    • 另一个简单的方式也有效,就是尽量把弹窗拿出来放到外层,不要放循环里
  3. 没加 nextTick() 的时候,打开编辑器后没有数据,点击一下弹窗内部获取焦点后才会显示数据:

    editDataset(seriesItem) {
      this.showDatasetDialog = true;
      this.editingData = JSON.stringify(seriesItem.data);
      this.newSeriesItem = seriesItem
    },
    
    • 这个问题可能是由于在 editDataset() 方法中,弹窗被打开的时候,editingData 可能还没有被正确地设置,导致初始时是空白的(在 Vue 中,数据更新是异步的,因此在 editDataset 中设置 this.editingData 后,如果直接打开弹窗,可能会在弹窗中渲染时发现数据还没有更新),解决方案就是最开始展示的加上 nextTick()
    • 使用 nextTick 的目的是等待当前数据更新完成后再执行代码块(可以理解为执行完 nextTick 里的语句后才会执行此方法里 nextTick 之外的语句),确保了 editingData 已经包含了正确的数据,然后再打开弹窗,就不再出现空白的问题了

第三方编辑器组件

  • codeEdit.vue:

    <template>
        <div class="sc-code-editor" :style="{ 'height': _height }" style="z-index: 10000">
            <textarea ref="textarea" v-model="contentValue"></textarea>
        </div>
    </template>
    
    <script>
    import { markRaw } from "vue";
    
    //框架
    import CodeMirror from "codemirror";
    import "codemirror/lib/codemirror.css";
    
    //主题
    import "codemirror/theme/idea.css";
    import "codemirror/theme/darcula.css";
    
    //功能
    import "codemirror/addon/selection/active-line";
    
    //语言
    import "codemirror/mode/javascript/javascript";
    import "codemirror/mode/sql/sql";
    
    export default {
        props: {
            modelValue: {
                type: String,
                default: ""
            },
            mode: {
                type: String,
                default: "javascript"
            },
            height: {
                type: [String, Number],
                default: 300
            },
            options: {
                type: Object,
                default: () => {}
            },
            theme: {
                type: String,
                default: "idea"
            },
            readOnly: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
                contentValue: this.modelValue,
                coder: null,
                opt: {
                    theme: this.theme, //主题
                    styleActiveLine: true, //高亮当前行
                    lineNumbers: true, //行号
                    lineWrapping: false, //自动换行
                    tabSize: 4, //Tab缩进
                    indentUnit: 4, //缩进单位
                    indentWithTabs: true, //自动缩进
                    mode: this.mode, //语言
                    readOnly: this.readOnly, //只读
                    autoFormatOnLoad: true,
                    ...this.options
                }
            };
        },
        computed: {
            _height() {
                return Number(this.height) ? Number(this.height) + "px" : this.height;
            }
        },
        watch: {
            modelValue(val) {
                this.contentValue = val;
                if (val !== this.coder.getValue()) {
                    this.coder.setValue(val);
                }
            }
        },
        mounted() {
            this.$nextTick(() => {
                this.init();
            });
    
            //获取挂载的所有modes
            //console.log(CodeMirror.modes)
        },
        methods: {
            init() {
                this.coder = markRaw(CodeMirror.fromTextArea(this.$refs.textarea, this.opt));
                this.coder.on("change", coder => {
                    this.contentValue = coder.getValue();
                    this.$emit("update:modelValue", this.contentValue);
                });
            },
            formatStrInJson(strValue) {
                return JSON.stringify(JSON.parse(strValue), null, 4);
            }
        }
    };
    </script>
    
    <style scoped>
    .sc-code-editor {
        font-size: 14px;
        border: 1px solid #ddd;
        line-height: 150%;
    }
    .sc-code-editor:deep(.CodeMirror) {
        height: 100%;
    }
    </style>
    
  • 小提一嘴:在 Vue 中,数据更新是异步的,即当你修改了 Vue 实例的数据时,Vue 并不会立即应用这个变化,而是将变化放入一个队列中,然后在一个事件循环周期内异步地执行这些变化,最终更新视图。这种机制可以提高性能,避免不必要的重复渲染。

    • 在这种情况下,如果你在更新数据后立即访问这个数据,可能会得到之前的旧值,因为实际的数据更新是异步执行的。所以,上述如果在数据更新后立即打开弹窗,可能会在弹窗中渲染时发现数据还没有被正确地更新。
    • 所以通过使用 nextTick,你确保在其内部的回调(在其中更新 this.editingDatathis.newSeriesItem)将在下一个 DOM 更新周期之后执行。这确保了当你访问和设置 this.editingData 时,DOM 已经以最新的数据更新,防止在对话框中看到空白数据的问题。