当前位置: 首页 > article >正文

vue2中,codemirror编辑器的使用

交互说明 

在编辑器中输入{时,会自动弹出选项弹窗,然后可以选值插入。

代码

父组件

<variable-editorv-model="content":variables="variables"placeholder="请输入模板内容..."@blur="handleBlur"
/>data() {return {content: "这是一个示例 {user.name}",variables: [{id: "user",label: "user",type: "object",children: [{ id: "user.name", label: "name", type: "string" },{ id: "user.age", label: "age", type: "number" },],},{id: "items",label: "items",type: "array<object>",children: [{ id: "items.title", label: "title", type: "string" },{ id: "items.price", label: "price", type: "number" },],},],};},handleBlur(val) {console.log("编辑器内容已更新:", val);
},

子组件 

<template><div class="variable-editor"><div ref="editorRef" class="editor-container"></div><el-popoverv-if="variables && variables.length > 0"ref="popover"placement="left-start":value="popoverOpen":visible-arrow="false"trigger="manual"@after-enter="handleAfterOpen"><divclass="tree-wrap my-variable-popover"tabindex="-1"@keydown="handleKeyDown"@keydown.capture="handleKeyDownCapture"ref="treeRef"><el-tree:data="variables":props="defaultProps"default-expand-all:highlight-current="true":current-node-key="selectedKeys[0]"@current-change="handleCurrentChange"@node-click="handleVariableInsert"ref="tree"node-key="id"><div slot-scope="{ node, data }" class="flex-row-center"><i v-if="getTypeIcon(data)" :class="getTypeIcon(data)"></i><span class="ml-1">{{ node.label }}</span></div></el-tree></div><span slot="reference" ref="anchorRef" class="anchor-point"></span></el-popover></div>
</template><script>
import {EditorView,ViewPlugin,placeholder,Decoration,keymap,
} from "@codemirror/view";
import { EditorState, RangeSetBuilder, StateEffect } from "@codemirror/state";
import { defaultKeymap, insertNewlineAndIndent } from "@codemirror/commands";// 扁平化树结构
const flattenTree = (nodes, result = []) => {for (const node of nodes) {result.push({ key: node.id, title: node.label });if (node.children) {flattenTree(node.children, result);}}return result;
};export default {name: "VariableEditor",props: {value: {type: String,default: "",},variables: {type: Array,default: () => [],},placeholder: {type: String,default: "请输入内容...",},},data() {return {popoverOpen: false,selectedKeys: [],editorView: null,lastCursorPos: null,flattenedTree: [],defaultProps: {children: "children",label: "label",},// 类型图标映射typeIcons: {string: "el-icon-document",number: "el-icon-tickets",boolean: "el-icon-switch-button",object: "el-icon-folder","array<object>": "el-icon-collection",},};},computed: {currentIndex() {return this.flattenedTree.findIndex((node) => node.key === this.selectedKeys[0]);},},mounted() {this.flattenedTree = flattenTree(this.variables);this.initEditor();},beforeDestroy() {if (this.editorView) {this.editorView.destroy();}},watch: {variables: {handler(newVal) {this.flattenedTree = flattenTree(newVal);if (this.editorView) {// 重新配置编辑器以更新插件this.editorView.dispatch({effects: StateEffect.reconfigure.of(this.createExtensions()),});}},deep: true,},value(newVal) {if (this.editorView && newVal !== this.editorView.state.doc.toString()) {this.editorView.dispatch({changes: {from: 0,to: this.editorView.state.doc.length,insert: newVal,},});}},popoverOpen(val) {if (val && this.flattenedTree.length > 0) {this.selectedKeys = [this.flattenedTree[0].key];this.$nextTick(() => {if (this.$refs.tree) {this.$refs.tree.setCurrentKey(this.selectedKeys[0]);}});}},},methods: {getTypeIcon(data) {return this.typeIcons[data.type] || this.typeIcons.string;},initEditor() {if (!this.$refs.editorRef) return;this.editorView = new EditorView({doc: this.value,parent: this.$refs.editorRef,extensions: this.createExtensions(),});// 添加失焦事件this.$refs.editorRef.addEventListener("blur", this.onEditorBlur);},createExtensions() {return [placeholder(this.placeholder || "请输入内容..."),EditorView.editable.of(true),EditorView.lineWrapping,keymap.of([...defaultKeymap,{ key: "Enter", run: insertNewlineAndIndent },]),EditorState.languageData.of(() => {return [{ autocomplete: () => [] }];}),this.createUpdateListener(),this.createVariablePlugin(),this.createInterpolationPlugin(this.variables),];},createUpdateListener() {return EditorView.updateListener.of((update) => {if (update.docChanged) {// const content = update.state.doc.toString();// 不要在每次更改时都触发,而是在失焦时触发}});},createVariablePlugin() {const self = this;return ViewPlugin.fromClass(class {constructor(view) {this.view = view;}update(update) {if (update.docChanged || update.selectionSet) {const pos = update.state.selection.main.head;const doc = update.state.doc.toString();// 只有当光标位置真正变化时才更新if (self.lastCursorPos !== pos) {self.lastCursorPos = pos;// 延迟更新 Popover 位置setTimeout(() => {self.$refs.popover &&self.$refs.popover.$el &&self.$refs.popover.updatePopper();}, 10);}// 1. 正则查找所有的 {xxx}const regex = /\{(.*?)\}/g;let match;let inInterpolation = false;while ((match = regex.exec(doc)) !== null) {const start = match.index;const end = start + match[0].length;if (pos > start && pos < end) {// 光标在插值表达式内inInterpolation = true;setTimeout(() => {const coords = this.view.coordsAtPos(pos);const editorRect = this.view.dom.getBoundingClientRect();if (coords) {self.$refs.anchorRef.style.position = "absolute";self.$refs.anchorRef.style.left = `${coords.left - editorRect.left - 10}px`;self.$refs.anchorRef.style.top = `${coords.top - editorRect.top}px`;self.$refs.anchorRef.dataset.start = start;self.$refs.anchorRef.dataset.end = end;self.popoverOpen = true;}}, 0);break;}}if (!inInterpolation) {// 检测输入 { 的情况const prev = update.state.sliceDoc(pos - 1, pos);if (prev === "{") {setTimeout(() => {const coords = this.view.coordsAtPos(pos);const editorRect = this.view.dom.getBoundingClientRect();if (coords) {self.$refs.anchorRef.style.position = "absolute";self.$refs.anchorRef.style.left = `${coords.left - editorRect.left - 10}px`;self.$refs.anchorRef.style.top = `${coords.top - editorRect.top}px`;self.$refs.anchorRef.dataset.start = pos;self.$refs.anchorRef.dataset.end = pos;self.popoverOpen = true;}}, 0);} else {self.popoverOpen = false;}}}}});},createInterpolationPlugin(variables) {const self = this;return ViewPlugin.fromClass(class {constructor(view) {this.decorations = this.buildDecorations(view);}update(update) {if (update.docChanged || update.viewportChanged) {this.decorations = this.buildDecorations(update.view);}}buildDecorations(view) {const builder = new RangeSetBuilder();const doc = view.state.doc;const text = doc.toString();const regex = /\{(.*?)\}/g;let match;while ((match = regex.exec(text)) !== null) {const [full, expr] = match;const start = match.index;const end = start + full.length;const isValid = self.validatePath(variables, expr.trim());const deco = Decoration.mark({class: isValid? "cm-decoration-interpolation-valid": "cm-decoration-interpolation-invalid",});builder.add(start, end, deco);}return builder.finish();}},{decorations: (v) => v.decorations,});},validatePath(schema, rawPath) {const segments = rawPath.replace(/\[(\d+)\]/g, "[$1]").split(".");// 递归匹配function match(nodes, index) {if (index >= segments.length) return true;const currentKey = segments[index];for (const node of nodes) {const { label: title, type, children } = node;// 匹配数组字段,如 abc[0]if (/\[\d+\]$/.test(currentKey)) {const name = currentKey.replace(/\[\d+\]$/, "");if (title === name && type === "array<object>" && children) {return match(children, index + 1);}}// 匹配普通字段if (title === currentKey) {if ((type === "object" || type === "array<object>") && children) {return match(children, index + 1);}// 如果不是object类型,且已经是最后一个字段return index === segments.length - 1;}}return false;}return match(schema, 0);},handleAfterOpen() {if (this.$refs.treeRef) {this.$refs.treeRef.focus();}},handleCurrentChange(data) {if (data) {this.selectedKeys = [data.id];}},handleVariableInsert(data) {const key = data.id;this.selectedKeys = [key];const view = this.editorView;if (!view) return;const state = view.state;const pos = state.selection.main.head;const doc = state.doc.toString();let insertText = `{${key}}`;let targetFrom = pos;let targetTo = pos;let foundInBraces = false;// 检查光标是否在 {...} 内部const regex = /\{[^}]*\}/g;let match;while ((match = regex.exec(doc)) !== null) {const [full] = match;const start = match.index;const end = start + full.length;if (pos > start && pos < end) {targetFrom = start;targetTo = end;foundInBraces = true;break;}}// 如果不在 {...} 中,但光标前是 `{`,只插入 `${key}}`,不要加多一个 `{`if (!foundInBraces && doc[pos - 1] === "{") {targetFrom = pos;insertText = `${key}}`; // 前面已经有 {,只补后半段}const transaction = state.update({changes: {from: targetFrom,to: targetTo,insert: insertText,},selection: { anchor: targetFrom + insertText.length },});view.dispatch(transaction);view.focus();this.popoverOpen = false;},onEditorBlur(e) {const related = e.relatedTarget;// 如果焦点转移到了 Popover 内部,则不处理 blurif (related && related.closest(".my-variable-popover")) {return;}const view = this.editorView;if (view) {this.$emit("input", view.state.doc.toString());this.$emit("blur");}},handleKeyDownCapture(e) {if (!["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) {e.stopPropagation();}},handleKeyDown(e) {if (!["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) return;if (e.key === "ArrowDown") {let nextKey;if (this.currentIndex < this.flattenedTree.length - 1) {nextKey = this.flattenedTree[this.currentIndex + 1].key;} else {nextKey = this.flattenedTree[0].key;}this.selectedKeys = [nextKey];this.$refs.tree.setCurrentKey(nextKey);} else if (e.key === "ArrowUp") {let prevKey;if (this.currentIndex > 0) {prevKey = this.flattenedTree[this.currentIndex - 1].key;} else {prevKey = this.flattenedTree[this.flattenedTree.length - 1].key;}this.selectedKeys = [prevKey];this.$refs.tree.setCurrentKey(prevKey);} else if (e.key === "Enter" && this.selectedKeys[0]) {// 查找对应的节点数据const findNodeData = (key, nodes) => {for (const node of nodes) {if (node.id === key) return node;if (node.children) {const found = findNodeData(key, node.children);if (found) return found;}}return null;};const nodeData = findNodeData(this.selectedKeys[0], this.variables);if (nodeData) {this.handleVariableInsert(nodeData);}}},},
};
</script><style scoped>
.variable-editor {position: relative;width: 100%;
}.editor-container {width: 100%;border: 1px solid #dcdfe6;border-radius: 4px;min-height: 150px;overflow: hidden;transition: border-color 0.2s, box-shadow 0.2s;
}/* CodeMirror 6 编辑器样式 */
:global(.cm-editor) {height: 150px !important;min-height: 150px !important;overflow-y: auto;
}/* 编辑器获取焦点时的样式 */
:global(.cm-editor.cm-focused) {outline: none;
}/* 使用更具体的选择器确保只有一层边框高亮 */
.editor-container:focus-within {border-color: #409eff !important;
}.anchor-point {position: absolute;z-index: 10;
}.tree-wrap {min-width: 200px;
}.flex-row-center {display: flex;align-items: center;flex-wrap: nowrap;
}.ml-1 {margin-left: 4px;
}/* 添加到全局样式中 */
:global(.cm-decoration-interpolation-valid) {color: #409eff;background-color: rgba(64, 158, 255, 0.1);
}:global(.cm-decoration-interpolation-invalid) {color: #f56c6c;background-color: rgba(245, 108, 108, 0.1);text-decoration: wavy underline #f56c6c;
}
</style>

依赖安装 

npm install @codemirror/state @codemirror/view @codemirror/commands

相关文章:

vue2中,codemirror编辑器的使用

交互说明 在编辑器中输入{时&#xff0c;会自动弹出选项弹窗&#xff0c;然后可以选值插入。 代码 父组件 <variable-editorv-model"content":variables"variables"placeholder"请输入模板内容..."blur"handleBlur" />data…...

FastAPI与MongoDB分片集群:异步数据路由与聚合优化

title: FastAPI与MongoDB分片集群:异步数据路由与聚合优化 date: 2025/05/26 16:04:31 updated: 2025/05/26 16:04:31 author: cmdragon excerpt: FastAPI与MongoDB分片集群集成实战探讨了分片集群的核心概念、Motor驱动配置技巧、分片数据路由策略、聚合管道高级应用、分片…...

Perl单元测试实战指南:从Test::Class入门到精通的完整方案

阅读原文 前言:为什么Perl开发者需要重视单元测试? "这段代码昨天还能运行,今天就出问题了!"——这可能是每位Perl开发者都经历过的噩梦。在没有充分测试覆盖的情况下,即使是微小的改动也可能导致系统崩溃。单元测试正是解决这一痛点的最佳实践,它能帮助我们在…...

强大的免费工具,集合了30+功能

今天给大家分享一款免费的绿色办公软件&#xff0c;它涵盖了自动任务、系统工具、文件工具、PDF 工具、OCR 图文识别、文字处理、电子表格这七个模块&#xff0c;多达 30 余项实用功能&#xff0c;堪称办公利器。 作者开发这款软件的初衷是为了解决日常办公中常见的痛点问题&am…...

从0开始学习R语言--Day11--主成分分析

主成分分析&#xff08;PCA&#xff09; PCA是一种降维技术&#xff0c;它把一堆相关的变量&#xff08;比如身高、体重、年龄&#xff09;转换成少数几个不相关的新变量&#xff08;叫“主成分”&#xff09;&#xff0c;这些新变量能最大程度保留原始数据的信息。 核心理念 …...

通用前端框架项目静态部署到Hugging Face Space的实践指南

背景介绍 在轻量级展示前端项目的场景中,Hugging Face Space 提供了一个便捷的静态托管平台。需求是将无后端服务的Vite的 Vue项目部署到Hugging Face Space 上。其实无论是基于Vite的Vue/React项目,还是使用Webpack构建的工程化方案,都可以通过两种方式将其部署到Space:自…...

AI辅助写作 从提笔难到高效创作的智能升级

你是否经历过面对空白文档头脑空白的绝望&#xff1f;是否为整理实验数据通宵达旦&#xff1f;在这个信息爆炸的时代&#xff0c;一种新型写作方式正悄悄改变知识工作者的创作模式—AI辅助写作。这种技术既不像科幻作品里的自动生成机器人&#xff0c;也非简单的文字模板&#…...

十一、Samba文件共享服务

目录 1、Samba介绍1.1、Samba概述1.2、Samba服务器的主要组成部分1.3、Samba的工作原理2、Samab服务器的安装与配置2.1、安装samba2.2、Samba主配置文件2.2.1、全局设置段[global]2.2.2、用户目录段[homes]2.2.3、配置文件检查工具3、示例3.1、需要用户验证的共享3.2、用户映射…...

医疗影像检测系统设计与实现

以下是一个基于YOLO系列模型的医疗影像检测系统实现及对比分析的详细技术文档。由于目前官方YOLOv11尚未发布,本文将基于YOLOv8架构设计改进型YOLOv11,并与YOLOv8、YOLOv5进行对比实验。全文包含完整代码实现及分析,字数超过6000字。 # 注意:本文代码需要Python 3.8+、PyT…...

11.13 LangGraph记忆机制解析:构建生产级AI Agent的关键技术

LangGraph 持久化与记忆:构建具备记忆能力的生产级 AI Agent 关键词:LangGraph 持久化, 多回合记忆, 单回合记忆, 检查点系统, 状态管理 1. 记忆机制的核心价值 在对话式 AI Agent 的开发中,记忆管理直接决定了用户体验的连贯性和智能性。LangGraph 通过 多回合记忆(Mult…...

C++23中std::span和std::basic_string_view可平凡复制提案解析

文章目录 一、引言二、相关概念解释2.1 平凡复制&#xff08;Trivially Copyable&#xff09;2.2 std::span2.3 std::basic_string_view 三、std::span和std::basic_string_view的应用场景3.1 std::span的应用场景3.2 std::basic_string_view的应用场景 四、P2251R1提案对std::…...

[yolov11改进系列]基于yolov11引入感受野注意力卷积RFAConv的python源码+训练源码

[RFAConv介绍] 1、RFAConv 在传统卷积操作中&#xff0c;每个感受野都使用相同的卷积核参数&#xff0c;无法区分不同位置的信息差异&#xff0c;这都限制了网络性能。此外&#xff0c;由于空间注意力以及现有空间注意力机制的局限性&#xff0c;虽然能够突出关键特征&#xf…...

Springboot引入Spring Cloud for AWS的配置中心(Parameter Store和Secrets)

问题 现在手上有一个老Spring2.5.15项目&#xff0c;需要使用AWS Parameter Store作为配置中心服务。 思路 引入这个Spring版本对应的Spring Cloud&#xff0c;然后再引入Spring Cloud AWS相关组件。然后&#xff0c;在AWS云上面准备好配置&#xff0c;然后&#xff0c;启动…...

打破云平台壁垒支持多层级JSON生成的MQTT网关技术解析

工业智能网关的上行通信以MQTT协议为核心&#xff0c;但在实际应用中&#xff0c;企业往往需要将数据同时或分场景接入多个公有云平台&#xff08;如华为云IoT、阿里云IoT、亚马逊AWS IoT&#xff09;&#xff0c;甚至私有化部署的第三方平台。为实现这一目标&#xff0c;网关需…...

Modbus通信中的延迟和时间间隔详解

在工业自动化领域,Modbus协议作为最广泛使用的通信协议之一,其通信时序和延迟控制直接影响到系统的稳定性和效率。本文将深入探讨Modbus通信中涉及的各种延迟和时间间隔,帮助开发者更好地理解和应用这些概念。 一、串口Modbus通信中的延迟问题 1.1 为什么需要延迟? 在基…...

maven 最短路径依赖优先

问题描述&#xff1a; 项目在升级大版本后出现了&#xff0c;两个不同模块所引用的同一个依赖包版本不同 module A 引用了 module B&#xff0c;module B 引用了 A_1.0.jar->B_1.0.jar->C_1.0.jar(C 为B 里面的包) 在执行 mvn dependency:tree 后发现&#xff1a; modul…...

SAAS架构设计2-流程图-用户与租户之间对应关系图

在SAAS&#xff08;Software as a Service&#xff0c;软件即服务&#xff09;结构中&#xff0c;用户与租户之间的关系可以通过一对一和多对多两种方式来定义。这两种关系模式各自有着不同的应用场景和特点。 用户和租户的关系&#xff08;一对一&#xff09; 一对一关系 在这…...

TypeScript入门到精通

学习ts之前&#xff0c;我们首先了解一下我们为什么要学ts,ts是什么&#xff1f;ts比js有不同呢&#xff1f; TypeScript 是 JavaScript 的一个超集&#xff0c;是由微软开发的自由和开源的编程语言&#xff0c;支持 ECMAScript 6 标准&#xff08;ES6 教程&#xff09;。在 Ja…...

三、Docker目录挂载、卷映射、网络

目录挂载 如果主机目录为空&#xff0c;则容器内也为空 -v表示目录挂载 冒号前面的是主机上的目录&#xff0c;冒号后面的是docker容器里面的地址 修改主机上的文件&#xff0c;发现docker容器里面的内容也随之改变。 同样修改docker容器里面的内容&#xff0c;主机上的文件…...

迪米特法则 (Law of Demeter, LoD)

定义&#xff1a;迪米特法则(Law of Demeter, LoD)&#xff1a;一个软件实体应当尽可能少地与其他实体发生相互作用。 迪米特法则&#xff08;Law of Demeter&#xff0c;LoD&#xff09;又叫作最少知识原则&#xff08;Least Knowledge Principle&#xff0c;LKP)&#xff0c;…...

【R语言编程绘图-函数篇】

基础函数绘制 R语言可通过curve()函数直接绘制数学函数图形&#xff0c;无需预先生成数据点。例如绘制正弦函数&#xff1a; curve(sin, from -pi, to pi, col "blue", lwd 2)自定义函数绘制 对于用户自定义函数&#xff0c;需先定义函数表达式&#xff1a; …...

训练自己的yolo模型,并部署到rk3588上

文章目录 1. 训练自己的模型2. pt转onnx3. onnx转rknn4. 后续…… 1. 训练自己的模型 如何训练自己的yolo模型&#xff0c;网络上已经有足够多的教程&#xff0c;这里只简单的描述一下。如果已经有了自己的.pt模型&#xff0c;那么可以直接跳转到第二节。 此处是以检测模型的…...

Leetcode 3556. Sum of Largest Prime Substrings

Leetcode 3556. Sum of Largest Prime Substrings 1. 解题思路2. 代码实现3. 算法优化 题目链接&#xff1a;3556. Sum of Largest Prime Substrings 1. 解题思路 这一题毕竟只是这一次双周赛的第一题&#xff0c;虽然标记为medium的题目&#xff0c;但是思路上还是非常简单…...

以少学习:通过无标签数据从大型语言模型进行知识蒸馏

Learning with Less: Knowledge Distillation from Large Language Models via Unlabeled Data 发表&#xff1a;NNACL-Findings 2025 机构&#xff1a;密歇根州立大学 Abstract 在实际的自然语言处理&#xff08;NLP&#xff09;应用中&#xff0c;大型语言模型&#xff08…...

鸿蒙OSUniApp 实现带有滑动删除的列表#三方框架 #Uniapp

使用 UniApp 实现带有滑动删除的列表 在移动应用开发中&#xff0c;滑动删除&#xff08;Swipe to Delete&#xff09;是一种常见且实用的交互方式&#xff0c;广泛应用于消息、待办、收藏等列表场景。用户只需在列表项上左右滑动&#xff0c;即可快速删除或管理数据。随着 Ha…...

Qt qml Network error问题

最近在学习Qt&#xff0c;需要调用地图&#xff0c;所以用到了QML&#xff0c;但是却遇到了这样的问题 d://qt_project//run//main.qml: Network error 现在我展示一下我的main文件的代码&#xff1a; #include <QApplication> #include <QQuickView> #include &l…...

Prompt工程:解锁大语言模型的终极密钥

Prompt工程&#xff1a;解锁大语言模型的终极密钥 一、引言&#xff1a;Prompt的战略价值重构 在人工智能技术加速渗透的2025年&#xff0c;Prompt&#xff08;提示词&#xff09;作为连接人类意图与大语言模型&#xff08;LLM&#xff09;的核心接口&#xff0c;其战略地位已…...

Spring Boot微服务架构(六):伪装的微服务有哪些问题?

伪装的微服务有哪些问题&#xff1f; 伪装的微服务架构&#xff08;即表面上模仿微服务设计&#xff0c;但未真正遵循其核心原则的系统&#xff09;通常具备以下特征点&#xff0c;这些特征可能导致系统复杂度增加、维护困难或性能下降&#xff1a; ​​1. 服务间强耦合​​ …...

恶意npm与VS Code包窃取数据及加密货币资产

60个npm包窃取系统敏感信息 安全研究人员在npm软件包注册表中发现60个恶意组件&#xff0c;这些组件能够收集主机名、IP地址、DNS服务器和用户目录信息&#xff0c;并将其发送至Discord平台控制的终端节点。据Socket安全研究员Kirill Boychenko上周发布的报告显示&#xff0c;…...

Matlab快速上手五十六:详解符号运算里假设的用法,通过假设可以设置符号变量的取值范围,也可以通过假设设置变量属于集合:整数、正数和实数等

1.符号变量中假设的概念 在符号数学工具箱中&#xff0c;符号变量默认范围是全体复数&#xff0c;也就是说&#xff0c;符号运算是在全体复数域进行的&#xff0c;若需要运算中&#xff0c;不使用全体复数域&#xff0c;可以为变量设定取值范围&#xff0c;这就用到了假设&…...