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

server/2025/2/12 0:15:38/

实现截图

在这里插入图片描述

实现/带实现功能

  • 插入标签
  • 插入公式
  • 提示补全
  • 公式验证
  • 公式计算

需要的依赖

    "@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>

插入标签的实现

根据官网例子以及部分大佬思路改编

  1. 插入标签使用[[${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);}};
  1. 然后去匹配[[]]中的内容,取出来用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;}),}
);
  1. 设置样式
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",},
});
  1. 使用插件
    在这里插入图片描述

插入公式的实现

同理,我只是把[[]]换成了{{}},然后样式也修改了

注意:我们插入标签和公式的时候要指定光标位置,不然会出现问题,使用起来也不方便

提示补全的实现

也是根据官网例子改编,注意要先下载依赖@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);
}

使用插件
在这里插入图片描述
仓库地址
在线预览


http://www.ppmy.cn/server/166891.html

相关文章

MATLAB中count函数用法

目录 语法 说明 示例 对出现次数计数 使用模式对数字和字母进行计数 多个子字符串的所有出现次数 忽略大小写 对字符向量中的子字符串进行计数 count函数的功能是计算字符串中模式的出现次数。 语法 A count(str,pat) A count(str,pat,IgnoreCase,true) 说明 A c…

使用Python开发PPTX压缩工具

引言 在日常办公中&#xff0c;PPT文件往往因为图片过大而导致文件体积过大&#xff0c;不便于传输和存储。为了应对这一问题&#xff0c;我们可以使用Python的wxPython图形界面库结合python-pptx和Pillow&#xff0c;开发一个简单的PPTX压缩工具。本文将详细介绍如何实现这一…

idea整合deepseek实现AI辅助编程

1.File->Settings 2.安装插件codegpt 3.注册deepseek开发者账号&#xff0c;DeepSeek开放平台 4.按下图指示创建API KEY 5.回到idea配置api信息&#xff0c;File->Settings->Tools->CodeGPT->Providers->Custom OpenAI API key填写deepseek的api key Chat…

fastjson2学习大纲

一、基础篇 - JSON与fastjson2核心概念 JSON基础 JSON语法规范&#xff08;RFC 8259&#xff09;JSON数据类型与Java类型对应关系序列化/反序列化核心概念 fastjson2入门 与fastjson1的主要区别核心优势&#xff1a; 性能提升&#xff08;JSONB二进制协议&#xff09;更完善的…

BUU34 [BSidesCF 2020]Had a bad day1 【php://filter】

题目&#xff1a; 发现url有点奇怪 尝试读取一下flag.php&#xff0c;出现错误了 感觉有希望&#xff0c;一看url中还有个index.php&#xff0c;那就试试读取源码吧 出现错误&#xff0c;原来是index.php.php重合了&#xff0c;把php去掉 &#xff0c;出现了 <?php$file…

疯狂前端面试题(二)

一、Webpack的理解 Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。Webpack 能够将各种资源&#xff08;JavaScript、CSS、图片、字体等&#xff09;视为模块&#xff0c;并通过依赖关系图将这些模块打包成一个或多个最终的输出文件&#xff08;通常是一个或几个…

软件工程-软件需求分析基础

基本任务 准确地回答“系统必须做什么&#xff1f;”&#xff0c;也就是对目标系统提出完整、准确、清晰、具体的要求 目标是&#xff0c;在分析阶段结束之前&#xff0c;系统分析员应该写出软件需求规格说明书&#xff0c;以书面形式准确地描述软件需求。 准则 1&#xff…

无人机目标飞行跟踪

无人机目标飞行跟踪主要通过无人机搭载的摄像头或其他传感器采集环境信息&#xff0c;通过算法分析识别目标物体&#xff0c;并对其进行精确跟踪‌。‌无人机采用先进的控制算法和导航系统&#xff0c;根据目标的位置和运动状态动态调整飞行路径‌。这些算法能够处理传感器传来…