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, //插入tag
    placeholderFn, //插入函数
    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>
  <MyDialog
    v-model="state.visible"
    title="Editor"
    :width="800"
    center
    :close-on-click-modal="false"
    :destroy-on-close="true"
    @close="close"
  >
    <div class="editor-container">
      <TreeCom
        class="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.none
        return 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);
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/966587.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MIT 6.5940(一)

记录了Lecture 1~8 Lecture 1 Introduction TinyML and Efficient Deep Learning Computing 摘要 AI systems need to continually adapt to new data collected locally 在设备学习&#xff1a;better privacy, lower cost, customization, life-long learningTraining is…

Linux TCP 编程详解与实例

一、引言 在网络编程的领域中&#xff0c;TCP&#xff08;Transmission Control Protocol&#xff09;协议因其可靠的数据传输特性而被广泛应用。在 Linux 环境下&#xff0c;使用 C 或 C 进行 TCP 编程可以实现各种强大的网络应用。本文将深入探讨 Linux TCP 编程的各个方面&…

一款由 .NET 官方团队开源的电子商务系统 - eShop

项目介绍 eShop是一款由.NET官方开源的&#xff0c;基于.NET Aspire构建的用于参考学习的服务架构电子商务系统&#xff0c;旨在展示如何利用.NET框架及其相关技术栈构建一个现代化的电子商务网站。该项目采用服务架构&#xff0c;将应用程序分解为多个独立的服务&#xff0c;…

crewai框架第三方API使用官方RAG工具(pdf,csv,json)

最近在研究调用官方的工具&#xff0c;但官方文档的说明是在是太少了&#xff0c;后来在一个视频里看到了如何配置&#xff0c;记录一下 以PDF RAG Search工具举例&#xff0c;官方文档对于自定义模型的说明如下&#xff1a; 默认情况下&#xff0c;该工具使用 OpenAI 进行嵌…

2011-2020年各省电话普及率数据

2011-2020年各省电话普及率数据 1、时间&#xff1a;2011-2020年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区名称、年份、电话普及率(包括移动电话)(部/百人) 4、范围&#xff1a;31省 5、指标说明&#xff1a;电话普及率是衡量一个…

【自开发工具介绍】SQLSERVER的ImpDp和ExpDp工具演示05

SQLSERVER的ImpDp和ExpDp工具演示 1、表部分数据导出 (-query) ※「-query」和「-include_table」必须一起使用 「-query」后面字符串是sql文的where语句&#xff0c;但要注意要使用%&#xff0c;需要写%% 验证用&#xff1a;导出的表&#xff0c;导入到新的数据库 db的数…

ASP.NET Core 使用 WebClient 从 URL 下载

本文使用 ASP .NET Core 3.1&#xff0c;但它在.NET 5、 .NET 6和.NET 8上也同样适用。如果使用较旧的.NET Framework&#xff0c;请参阅本文&#xff0c;不过&#xff0c;变化不大。 如果想要从 URL 下载任何数据类型&#xff0c;请参阅本文&#xff1a;HttpClient 使用WebC…

快速上手Vim的使用

Vim Linux编辑器-vim使用命令行模式下所有选项都可以带数字底行模式可视块模式&#xff08;ctrlV进入&#xff09; Linux编辑器-vim使用 Vim有多种模式的编辑器。能帮助我们很快的进行代码的编辑&#xff0c;甚至完成很多其他事情。 默认情况下我们打开vim在命令模式下&#x…

nodejs - vue 视频切片上传,本地正常,线上环境导致磁盘爆满bug

nodejs 视频切片上传&#xff0c;本地正常&#xff0c;线上环境导致磁盘爆满bug 原因&#xff1a; 然后在每隔一分钟执行du -sh ls &#xff0c;发现文件变得越来越大&#xff0c;即文件下的mp4文件越来越大 最后导致磁盘直接爆满 排查原因 1、尝试将m3u8文件夹下的所有视…

114,【6】攻防世界 web wzsc_文件上传

进入靶场 传个桌面有的 直接空白了 我们 访问一下上传的东西 /index 没显示用于解析的.htaccess和.user.ini 文件&#xff0c;还两个都不显示 .htaccess 和 .user.ini 文件分别用于 Apache 服务器和 PHP-FPM 环境的目录级配置 但上传的时候bp查看状态码是200&#xff0c;…

Open3d Qt的环境配置

Open3d Qt的环境配置 一、概述二、操作流程2.1 下载文件2.2 新建文件夹2.3 环境变量设置2.4 qt6 引用3、qt中调用4、资源下载一、概述 目前统一使用qt6配置,open3d中可视化功能目前使用vtk代替,语言为c++。 二、操作流程 2.1 下载文件 访问open3d github链接,进入releas…

零基础都可以本地部署Deepseek R1

文章目录 一、硬件配置需求二、详细部署步骤1. 安装 Ollama 工具2. 部署 DeepSeek-R1 模型3. API使用4. 配置图形化交互界面&#xff08;可选&#xff09;5. 使用与注意事项 一、硬件配置需求 不同版本的 DeepSeek-R1 模型参数量不同&#xff0c;对硬件资源的要求也不尽相同。…

Rocky Linux9安装Zabbix7.0(精简版)

Linux 系统版本 Rocky Linux release 9.3 (Blue Onyx) 注意&#xff1a;zabbix 7以上版本不支持CentOS 7系统&#xff0c;需要CentOS 8以上&#xff0c; 本教程支持CentOS9及Rocky Linux 9 在Rocky Linux release 9.3测试通过 Linux环境准备 关闭防火墙和selinux #关闭防…

Qt程序发布

关注后回复 qt 获取相关资料 找到Qt安装目录中的 windeployqt.exe 将其路径添加到Path环境变量中可能会涉及到多平台架构的版本&#xff0c;选择一个目标版本将Release版中的 ***.exe 复制到某空文件夹cmd 进入上述文件夹中执行 windeployqt.exe ***.exe此时会将该 ***.exe 文件…

从O(k*n)到O(1):如何用哈希表终结多层if判断的性能困局

【前言】   本文将以哈希表重构实战为核心&#xff0c;完整展示如何将传统条件匹配逻辑(上千层if-else判断)转化为O(1)的哈希表高效实现。通过指纹验证场景的代码级解剖&#xff0c;您将深入理解&#xff1a;   1.哈希函数设计如何规避冲突陷阱   2.链式寻址法的工程实现…

后端java工程师经验之谈,工作7年,mysql使用心得

mysql 工作7年&#xff0c;mysql使用心得 mysql1.创建变量2.创建存储过程2.1&#xff1a;WHILE循环2.2&#xff1a;repeat循环2.3&#xff1a;loop循环2.4&#xff1a;存储过程&#xff0c;游标2.5&#xff1a;存储过程&#xff0c;有输入参数和输出参数 3.三种注释写法4.case …

【WB 深度学习实验管理】利用 Hugging Face 实现高效的自然语言处理实验跟踪与可视化

本文使用到的 Jupyter Notebook 可在GitHub仓库002文件夹找到&#xff0c;别忘了给仓库点个小心心~~~ https://github.com/LFF8888/FF-Studio-Resources 在自然语言处理领域&#xff0c;使用Hugging Face的Transformers库进行模型训练已经成为主流。然而&#xff0c;随着模型复…

智能理解 PPT 内容,快速生成讲解视频

当我们想根据一版 PPT 制作出相对应的解锁视频时&#xff0c;从撰写解锁词&#xff0c;录制音频到剪辑视频&#xff0c;每一个环节都需要投入大量的时间和精力&#xff0c;本方案将依托于阿里云函数计算 FC 和百炼模型服务&#xff0c;实现从 PPT 到视频的全自动转换&#xff0…

如何使用Gemini模型,国内如何订阅购买Gemini Pro的教程,Gemini Pro 免费试用操作步骤, 谷歌 aistudio 使用入口

最近的榜首又被Gemini给霸占了&#xff0c;很多童鞋想要体验一翻 Gemini免费库模型更新了 Gemini2.0向所有人开放了&#xff01;使用了真香 目前呢2.0flash和Gemini-2.0-Flash-Thinking-Exp、Gemini-2.0-Flash-Thinking-Exp-with-apps已经免费给所有注册用户开放了&#xff0c…

【学术投稿】第五届计算机网络安全与软件工程(CNSSE 2025)

重要信息 官网&#xff1a;www.cnsse.org 时间&#xff1a;2025年2月21-23日 地点&#xff1a;中国-青岛 简介 第五届计算机网络安全与软件工程&#xff08;CNSSE 2025&#xff09;将于2025年2月21-23日在中国-青岛举行。CNSSE 2025专注于计算机网络安全、软件工程、信号处…