Vue3+codemirror6实现公式(规则)编辑器
实现截图

实现/带实现功能
- 插入标签
- 插入公式
- 提示补全
- 公式验证
- 公式计算
需要的依赖
"@codemirror/autocomplete": "^6.18.4","@codemirror/lang-javascript": "^6.2.2","@codemirror/state": "^6.5.2","@codemirror/view": "^6.36.2","codemirror": "^6.0.1",
初始化编辑器
// index.ts
export const useCodemirror = () => {const code = ref("");const view = shallowRef<EditorView>();const editorRef = ref<InstanceType<typeof HTMLDivElement>>();const extensions = [placeholderTag, //插入tagplaceholderFn, //插入函数baseTheme, //基础样式EditorView.lineWrapping, //换行basicSetup, //基础配置javascript(), //js语言支持autocompletion({ override: [myCompletions] }), //补全提示];/*** @description 初始化编辑器*/const init = () => {if (editorRef.value) {view.value = new EditorView({parent: editorRef.value,state: EditorState.create({doc: code.value,extensions: extensions,}),});setTimeout(() => {view.value?.focus();}, 0);}};/*** @description 销毁编辑器*/const destroyed = () => {view.value?.destroy();view.value = undefined;};/*** @description 插入文本并设置光标位置*/const insertText = (text: string, type: "fn" | "tag" = "tag") => {if (view.value) {let content = type === "tag" ? `[[${text}]]` : `{{${text}}}()`;const selection = view.value.state.selection;if (!selection.main.empty) {// 如果选中文本,则替换选中文本const from = selection.main.from;const to = selection.main.to;const anchor =type === "tag" ? from + content.length : from + content.length - 1;const transaction = view.value!.state.update({changes: { from, to, insert: content }, // 在当前光标位置插入标签selection: {anchor,}, // 指定新光标位置});view.value.dispatch(transaction);} else {// 如果没有选中文本,则插入标签const pos = selection.main.head;const anchor =type === "tag" ? pos + content.length : pos + content.length - 1;const transaction = view.value.state.update({changes: { from: pos, to: pos, insert: content }, // 在当前光标位置插入标签selection: {anchor: anchor,}, // 指定新光标位置});view.value.dispatch(transaction);}setTimeout(() => {view.value?.focus();}, 0);}};return {code,view,editorRef,init,destroyed,insertText,};
};
<template><MyDialogv-model="state.visible"title="Editor":width="800"center:close-on-click-modal="false":destroy-on-close="true"@close="close"><div class="editor-container"><TreeComclass="editor-tree":data="state.paramsData"@node-click="insertTag"></TreeCom><div class="editor-content"><div class="editor-main" ref="editorRef"></div><div class="fn"><div class="fn-list"><TreeCom:default-expand-all="true":data="state.fnData"@node-click="insertFn"@mouseenter="hoverFn"></TreeCom></div><div class="fn-desc"><DescCom v-bind="state.info"></DescCom></div></div></div></div><template #footer><div><el-button @click="close">取消</el-button><el-button type="primary" @click="submit">确认</el-button></div></template></MyDialog>
</template><script lang="ts">
export default { name: "Editor" };
</script>
<script lang="ts" setup>
import { nextTick, reactive } from "vue";
import TreeCom from "./components/tree.vue";
import DescCom from "./components/desc.vue";
import { useCodemirror, functionDescription } from ".";
import { Tree } from "@/types/common";const state = reactive({visible: false,paramsData: [{label: "参数1",id: "1",},{label: "参数2",id: "2",},{label: "参数3",id: "3",},],fnData: [{label: "常用函数",id: "1",children: [{label: "SUM",desc: "求和",id: "1-1",},{label: "IF",desc: "条件判断",id: "1-2",},],},],info: {},
});const { code, view, editorRef, init, destroyed, insertText } = useCodemirror();
/*** @description 插入标签*/
const insertTag = (data: Tree) => {if (!data.children) {insertText(`${data.id}.${data.label}`);}
};
/*** @description 插入函数*/
const insertFn = (data: Tree) => {if (!data.children) {insertText(`${data.label}`, "fn");}
};
/*** @description 鼠标悬停展示函数描述*/
const hoverFn = (data: Tree) => {const info = functionDescription(data.label);if (info) {state.info = info;}
};
/*** @description 获取数据*/
const submit = () => {const data = view.value?.state.doc;console.log(data);
};
const open = () => {state.visible = true;nextTick(() => {init();});
};
const close = () => {destroyed();state.visible = false;
};defineExpose({open,
});
</script><style lang="scss" scoped>
.editor-container {position: relative;.editor-tree {width: 200px;position: absolute;left: 0;top: 0;height: 100%;}.editor-content {margin-left: 210px;display: flex;flex-direction: column;.editor-main {border: 1px solid #ccc;height: 200px;}.fn {display: flex;height: 200px;> div {flex: 1;border: 1px solid #ccc;}}}
}
:deep(.cm-focused) {outline: none;
}
:deep(.cm-gutters) {display: none;
}
</style>
插入标签的实现
根据官网例子以及部分大佬思路改编
- 插入标签使用
[[${id}.${label}]]
/*** @description 插入文本并设置光标位置*/const insertText = (text: string, type: "fn" | "tag" = "tag") => {if (view.value) {let content = type === "tag" ? `[[${text}]]` : `{{${text}}}()`;const selection = view.value.state.selection;if (!selection.main.empty) {// 如果选中文本,则替换选中文本const from = selection.main.from;const to = selection.main.to;const anchor =type === "tag" ? from + content.length : from + content.length - 1;const transaction = view.value!.state.update({changes: { from, to, insert: content }, // 在当前光标位置插入标签selection: {anchor,}, // 指定新光标位置});view.value.dispatch(transaction);} else {// 如果没有选中文本,则插入标签const pos = selection.main.head;const anchor =type === "tag" ? pos + content.length : pos + content.length - 1;const transaction = view.value.state.update({changes: { from: pos, to: pos, insert: content }, // 在当前光标位置插入标签selection: {anchor: anchor,}, // 指定新光标位置});view.value.dispatch(transaction);}setTimeout(() => {view.value?.focus();}, 0);}};
- 然后去匹配
[[]]中的内容,取出来用span包裹
/*** @description 插入tag*/
const placeholderTagMatcher = new MatchDecorator({regexp: /\[\[(.+?)\]\]/g,decoration: (match) => {return Decoration.replace({ widget: new PlaceholderTag(match[1]) });},
});
// 定义一个 PlaceholderTag 类,继承自 WidgetType
class PlaceholderTag extends WidgetType {// 定义一个字符串类型的 id 属性,默认值为空字符串id: string = "";// 定义一个字符串类型的 text 属性,默认值为空字符串text: string = "";// 构造函数,接收一个字符串类型的 text 参数constructor(text: string) {// 调用父类的构造函数super();// 被替换的数据处理if (text) {const [id, ...texts] = text.split(".");if (id && texts.length) {this.text = texts.join(".");this.id = id;console.log(this.text, "id:", this.id);}}}eq(other: PlaceholderTag) {return this.text == other.text;}// 此处是我们的渲染方法toDOM() {let elt = document.createElement("span");if (!this.text) return elt;elt.className = "cm-tag";elt.textContent = this.text;return elt;}ignoreEvent() {return true;}
}
// 导出一个名为placeholders的常量,它是一个ViewPlugin实例,通过fromClass方法创建
const placeholderTag = ViewPlugin.fromClass(// 定义一个匿名类,该类继承自ViewPlugin的基类class {// 定义一个属性placeholders,用于存储装饰集placeholders: DecorationSet;// 构造函数,接收一个EditorView实例作为参数constructor(view: EditorView) {// 调用placeholderMatcher.createDeco方法,根据传入的view创建装饰集,并赋值给placeholders属性this.placeholders = placeholderTagMatcher.createDeco(view);}// update方法,用于在视图更新时更新装饰集update(update: ViewUpdate) {// 调用placeholderMatcher.updateDeco方法,根据传入的update和当前的placeholders更新装饰集,并重新赋值给placeholders属性this.placeholders = placeholderTagMatcher.updateDeco(update,this.placeholders);}},// 配置对象,用于定义插件的行为{// decorations属性,返回当前实例的placeholders属性,用于提供装饰集decorations: (v) => v.placeholders,// provide属性,返回一个函数,该函数返回一个EditorView.atomicRanges的提供者provide: (plugin) =>EditorView.atomicRanges.of((view) => {// 从view中获取当前插件的placeholders属性,如果不存在则返回Decoration.nonereturn view.plugin(plugin)?.placeholders || Decoration.none;}),}
);
- 设置样式
const baseTheme = EditorView.baseTheme({".cm-tag": {paddingLeft: "6px",paddingRight: "6px",paddingTop: "3px",paddingBottom: "3px",marginLeft: "3px",marginRight: "3px",backgroundColor: "#ffcdcc",borderRadius: "4px",},".cm-fn": {color: "#01a252",},
});
- 使用插件

插入公式的实现
同理,我只是把[[]]换成了{{}},然后样式也修改了
注意:我们插入标签和公式的时候要指定光标位置,不然会出现问题,使用起来也不方便
提示补全的实现
也是根据官网例子改编,注意要先下载依赖@codemirror/autocomplete
/*** @description 补全提示*/
const completions = [{label: "SUM",apply: insetCompletion,},{label: "IF",apply: insetCompletion,},
];
/*** @description 补全提示* @param {CompletionContext} context* @return {*}*/
function myCompletions(context: CompletionContext) {// 匹配到以s或su或sum或i或if开头的单词let before = context.matchBefore(/[s](?:u(?:m)?)?|[i](?:f)?/gi);if (!context.explicit && !before) return null;return {from: before ? before.from : context.pos,options: completions,};
}
/*** @description 插入补全* @param {EditorView} view* @param {Completion} completion* @param {number} from* @param {number} to*/
function insetCompletion(view: EditorView,completion: Completion,from: number,to: number
) {const content = `{{${completion.label}}}()`;const anchor = from + content.length - 1;const transaction = view.state.update({changes: { from, to, insert: content }, // 在当前光标位置插入标签selection: {anchor: anchor,}, // 指定新光标位置});view.dispatch(transaction);
}
使用插件

仓库地址
在线预览
相关文章:
Vue3+codemirror6实现公式(规则)编辑器
实现截图 实现/带实现功能 插入标签 插入公式 提示补全 公式验证 公式计算 需要的依赖 "codemirror/autocomplete": "^6.18.4","codemirror/lang-javascript": "^6.2.2","codemirror/state": "^6.5.2","cod…...
Lua中文语言编程源码-第十一节,其它小改动汉化过程
__tostring 汉化过程 liolib.c metameth[] {"__转换为字符串", f_tostring}, lauxlib.c luaL_callmeta(L, idx, "__转换为字符串") lua.c luaL_callmeta(L, 1, "__转换为字符串") __len 汉化过程 ltm.c luaT_eventname[] ltablib.c c…...
Safari常用快捷键
一、书签边栏 1、显示或隐藏书签边栏:Control-Command-1 2、选择下一个书签或文件夹:向上头键或向下头键 3、打开所选书签:空格键 4、打开所选文件夹:空格键或右箭头键 5、关闭所选文件夹:空格键或左箭头键 6、更…...
Git登录并解决 CAPTCHA
修改公司域账户密码之后,导致今天pull代码时显示:remote error: CAPTCHA required 本文将介绍如何解决 Git 中的常见错误“fatal: Authentication failed for git”。该问题通常出现在尝试访问远程 Git 仓库时,表示身份验证失败。以下是几种常…...
Websocket从原理到实战
引言 WebSocket 是一种在单个 TCP 连接上进行全双工通信的网络协议,它使得客户端和服务器之间能够进行实时、双向的通信,既然是通信协议一定要从发展历史到协议内容到应用场景最后到实战全方位了解 发展历史 WebSocket 最初是为了解决 HTTP 协议在实时…...
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_get_options函数
声明 就在 main函数所在的 nginx.c 中: static ngx_int_t ngx_get_options(int argc, char *const *argv); 实现 static ngx_int_t ngx_get_options(int argc, char *const *argv) {u_char *p;ngx_int_t i;for (i 1; i < argc; i) {p (u_char *) argv[i]…...
判断您的Mac当前使用的是Zsh还是Bash:echo $SHELL、echo $0
要判断您的Mac当前使用的是Zsh还是Bash,可以使用以下方法: 查看默认Shell: 打开“终端”应用程序,然后输入以下命令: echo $SHELL这将显示当前默认使用的Shell。例如,如果输出是/bin/zsh,则说明您使用的是Z…...
Centos执行yum命令报错
错误描述 错误:为仓库 ‘appstream’ 下载元数据失败 : Cannot prepare internal mirrorlist: Curl error (6): Couldn’t resolve host name for http://mirrorlist.centos.org/?release8&archx86_64&repoAppStream&infrastock [Could not resolve h…...
订单超时设计(1)--- 如何使用redis实现订单超时实时关闭功能
如何使用redis实现订单超时实时关闭功能 准备工作实现步骤解释注意事项(重点) 使用Redis实现订单超时实时关闭功能,可以利用Redis的延时队列(使用Sorted Set实现)和过期键(使用TTL和Keyspace Notifications…...
485网关数据收发测试
目录 1.UDP SERVER数据收发测试 使用产品: || ZQWL-GW1600NM 产品||【智嵌物联】智能网关型串口服务器 1.UDP SERVER数据收发测试 A(TX)连接RX B(RX)连接TX 打开1个网络调试助手,模拟用户的UDP客户端设…...
RabbitMQ快速上手及入门
概念 概念: publisher:生产者,也就是发送消息的一方 consumer:消费者,也就是消费消息的一方 queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理 exchang…...
4种架构的定义和关联
文章目录 **1. 各架构的定义****业务架构(Business Architecture)****应用架构(Application Architecture)****数据架构(Data Architecture)****技术架构(Technology Architecture)*…...
109,【1】攻防世界 web 题目名称-文件包含
进入靶场 直接显示源代码 提示我们通过get方式传递名为filename的参数,同时给出了文件名check.php filenamecheck.php 显示使用了正确的用法,错误的方法 filename./check.php 还是一样的回显 傻了,题目名称是文件包含,需要用到…...
leetcode90 子集II
1. 题意 给一个可能含有重复元素的数组,求这个数组的所有子集。 2. 题解 跟leetcode 72 子集的差别在于,我们需要将重复的元素给去掉。那如何去重呢,实际上我们可以先排序将重复的元素给放在一起。然后在回溯后,找到下一个不与…...
DeepSeek模型构建与训练
在完成数据预处理之后,下一步就是构建和训练深度学习模型。DeepSeek提供了简洁而强大的API,使得模型构建和训练变得非常直观。无论是简单的全连接网络,还是复杂的卷积神经网络(CNN)或循环神经网络(RNN),DeepSeek都能轻松应对。本文将带你一步步构建一个深度学习模型,并…...
PyTorch torch.unbind、torch.split 和 torch.chunk函数介绍
pytorch中 torch.unbind、torch.split 和 torch.chunk等函数可用于张量的拆分操作。 1. torch.unbind 功能说明: torch.unbind 沿指定的维度将张量“解包”为多个张量,返回一个元组。解包后被操作的那个维度会消失,每个输出张量的维度数会比…...
【愚公系列】《循序渐进Vue.js 3.x前端开发实践》061-Vue Router的动态路由
标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主&…...
杭州某小厂面试
问的都是基础知识,主要是三个部分:计网,数据库,java。计网答得挺好,数据答得一般,Java答得一坨。 目录 1.TCP/IP协议的5层模型 2.3次握手和4次挥手 3.操作系统中的进程和线程的区别 4.lunix top 命令看…...
C基础寒假练习(8)
一、终端输入10个学生成绩,使用冒泡排序对学生成绩从低到高排序 #include <stdio.h> int main(int argc, const char *argv[]) {int arr[10]; // 定义一个长度为10的整型数组,用于存储学生成绩int len sizeof(arr) / sizeof(arr[0]); // 计算数组…...
设计模式 ->模板方法模式(Template Method Pattern)
模板方法模式 模板方法模式是一种行为设计模式,它在一个方法中定义一个操作的算法骨架,而将一些步骤延迟到子类中实现。它允许子类在不改变算法结构的情况下重新定义算法中的某些步骤 特点 算法骨架: 在基类中定义算法的框架延迟实现&…...
UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...
XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...
阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。
1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj,再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...
JavaScript 数据类型详解
JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型(Primitive) 和 对象类型(Object) 两大类,共 8 种(ES11): 一、原始类型(7种) 1. undefined 定…...
android13 app的触摸问题定位分析流程
一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...
AI语音助手的Python实现
引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...
