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)
模板方法模式 模板方法模式是一种行为设计模式,它在一个方法中定义一个操作的算法骨架,而将一些步骤延迟到子类中实现。它允许子类在不改变算法结构的情况下重新定义算法中的某些步骤 特点 算法骨架: 在基类中定义算法的框架延迟实现&…...
Python中openpyxl库的基础解析与代码实例
目录 1. 前言 2. 安装openpyxl 3. 创建一个新的工作簿 4. 打开一个已有的工作簿 5. 读取和写入单元格 6. 操作工作表 7. 样式设置 8. 插入图像 9. 插入图表 10. 数据验证 11. 条件格式 12. 工作簿保护 13. 保存和关闭工作簿 14. 总结 1. 前言 在数据分析和处理的…...

MySql--定义表存储引擎、字符集和排序规则
示例: CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,email VARCHAR(100) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;注意事项: 字符集和排序规则可以按列覆盖表…...
AI对软件工程的影响及未来发展路径分析报告
目录 第一部分:引言 研究背景与意义 报告框架与方法论 第二部分:AI对不同行业软件工程的影响分析 数字化行业 制造业 零售业 工业领域 第三部分:大厂AI软件工程实践案例分析 微软 谷歌 阿里巴巴 华为 第四部分:未来…...

聊聊网络变压器的浪涌等级标准是怎样划分的呢?
Hqst盈盛(华强盛)电子导读:聊聊网络变压器的浪涌等级标准是怎样划分的呢? 在和做防雷产品的客户的深度沟通网络变压器产品选型中发现:客户对网络变压器的浪涌等级划分也很希望有更深的了解,今天就这个问题和…...

大模型深度学习之双塔模型
前言 双塔模型(Two-Tower Model)是一种在推荐系统、信息检索和自然语言处理等领域广泛应用的深度学习架构。其核心思想是通过两个独立的神经网络(用户塔和物品塔)分别处理用户和物品的特征,并在共享的语义空间中通过相…...

kafka学习笔记(三、消费者Consumer使用教程——从指定位置消费)
1.简介 Kafka的poll()方法消费无法精准的掌握其消费的起始位置,auto.offset.reset参数也只能在比较粗粒度的指定消费方式。更细粒度的消费方式kafka提供了seek()方法可以指定位移消费允许消费者从特定位置(如固定偏移量、时间戳或分区首尾)开…...

Flutter3.22适配运行鸿蒙系统问题记录
Flutter3.22适配运行鸿蒙系统问题记录 一:适配条件适配过程问题记录(1)环境配置问题(2)Concurrent modification during iteration: Instance(length:2) of_GrowableList 报错(3)三方插件寻找替…...
day40 python图像数据与显存
目录 一、图像数据的处理与预处理 (一)图像数据的特点 (二)数据预处理 二、神经网络模型的定义 (一)黑白图像模型的定义 (二)彩色图像模型的定义 (三)…...
分布式爬虫监控架构设计
1. 监控架构核心组件 1.1 日志集中管理 设计目标:聚合所有节点的运行日志,支持实时查询与异常分析。 实现方式: 日志采集:各节点通过 logging 模块将日志发送至中央存储(如Elasticsearch或Redis)。 日志…...

OpenGL Chan视频学习-7 Writing a Shader inOpenGL
bilibili视频链接: 【最好的OpenGL教程之一】https://www.bilibili.com/video/BV1MJ411u7Bc?p5&vd_source44b77bde056381262ee55e448b9b1973 函数网站: docs.gl 说明: 1.之后就不再整理具体函数了,网站直接翻译会更直观也会…...