【03编辑器与大模型篇】0到1打造基于文心大模型和飞桨小模型的智能在线编辑器

五、编辑器引入与操作方法

目前开源的编辑器框架有很多,这里主要介绍一款自定义能力强大的富文本编辑器Tiptap。

1、Tiptap介绍

Tiptap是一个基于 ProseMirror 构建的富文本编辑器,它是一个灵活、可扩展的富文本编辑器,同时适用于 Vue.js 和 React。Tiptap 的核心思路是通过插件系统提供丰富的功能,使得开发者可以根据需求定制编辑器的功能和样式
官网地址:https://tiptap.dev/
github地址:https://github.com/ueberdosis/tiptap
文档地址:https://tiptap.dev/docs/editor/installation/vue3
Tiptap 的主要有5大部分组成:
(1)、Core:Tiptap 的核心模块,负责处理编辑器的基本功能,如文本输入、选择、撤销和重做等。
(2)、Extensions:扩展模块,提供丰富的编辑功能,如加粗、斜体、列表、链接等。开发者可以根据需求选择需要的功能,并通过插件系统轻松地添加到编辑器中。
(3)、Commands:命令模块,用于执行编辑操作,如插入、删除、修改等。开发者可以通过命令 API 对编辑器进行操作,实现自定义的功能。
(4)、Schema:定义编辑器的文档结构,包括节点、标记和规则。通过自定义 Schema,可以实现特定的文档结构和约束。
(5)、Vue/React components:Tiptap 提供了 Vue 和 React 的组件,使得编辑器可以轻松地集成到这两个框架中。
系统架构图如下所示:

Tiptap 作为主要的入口,连接了 Core、Extensions、Commands、Schema 和 Vue/React components。Extensions 又包括了多个功能模块,如 Bold、Italic、List 和 Link。这样的架构使得 Tiptap 可以根据需求灵活地扩展功能和样式。

2、Tiptap安装与配置

在vscode终端执行如下pnpm安装指令安装Tiptap以及一些插件:

npm i tiptap @tiptap/starter-kit @tiptap/vue-3 -force
npm i @tiptap/extension-highlight @tiptap/extension-task-item @tiptap/extension-task-list -force
npm i @tiptap/extension-code-block-lowlight highlight.js @tiptap/extension-bullet-list @tiptap/extension-ordered-list -force
npm i @tiptap/extension-list-item  lowlight @tiptap/extension-placeholder -force
npm i @tiptap/extension-list-item -force
npm i  @tiptap/extension-placeholder -force
npm i lowlight -force

下面也有一些插件,非必须:

pnpm install @tiptap/extension-text-align  @tiptap/extension-underline @tiptap/extension-subscript @tiptap/extension-superscript @tiptap/extension-character-count
npm install @tiptap/extension-table
npm install @tiptap/extension-table-header -force
npm install @tiptap/extension-table-row -force
npm install @tiptap/extension-image -force
npm install  @tiptap/extension-table-cell -force

安装完成后重新启动项目,在之前创建Edit文件夹下的index.vue中编写下面的测试代码:

<template>
  <editor-content :editor="editor" />
</template>

<script lang="ts">
import { defineComponent, onMounted, onBeforeUnmount, ref } from 'vue';
import { Editor, EditorContent } from '@tiptap/vue-3';
import StarterKit from '@tiptap/starter-kit';

export default defineComponent({
  components: {
    EditorContent,
  },

  setup() {
    // 使用ref创建可变的响应式引用
    const editor = ref<Editor | null>(null);

    // 在组件挂载后初始化Editor
    onMounted(() => {
      editor.value = new Editor({
        content: '<p>欢迎使用Tiptap!🎉</p>',
        extensions: [StarterKit],
      });
    });

    // 在组件卸载前销毁Editor实例
    onBeforeUnmount(() => {
      editor.value?.destroy();
    });

    // 返回editor供模板使用
    return { editor };
  },
});
</script>

打开网站,查看效果,如果出现如下界面代表配置完成了。
image.png

3、编辑器搭建

3.1 、框架搭建

对于整个页面,是需要进行框架搭建的,暂不深入探讨具体布局细节。参照众多现行在线编辑器的用户交互模式,编辑区域通常位于界面中央,作为内容创作的核心地带,而辅助性功能如目录概览、个性化设置等,则巧妙地分布在界面的左侧与右侧,旨在提升用户的操作便捷性与效率。本教程将采纳这一成熟布局,优化用户交互体验:左侧配备一系列创新辅助工具,便于用户快速调用;顶部则部署基础编辑工具栏,集成了必备的编辑功能,确保用户能够轻松驾驭;右侧设计为详尽的大纲视图,帮助用户直观把握文档结构;而在底部,加入字数统计功能。这种布局比较通用,不要限制你的想象力,可以自行发挥,遵循简洁、易用原则即可。
先初始化一下页面的样式,打开style.css
image.png
修改#app的样式如下:

#app {
  margin: 0;
  padding: 0;
  position: relative;
  width:100%;
  height: 100%;
  display: grid;
}

打开Edit文件夹下的index.vue,进行整体框架的布局,这里使用grid布局,不同模块用不同颜色进行表示。
修改代码如下:

<template>
<div class="EditMain">
  <div class="lefttools"></div>
  <div class="editor">
    <editor-content :editor="editor" />
  </div>
  
  <div class="righttools"></div>
</div>
</template>

<script lang="ts">
import { defineComponent, onMounted, onBeforeUnmount, ref } from 'vue';
import { Editor, EditorContent } from '@tiptap/vue-3';
import StarterKit from '@tiptap/starter-kit';

export default defineComponent({
  components: {
    EditorContent,
  },

  setup() {
    // 使用ref创建可变的响应式引用
    const editor = ref<Editor | null>(null);

    // 在组件挂载后初始化Editor
    onMounted(() => {
      editor.value = new Editor({
        content: '<p>欢迎使用Tiptap!🎉</p>',
        extensions: [StarterKit],
      });
    });

    // 在组件卸载前销毁Editor实例
    onBeforeUnmount(() => {
      editor.value?.destroy();
    });

    // 返回editor供模板使用
    return { editor };
  },
});
</script>
<style>
.EditMain{
  position: relative;
  width:100vw;
  height: 100vh;
  background-color: aquamarine;
  display: grid;
  grid-template-columns: 20% 60% 20%;

}
.lefttools{
  background-color: rgb(111 118 177 / 60%);
  height: 100%;
  width: 100%;
}
.righttools{
  background-color: rgb(206 226 117);
  height: 100%;
  width: 100%;
}
.editor{
  background-color:#f8f1e9;
}

</style>

初始化布局如下所示,颜色部分为了区分,后续去掉,中间部分为编辑器部分。
image.png

3.2 、编辑器部分

打开Edit文件夹下的index.vue,进行编辑器框架的布局,先引入列表、代码等模块,后续用得到,修改文件内容如下所示:

<template>
  <div class="EditMain">
    <div class="lefttools"></div>
    <div class="editor">
      <div class="editorcard">
        <div class="toptools"></div>
        <div class="editcont">
          <EditorContent
            style="padding: 8px;  overflow-y: auto;"
            :editor="editor"
            />
        </div>
        <div class="bottomcount"></div>
      </div>
    </div>

    <div class="righttools"></div>
  </div>
</template>

<script lang="ts">
  import { defineComponent, onMounted, onBeforeUnmount, ref,watch } from 'vue';
  import { Editor, EditorContent, useEditor, BubbleMenu  } from '@tiptap/vue-3';
  import { storeToRefs } from 'pinia'
  import Underline from '@tiptap/extension-underline'

  // 列表
  import ListItem from '@tiptap/extension-list-item'
  import OrderedList from '@tiptap/extension-ordered-list'
  import BulletList from '@tiptap/extension-bullet-list'
  // 代码
  import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
  import css from 'highlight.js/lib/languages/css'
  import js from 'highlight.js/lib/languages/javascript'
  import ts from 'highlight.js/lib/languages/typescript'
  import html from 'highlight.js/lib/languages/xml'
  import { common, createLowlight } from 'lowlight'
  const lowlight = createLowlight()
  lowlight.register({ html, ts, css, js })
  // 字数统计
  import StarterKit from '@tiptap/starter-kit'
  import Placeholder from '@tiptap/extension-placeholder'
  import { UndoRound, MoreHorizOutlined } from '@vicons/material'
  import TaskItem from '@tiptap/extension-task-item'
  import TaskList from '@tiptap/extension-task-list'

  // 使用Pinia
  import { useEditorStore } from '@/stores'
  import EditorMenu from './EditorMenu/index.vue'


  export default defineComponent({
    components: {
      EditorContent,
    },
    setup() {
      // 使用ref创建可变的响应式引用
      // 编辑器初始化
      const editor = useEditor({
        content: ``,
        extensions: [
          StarterKit,
          TaskList,
          TaskItem,
          Placeholder.configure({
            placeholder: '开始输入文本 …'
          }),
          OrderedList,
          BulletList,
          ListItem,
        ],
        injectCSS: false
      })

      // 在组件挂载后初始化Editor
      onMounted(() => {
      });

      // 在组件卸载前销毁Editor实例
      onBeforeUnmount(() => {
        editor.value?.destroy();
      });

      // 返回editor供模板使用
      return { editor };
        },
  });
  </script>
  <style>
  .EditMain{
    position: relative;
    width:100vw;
    height: 100vh;

    display: grid;
    grid-template-columns: 20% 60% 20%;
  
  }
  .lefttools{
    background-color: rgb(111 118 177 / 60%);
    height: 100%;
    width: 100%;
  }
  .righttools{
    background-color: rgb(206 226 117);
    height: 100%;
    width: 100%;
  }
  .editor{
 
  }
  .editorcard{
    position: relative;
    width:95%;
    height: 95%;
    left: 2.5%;
    top:2.5%;
    display: grid;
    grid-template-rows: 5% 92% 3%;
    border: 1px solid #4f5c5765;
  }
  .editorcard .editor{
    position: relative;
    width:100%;
    height: 100%;
    left: 0;
    top:0;
    display: grid;
    grid-template-rows: 10% 90%;
  }
  .editorcard .editor{
    position: relative;
    width:100%;
    height: 100%;
    left: 0;
    top:0;
    display: grid;
    grid-template-rows: 10% 90%;
  }
  .toptools{
    background-color: rgba(207, 220, 245, 0.199);
    border-bottom: 1px dashed #9ca19f65;
  }
  .bottomcount{
    background-color: rgba(207, 220, 245, 0.199);
    border-top: 1px dashed #9ca19f65;
    height: 100%;
    width: 100%;
    display: grid;
    grid-template-columns: 100%;
    grid-template-rows: 100%;
    justify-items: center;
    align-items: center;
  }
  .editcont{
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
  </style>
  
  <style lang="scss">
  b {
    font-weight: bold;
  }
  .ProseMirror {
    overflow-y: scroll;
  }
  .ProseMirror p {
    margin: 0;
  }
  .ProseMirror:focus {
    outline: none;
  }
  .tiptap p.is-editor-empty:first-child::before {
    color: #adb5bd;
    content: attr(data-placeholder);
    float: left;
    height: 0;
    pointer-events: none;
  }
  
  .tiptap {
    > * + * {
      margin-top: 0.75em;
    }
  
    ul {
      padding: 0 2rem;
      list-style: square;
    }
    ol {
      padding: 0 2rem;
      list-style: decimal;
    }
    table {
      border-collapse: collapse;
      table-layout: fixed;
      width: 100%;
      margin: 0;
      overflow: hidden;
  
      td,
      th {
        min-width: 1em;
        border: 2px solid #ced4da;
        padding: 3px 5px;
        vertical-align: top;
        box-sizing: border-box;
        position: relative;
  
        > * {
          margin-bottom: 0;
        }
      }
  
      th {
        font-weight: bold;
        text-align: left;
        background-color: #f1f3f5;
      }
  
      .selectedCell:after {
        z-index: 2;
        position: absolute;
        content: '';
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        background: rgba(200, 200, 255, 0.4);
        pointer-events: none;
      }
  
      .column-resize-handle {
        position: absolute;
        right: -2px;
        top: 0;
        bottom: -2px;
        width: 4px;
        background-color: #adf;
        pointer-events: none;
      }
  
      p {
        margin: 0;
      }
    }
    pre {
      background: #0d0d0d;
      color: #fff;
      font-family: 'JetBrainsMono', monospace;
      padding: 0.75rem 1rem;
      border-radius: 0.5rem;
  
      code {
        color: inherit;
        padding: 0;
        background: none;
        font-size: 0.8rem;
      }
  
      .hljs-comment,
      .hljs-quote {
        color: #616161;
      }
  
      .hljs-variable,
      .hljs-template-variable,
      .hljs-attribute,
      .hljs-tag,
      .hljs-name,
      .hljs-regexp,
      .hljs-link,
      .hljs-name,
      .hljs-selector-id,
      .hljs-selector-class {
        color: #f98181;
      }
      .hljs-number,
      .hljs-meta,
      .hljs-built_in,
      .hljs-builtin-name,
      .hljs-literal,
      .hljs-type,
      .hljs-params {
        color: #fbbc88;
      }
  
      .hljs-string,
      .hljs-symbol,
      .hljs-bullet {
        color: #b9f18d;
      }
  
      .hljs-title,
      .hljs-section {
        color: #faf594;
      }
  
      .hljs-keyword,
      .hljs-selector-tag {
        color: #70cff8;
      }
  
      .hljs-emphasis {
        font-style: italic;
      }
  
      .hljs-strong {
        font-weight: 700;
      }
    }
  }
  
  .tableWrapper {
    overflow-x: auto;
  }
  
  .resize-cursor {
    cursor: ew-resize;
    cursor: col-resize;
  }
  </style>

搭建框架的效果如下所示:
image.png
接下来,完成编辑器的一些功能搭建。

4、基本工具栏

在顶部加一个功能菜单,先设置每一个功能的基本样式,在src/views/Edit文件夹下(后面的都在这创建)创建MenuItem/index.vue文件,编写代码如下:

<template>
  <button
    class="menu-item"
    :class="{ 'is-active': props.isActive ? props.isActive() : null }"
    @click="props.action"
    :title="props.title"
    >
    <svg class="remix">
      <use :xlink:href="`${remixiconUrl}#ri-${props.icon}`" />
    </svg>
  </button>
</template>

<script setup lang="ts">
  import remixiconUrl from 'remixicon/fonts/remixicon.symbol.svg'
  const props = defineProps<{
    icon: string
      title: string
  action: Function
  isActive?: Function
    }>()
      </script>

<style lang="scss">
  .menu-item {
    background: transparent;
    border: none;
    border-radius: 0.4rem;
    color: #333;
    cursor: pointer;
    height: 1.75rem;
    padding: 0.25rem;
    margin-right: 0.25rem;
    width: 1.75rem;

    svg {
      fill: currentColor;
      height: 100%;
      width: 100%;
    }

    &.is-active,
    &:hover {
      background-color: #d6d6d6;
    }
  }
</style>

在src文件夹下创建EditorMenu/index.vue文件,引入MenuItem/index.vue,编写代码如下:

<template>
    <div>
      <template v-for="(item, index) in items">
        <div
          class="divider"
          v-if="item.type === 'divider'"
          :key="`divider${index}`"
        />
        <MenuItem
          v-else
          :key="index"
          v-bind="item"
        />
      </template>
    </div>
  </template>
  
  <script setup lang="ts">
  import { Editor } from '@tiptap/vue-3'
  import MenuItem from '@/views/Edit/MenuItem/index.vue'
  
  const props = defineProps<{ editor: Editor }>()
  
  const items = [
    {
      icon: 'bold',
      title: 'Bold',
      action: () => props.editor?.chain().focus().toggleBold().run(),
      isActive: () => props.editor?.isActive('bold')
    },
    {
      icon: 'italic',
      title: 'Italic',
      action: () => props.editor?.chain().focus().toggleItalic().run(),
      isActive: () => props.editor?.isActive('italic')
    },
    {
      icon: 'strikethrough',
      title: 'Strike',
      action: () => props.editor?.chain().focus().toggleStrike().run(),
      isActive: () => props.editor?.isActive('strike')
    },
    {
      icon: 'code-view',
      title: 'Code',
      action: () => props.editor?.chain().focus().toggleCode().run(),
      isActive: () => props.editor?.isActive('code')
    },
    {
      icon: 'mark-pen-line',
      title: 'Highlight',
      action: () => props.editor?.chain().focus().toggleHighlight().run(),
      isActive: () => props.editor?.isActive('highlight')
    },
    {
      type: 'divider'
    },
    {
      icon: 'h-1',
      title: 'Heading 1',
      action: () =>
        props.editor?.chain().focus().toggleHeading({ level: 1 }).run(),
      isActive: () => props.editor?.isActive('heading', { level: 1 })
    },
    {
      icon: 'h-2',
      title: 'Heading 2',
      action: () =>
        props.editor?.chain().focus().toggleHeading({ level: 2 }).run(),
      isActive: () => props.editor?.isActive('heading', { level: 2 })
    },
    {
      icon: 'h-3',
      title: 'Heading 3',
      action: () =>
        props.editor?.chain().focus().toggleHeading({ level: 3 }).run(),
      isActive: () => props.editor?.isActive('heading', { level: 3 })
    },
    {
      icon: 'h-4',
      title: 'Heading 4',
      action: () =>
        props.editor?.chain().focus().toggleHeading({ level: 4 }).run(),
      isActive: () => props.editor?.isActive('heading', { level: 4})
    },
    {
      icon: 'paragraph',
      title: 'Paragraph',
      action: () => props.editor?.chain().focus().setParagraph().run(),
      isActive: () => props.editor?.isActive('paragraph')
    },
    {
      icon: 'list-unordered',
      title: 'Bullet List',
      action: () => props.editor?.chain().focus().toggleBulletList().run(),
      isActive: () => props.editor?.isActive('bulletList')
    },
    {
      icon: 'list-ordered',
      title: 'Ordered List',
      action: () => props.editor?.chain().focus().toggleOrderedList().run(),
      isActive: () => props.editor?.isActive('orderedList')
    },
    {
      icon: 'list-check-2',
      title: 'Task List',
      action: () => props.editor?.chain().focus().toggleTaskList().run(),
      isActive: () => props.editor?.isActive('taskList')
    },
    {
      icon: 'code-box-line',
      title: 'Code Block',
      action: () => props.editor?.chain().focus().toggleCodeBlock().run(),
      isActive: () => props.editor?.isActive('codeBlock')
    },
    {
      type: 'divider'
    },
    {
      icon: 'double-quotes-l',
      title: 'Blockquote',
      action: () => props.editor?.chain().focus().toggleBlockquote().run(),
      isActive: () => props.editor?.isActive('blockquote')
    },
    {
      icon: 'separator',
      title: 'Horizontal Rule',
      action: () => props.editor?.chain().focus().setHorizontalRule().run()
    },
    {
      type: 'divider'
    },
    {
      icon: 'text-wrap',
      title: 'Hard Break',
      action: () => props.editor?.chain().focus().setHardBreak().run()
    },
    {
      icon: 'format-clear',
      title: 'Clear Format',
      action: () =>
        props.editor?.chain().focus().clearNodes().unsetAllMarks().run()
    },
    {
      type: 'divider'
    },
    {
      icon: 'arrow-go-back-line',
      title: 'Undo',
      action: () => props.editor?.chain().focus().undo().run()
    },
    {
      icon: 'arrow-go-forward-line',
      title: 'Redo',
      action: () => props.editor?.chain().focus().redo().run()
    }
  ]
  </script>
  
  <style lang="scss">
  .divider {
    background-color: rgba(#fff, 0.25);
    height: 1.25rem;
    margin-left: 0.5rem;
    margin-right: 0.75rem;
    width: 1px;
    display: inline-block;
  }
  </style>
  

最后修改Edit/index.vue,引入EditorMenu/index.vue,修改代码如下。

<template>
  <div class="EditMain">
    <div class="lefttools"></div>
    <div class="editor">
      <div class="editorcard">
        <div class="toptools">
          <EditorMenu :editor="editor" />
        </div>
        <div class="editcont">
          <EditorContent
          style="padding: 8px;  overflow-y: auto;"
          :editor="editor"
        />
        </div>
        <div class="bottomcount">
          字数统计:
          {{ editor?.storage.characterCount.characters() }}
        </div>
      </div>
    </div>
    
    <div class="righttools"></div>
  </div>
  </template>
  
  <script lang="ts">
  import { defineComponent, onMounted, onBeforeUnmount, ref,watch } from 'vue';
  import { Editor, EditorContent, useEditor, BubbleMenu  } from '@tiptap/vue-3';
  import { storeToRefs } from 'pinia'
  import Underline from '@tiptap/extension-underline'

  // 列表
  import ListItem from '@tiptap/extension-list-item'
  import OrderedList from '@tiptap/extension-ordered-list'
  import BulletList from '@tiptap/extension-bullet-list'
  // 代码
  import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
  import css from 'highlight.js/lib/languages/css'
  import js from 'highlight.js/lib/languages/javascript'
  import ts from 'highlight.js/lib/languages/typescript'
  import html from 'highlight.js/lib/languages/xml'
  import { common, createLowlight } from 'lowlight'
  const lowlight = createLowlight()
  lowlight.register({ html, ts, css, js })
  // 字数统计
import CharacterCount from '@tiptap/extension-character-count'
import Heading from '@tiptap/extension-heading'
import StarterKit from '@tiptap/starter-kit'
import Placeholder from '@tiptap/extension-placeholder'
import { UndoRound, MoreHorizOutlined } from '@vicons/material'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'

  
  // 使用Pinia
  import { useEditorStore } from '@/stores'
  import EditorMenu from './EditorMenu/index.vue'


  export default defineComponent({
    components: {
      EditorContent,
      EditorMenu,
    },
  
    setup() {
      // 使用ref创建可变的响应式引用
      // 编辑器初始化
      const editor = useEditor({
          content: ``,
          extensions: [
            StarterKit,
            TaskList,
            TaskItem,
            Placeholder.configure({
              placeholder: '开始输入文本 …'
            }),
            OrderedList,
            BulletList,
            ListItem,
            CharacterCount.configure({
            limit: 10000
          })
          ],
          injectCSS: false,

        })
  
      onMounted(() => {
  
      });
  
      // 在组件卸载前销毁Editor实例
      onBeforeUnmount(() => {
        editor.value?.destroy();
      });
  
      // 返回editor供模板使用
      return { editor };
    },
  });
  </script>
  <style>
  .EditMain{
    position: relative;
    width:100vw;
    height: 100vh;

    display: grid;
    grid-template-columns: 20% 60% 20%;
  
  }
  .lefttools{
    background-color: rgb(111 118 177 / 60%);
    height: 100%;
    width: 100%;
  }
  .righttools{
    background-color: rgb(206 226 117);
    height: 100%;
    width: 100%;
  }
  .editor{
 
  }
  .editorcard{
    position: relative;
    width:95%;
    height: 95%;
    left: 2.5%;
    top:2.5%;
    display: grid;
    grid-template-rows: 5% 92% 3%;
    border: 1px solid #4f5c5765;
  }
  .editorcard .editor{
    position: relative;
    width:100%;
    height: 100%;
    left: 0;
    top:0;
    display: grid;
    grid-template-rows: 10% 90%;
  }
  .editorcard .editor{
    position: relative;
    width:100%;
    height: 100%;
    left: 0;
    top:0;
    display: grid;
    grid-template-rows: 10% 90%;
  }
  .toptools{
    background-color: rgba(207, 220, 245, 0.199);
    border-bottom: 1px dashed #9ca19f65;
  }
  .bottomcount{
    background-color: rgba(207, 220, 245, 0.199);
    border-top: 1px dashed #9ca19f65;
    height: 100%;
    width: 100%;
    display: grid;
    grid-template-columns: 100%;
    grid-template-rows: 100%;
    justify-items: center;
    align-items: center;
  }
  .editcont{
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
  </style>
  
  <style lang="scss">
  b {
    font-weight: bold;
  }
  .ProseMirror {
    overflow-y: scroll;
  }
  .ProseMirror p {
    margin: 0;
  }
  .ProseMirror:focus {
    outline: none;
  }
  .tiptap p.is-editor-empty:first-child::before {
    color: #adb5bd;
    content: attr(data-placeholder);
    float: left;
    height: 0;
    pointer-events: none;
  }
  
  .tiptap {
    > * + * {
      margin-top: 0.75em;
    }
  
    ul {
      padding: 0 2rem;
      list-style: square;
    }
    ol {
      padding: 0 2rem;
      list-style: decimal;
    }
    table {
      border-collapse: collapse;
      table-layout: fixed;
      width: 100%;
      margin: 0;
      overflow: hidden;
  
      td,
      th {
        min-width: 1em;
        border: 2px solid #ced4da;
        padding: 3px 5px;
        vertical-align: top;
        box-sizing: border-box;
        position: relative;
  
        > * {
          margin-bottom: 0;
        }
      }
  
      th {
        font-weight: bold;
        text-align: left;
        background-color: #f1f3f5;
      }
  
      .selectedCell:after {
        z-index: 2;
        position: absolute;
        content: '';
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        background: rgba(200, 200, 255, 0.4);
        pointer-events: none;
      }
  
      .column-resize-handle {
        position: absolute;
        right: -2px;
        top: 0;
        bottom: -2px;
        width: 4px;
        background-color: #adf;
        pointer-events: none;
      }
  
      p {
        margin: 0;
      }
    }
    pre {
      background: #0d0d0d;
      color: #fff;
      font-family: 'JetBrainsMono', monospace;
      padding: 0.75rem 1rem;
      border-radius: 0.5rem;
  
      code {
        color: inherit;
        padding: 0;
        background: none;
        font-size: 0.8rem;
      }
  
      .hljs-comment,
      .hljs-quote {
        color: #616161;
      }
  
      .hljs-variable,
      .hljs-template-variable,
      .hljs-attribute,
      .hljs-tag,
      .hljs-name,
      .hljs-regexp,
      .hljs-link,
      .hljs-name,
      .hljs-selector-id,
      .hljs-selector-class {
        color: #f98181;
      }
      .hljs-number,
      .hljs-meta,
      .hljs-built_in,
      .hljs-builtin-name,
      .hljs-literal,
      .hljs-type,
      .hljs-params {
        color: #fbbc88;
      }
  
      .hljs-string,
      .hljs-symbol,
      .hljs-bullet {
        color: #b9f18d;
      }
  
      .hljs-title,
      .hljs-section {
        color: #faf594;
      }
  
      .hljs-keyword,
      .hljs-selector-tag {
        color: #70cff8;
      }
  
      .hljs-emphasis {
        font-style: italic;
      }
  
      .hljs-strong {
        font-weight: 700;
      }
    }
  }
  
  .tableWrapper {
    overflow-x: auto;
  }
  
  .resize-cursor {
    cursor: ew-resize;
    cursor: col-resize;
  }
  </style>

重新打开页面,不出意外的话加上了顶部的工具栏,效果如下所示:
image.png

5、字数统计

在之前预留的bottomcount中加入代码进行字数的统计,代码如下。

<div class="bottomcount">
  字数统计:
  {{ editor?.storage.characterCount.characters() }}
</div>

效果如下,在编辑器底部可以看到字数统计信息。
image.png

6、大纲

编写一个识别标题的的函数,每当富文本编辑器创建或者状态发生改变时,都会进行调用这个函数,从而实现大纲的效果,以下代码都添加到Edit/index.vue中,函数如下:

// 加载headings
const loadHeadings = () => {
  const headings = [] as any[]
  if (!editor.value) return
  const transaction = editor.value.state.tr
  if (!transaction) return

  editor.value?.state.doc.descendants((node, pos) => {
    if (node.type.name === 'heading') {
      console.log(pos, node)
      const start = pos
      const end = pos + node.content.size
      // const end = pos + node
      const id = `heading-${headings.length + 1}`
      if (node.attrs.id !== id) {
        transaction?.setNodeMarkup(pos, undefined, {
          ...node.attrs,
          id
        })
      }

      headings.push({
        level: node.attrs.level,
        text: node.textContent,
        start,
        end,
        id
      })
    }
  })

  transaction?.setMeta('addToHistory', false)
  transaction?.setMeta('preventUpdate', true)

  editor.value?.view.dispatch(transaction)
  editorStore.setHeadings(headings)
}

heading的信息会在其他地方被使用,可以使用pinia存储在editStore里。下面代码添加到src/router/index.ts中。

import { defineStore} from 'pinia'
import { h, ref, type Component } from 'vue'

export const mainStore = defineStore('main',{
  state:()=>{
    return {
        helloPinia:'你好 Pinia!'
    }
  },
  getters:{},
  actions:{}
})

export const useEditorStore = defineStore('editor', () => {
  const headings = ref()
  const activeHeading = ref()
  const editorInstance = ref()
  const setHeadings = (data) => {
    headings.value = data
  }
  const setActiveHeading = (data) => {
    activeHeading.value = data
  }
  const setEditorInstance = (data) => {
    console.log(editorInstance.value)

    editorInstance.value = data
  }
  return {
    headings,
    setHeadings,
    activeHeading,
    setActiveHeading,
    editorInstance,
    setEditorInstance
  }
})

在src文件夹下创建Outline/index.vue组件,用于展示大纲,代码如下:

<script setup lang="ts">
import { h, ref, type Component } from 'vue'

import { useEditorStore } from '@/store'
import { storeToRefs } from 'pinia'

const editorStore = useEditorStore()
const { headings } = storeToRefs(editorStore)
/**
 * 左侧区域
 */
const handleHeadingClick = (data) => {
  setActiveHeading(data)
}
</script>

<template>
  <div class="outline__list" style="display: flex; flex-direction: column;">
    <h2 class="text-gray-400">大纲</h2>
    <template v-for="(heading, index) in headings" :key="index">
      <el-popover
        trigger="click"
        placement="right"
      >
        <template #reference>
          <el-button
            @click="handleHeadingClick(heading.text)"
            text
            class="outline__item"
            :class="`outline__item--${heading.level}`"
          >
            {{ heading.text }}
            <el-icon v-if="heading.icon"><component :is="heading.icon"/></el-icon>
          </el-button>
        </template>
        <!-- 如果需要弹出内容,请在这里添加 -->
      </el-popover>
    </template>
  </div>
</template>

<style scoped lang="scss">
.outline {
  opacity: 0.75;
  border-radius: 0.5rem;
  padding: 0.75rem;
  background: rgba(black, 0.1);

  &__list {
    list-style: none;
    font-size: 18px;
    padding: 0;
  }

  &__item {
    a:hover {
      opacity: 0.5;
    }
    &--1 {
      font-size: 23px;
    }
    &--3 {
      padding-left: 1rem;
    }

    &--4 {
      padding-left: 2rem;
    }

    &--5 {
      padding-left: 3rem;
    }

    &--6 {
      padding-left: 4rem;
    }
  }
}
</style>

在Edit/index.vue中进行引入(完整代码):

<template>
  <div class="EditMain">
    <div class="lefttools"></div>
    <div class="editor">
      <div class="editorcard">
        <div class="toptools">
          <EditorMenu :editor="editor" />
        </div>
        <div class="editcont">
          <EditorContent
          style="padding: 8px;  overflow-y: auto;"
          :editor="editor"
        />
        </div>
        <div class="bottomcount">
          字数统计:
          {{ editor?.storage.characterCount.characters() }}
        </div>
      </div>
    </div>
    <div class="righttools">
      <Outline></Outline>
    </div>
  </div>
</template>
<script lang="ts" setup>
  import { defineComponent, onMounted, onBeforeUnmount, ref,watch } from 'vue';
  import { Editor, EditorContent, useEditor, BubbleMenu  } from '@tiptap/vue-3';
  import { storeToRefs } from 'pinia'
  import Underline from '@tiptap/extension-underline'
  // 列表
  import ListItem from '@tiptap/extension-list-item'
  import OrderedList from '@tiptap/extension-ordered-list'
  import BulletList from '@tiptap/extension-bullet-list'
  // 代码
  import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
  import css from 'highlight.js/lib/languages/css'
  import js from 'highlight.js/lib/languages/javascript'
  import ts from 'highlight.js/lib/languages/typescript'
  import html from 'highlight.js/lib/languages/xml'
  import { common, createLowlight } from 'lowlight'
  // 字数统计
  import CharacterCount from '@tiptap/extension-character-count'
  import Heading from '@tiptap/extension-heading'
  import StarterKit from '@tiptap/starter-kit'
  import Placeholder from '@tiptap/extension-placeholder'
  import { UndoRound, MoreHorizOutlined } from '@vicons/material'
  import TaskItem from '@tiptap/extension-task-item'
  import TaskList from '@tiptap/extension-task-list'
  import Outline from './Outline/index.vue'
    // 使用Pinia
  import { useEditorStore } from '@/store'
  import EditorMenu from './EditorMenu/index.vue'
  import { defineStore } from 'pinia'
  import { ElMessage } from 'element-plus';

  const lowlight = createLowlight()
  lowlight.register({ html, ts, css, js })
  const editorStore = useEditorStore()
  // 加载headings
  const loadHeadings = () => {
          const headings = [] as any[]
          if (!editor.value) return
          const transaction = editor.value.state.tr
          if (!transaction) return

          editor.value?.state.doc.descendants((node, pos) => {
            if (node.type.name === 'heading') {
              console.log(pos, node)
              const start = pos
              const end = pos + node.content.size
              // const end = pos + node
              const id = `heading-${headings.length + 1}`
              if (node.attrs.id !== id) {
                transaction?.setNodeMarkup(pos, undefined, {
                  ...node.attrs,
                  id
                })
              }

              headings.push({
                level: node.attrs.level,
                text: node.textContent,
                start,
                end,
                id
              })
            }
          })

          transaction?.setMeta('addToHistory', false)
          transaction?.setMeta('preventUpdate', true)

          editor.value?.view.dispatch(transaction)
          editorStore.setHeadings(headings)
    }
  // 使用ref创建可变的响应式引用
  // 编辑器初始化
  const editor = useEditor({
          content: ``,
          extensions: [
          StarterKit.configure({
              heading: {
                levels: [1, 2, 3,4,5],
              },
            }),
            TaskList,
            TaskItem,
            Placeholder.configure({
              placeholder: '开始输入文本 …'
            }),
            OrderedList,
            BulletList,
            ListItem,
            CharacterCount.configure({
            limit: 10000
          })
          ],
          onUpdate({ edit }) {
            loadHeadings()
            editorStore.setEditorInstance(editor.value)
          },
          onCreate({ edit }) {
            loadHeadings()
            editorStore.setEditorInstance(editor.value)
          },
          injectCSS: false,

  })
  </script>
  <style>
  .EditMain{
    position: relative;
    width:100vw;
    height: 100vh;

    display: grid;
    grid-template-columns: 20% 60% 20%;
  
  }
  .lefttools{
    background-color: rgb(111 118 177 / 60%);
    height: 100%;
    width: 100%;
  }
  .righttools{
    background-color: rgb(206 226 117);
    height: 100%;
    width: 100%;
  }
  .editor{
 
  }
  .editorcard{
    position: relative;
    width:95%;
    height: 95%;
    left: 2.5%;
    top:2.5%;
    display: grid;
    grid-template-rows: 5% 92% 3%;
    border: 1px solid #4f5c5765;
  }
  .editorcard .editor{
    position: relative;
    width:100%;
    height: 100%;
    left: 0;
    top:0;
    display: grid;
    grid-template-rows: 10% 90%;
  }
  .editorcard .editor{
    position: relative;
    width:100%;
    height: 100%;
    left: 0;
    top:0;
    display: grid;
    grid-template-rows: 10% 90%;
  }
  .toptools{
    background-color: rgba(207, 220, 245, 0.199);
    border-bottom: 1px dashed #9ca19f65;
  }
  .bottomcount{
    background-color: rgba(207, 220, 245, 0.199);
    border-top: 1px dashed #9ca19f65;
    height: 100%;
    width: 100%;
    display: grid;
    grid-template-columns: 100%;
    grid-template-rows: 100%;
    justify-items: center;
    align-items: center;
  }
  .editcont{
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
  </style>
  
  <style lang="scss">
  b {
    font-weight: bold;
  }
  .ProseMirror {
    overflow-y: scroll;
  }
  .ProseMirror p {
    margin: 0;
  }
  .ProseMirror:focus {
    outline: none;
  }
  .tiptap p.is-editor-empty:first-child::before {
    color: #adb5bd;
    content: attr(data-placeholder);
    float: left;
    height: 0;
    pointer-events: none;
  }
  
  .tiptap {
    > * + * {
      margin-top: 0.75em;
    }
  
    ul {
      padding: 0 2rem;
      list-style: square;
    }
    ol {
      padding: 0 2rem;
      list-style: decimal;
    }
    table {
      border-collapse: collapse;
      table-layout: fixed;
      width: 100%;
      margin: 0;
      overflow: hidden;
  
      td,
      th {
        min-width: 1em;
        border: 2px solid #ced4da;
        padding: 3px 5px;
        vertical-align: top;
        box-sizing: border-box;
        position: relative;
  
        > * {
          margin-bottom: 0;
        }
      }
  
      th {
        font-weight: bold;
        text-align: left;
        background-color: #f1f3f5;
      }
  
      .selectedCell:after {
        z-index: 2;
        position: absolute;
        content: '';
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        background: rgba(200, 200, 255, 0.4);
        pointer-events: none;
      }
  
      .column-resize-handle {
        position: absolute;
        right: -2px;
        top: 0;
        bottom: -2px;
        width: 4px;
        background-color: #adf;
        pointer-events: none;
      }
  
      p {
        margin: 0;
      }
    }
    pre {
      background: #0d0d0d;
      color: #fff;
      font-family: 'JetBrainsMono', monospace;
      padding: 0.75rem 1rem;
      border-radius: 0.5rem;
  
      code {
        color: inherit;
        padding: 0;
        background: none;
        font-size: 0.8rem;
      }
  
      .hljs-comment,
      .hljs-quote {
        color: #616161;
      }
  
      .hljs-variable,
      .hljs-template-variable,
      .hljs-attribute,
      .hljs-tag,
      .hljs-name,
      .hljs-regexp,
      .hljs-link,
      .hljs-name,
      .hljs-selector-id,
      .hljs-selector-class {
        color: #f98181;
      }
      .hljs-number,
      .hljs-meta,
      .hljs-built_in,
      .hljs-builtin-name,
      .hljs-literal,
      .hljs-type,
      .hljs-params {
        color: #fbbc88;
      }
  
      .hljs-string,
      .hljs-symbol,
      .hljs-bullet {
        color: #b9f18d;
      }
  
      .hljs-title,
      .hljs-section {
        color: #faf594;
      }
  
      .hljs-keyword,
      .hljs-selector-tag {
        color: #70cff8;
      }
  
      .hljs-emphasis {
        font-style: italic;
      }
  
      .hljs-strong {
        font-weight: 700;
      }
    }
  }
  
  .tableWrapper {
    overflow-x: auto;
  }
  
  .resize-cursor {
    cursor: ew-resize;
    cursor: col-resize;
  }
  </style>

实现的效果大致如下,样式可以根据需求自行调整:
image.png

六、划词AI润色

1、文本划词

在编辑器的操作过程中,当文本中的词汇被用户轻松划选时,随即展现的一系列AI辅助功能无疑极大地提升了用户体验和工作效率。接下来,将深入探讨这一功能的具体实现方式,以确保用户能够更为便捷地享受到这些智能化的编辑服务。

1.1、设计AI操作列表

在Edit/index.vue的中设计一个列表,如下:

<ul v-show="visiblemenu" :style="{ left: position.left + 'px', top: position.top + 'px', display: (visiblemenu ? 'block' : 'none') }" class="contextmenu">
      <div class="item"  @click="polish()">
          <el-icon><Service /></el-icon>
           润色
      </div>
      <div class="item" @click="continuation()">
          <el-icon><Service /></el-icon>
           续写
      </div>
</ul>

在编辑器中加入变量

ref="filecont"

先设计好样式,编写css代码:

<style lang="less" scoped>
   .contextmenu {
     width: 180px;
     margin: 0;
     background: #fff;
     z-index: 3000;
     position: absolute;
     list-style-type: none;
     padding:5px;
     padding-left: 15px;
     border-radius: 4px;
     font-size: 12px;
     font-weight: 400;
     color: #333;
     box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
     display: grid;
     grid-template-columns:50% 50%;
     .item {
       height: 35px;
       width:100%;
       line-height: 35px;
       color: rgb(29, 33, 41);
       cursor: pointer;
     }
     .item:hover {
       background: rgb(229, 230, 235);
     }
   }
</style>
1.2、定义所需函数和变量

有一些库需要引入,包括图标、弹框等,还有一些变量和函数需要定义,包括判断是否划词、操作的函数,在   


import { ElMessage } from 'element-plus';

const aipolish = ref("")
const aicontinuation = ref("")
const visiblemenu = ref(false)
const position = ref({
  top: 0,
  left: 0
})
var hasmove=ref(false);
var hisstring:any;
//进行润色的函数
const polish=()=>{
  ailoading.value=true
  visiblemenu.value = false;
  let formData = new FormData();
  formData.append("username","123456");
  formData.append("key","xxxxxxx");
  formData.append("cont",hisstring);
  let url = 'http://127.0.0.1:5000/getpolish' //访问后端接口的url
  let method = 'post'
  axios({
    method,
    url,
    data: formData,
  }).then(res => {
    console.log(res.data);
    var tpcard1={"title":"ai辅助评审","cont":hisstring,"review":res.data}
    ailist.value.push(tpcard1)
    navigator.clipboard.writeText(res.data)
    showMessage()
    ailoading.value=false
  });
}

//进行aiaireview
const continuation=()=>{
  ailoading.value=true
  visiblemenu.value = false;
  let formData = new FormData();
  formData.append("username","123456");
  formData.append("key","xxxxxxx");
  formData.append("cont",hisstring);
  let url = 'http://127.0.0.1:5000/getpolish' //访问后端接口的url
  let method = 'post'
  axios({
    method,
    url,
    data: formData,
  }).then(res => {
    console.log(res.data);
   
    showMessage()
    ailoading.value=false
  });
}

// 获取选中的文字
const selecttext= (e:MouseEvent)=>{
    selection = window.getSelection();
    if(selection!=null&&hisstring!=selection){
      var content = selection.toString();
      if(content!=""){
          var rect = filecont.value.getBoundingClientRect();
          visiblemenu.value = true
          // alert(e.clientY)
          // alert(e.clientX)
          position.value.top =  e.clientY;
          position.value.left =e.clientX;
          hisstring=content
        }
      // alert(content)
    }
    else{
      hisstring=""
    }
  }
  //鼠标移动
const mousemove=()=>{
    hasmove.value=true;
  }
  //鼠标点击
 const notsee=()=>{
    visiblemenu.value = false;
    selection=null;
  }
  //滚轮滚动
const hasscroll=()=>{
    visiblemenu.value = false;
    // window.getSelection().removeAllRanges()
  }

设置好后,对EditorContent进行监听

<EditorContent
    @scroll="hasscroll()"
    @mousemove="mousemove()" 
    @mouseup="selecttext($event)"
    style="padding: 8px;  overflow-y: auto;"
    :editor="editor"
/>
1.2、设计AI操作列表

完整代码如下所示:

<template>
  <div class="EditMain" ref="filecont" @mousedown="notsee()" >
    <ul @mousedown="see()" v-show="visiblemenu" :style="{ left: position.left + 'px', top: position.top + 'px', display: (visiblemenu ? 'grid' : 'none') }" class="contextmenu">
        <div class="item"  @click="polish()">
            <el-icon><Brush /></el-icon>
            润色
        </div>
        <div class="item" @click="continuation()">
            <el-icon><EditPen /></el-icon>
            续写
        </div>
    </ul>
    <div class="lefttools"></div>
    <div class="editor">
      <div class="editorcard" >
        <div class="toptools">
          <EditorMenu :editor="editor" />
        </div>
        <div class="editcont" >
          <EditorContent
          @scroll="hasscroll()"
          @mousedown="notsee()"
          @mousemove="mousemove()" 
          @mouseup="selecttext($event)"
          style="padding: 8px;  overflow-y: auto;"
          :editor="editor"
        />
        </div>
       
        <div class="bottomcount">
          字数统计:
          {{ editor?.storage.characterCount.characters() }}
        </div>
      </div>
    </div>
    
    <div class="righttools">
      <Outline></Outline>
    </div>
  </div>
  </template>
  <script lang="ts" setup>
  import {Brush,EditPen,} from '@element-plus/icons-vue'
  import { defineComponent, onMounted, onBeforeUnmount, ref,watch } from 'vue';
  import { Editor, EditorContent, useEditor, BubbleMenu  } from '@tiptap/vue-3';
  import { storeToRefs } from 'pinia'
  import Underline from '@tiptap/extension-underline'
  // 列表
  import ListItem from '@tiptap/extension-list-item'
  import OrderedList from '@tiptap/extension-ordered-list'
  import BulletList from '@tiptap/extension-bullet-list'
  // 代码
  import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
  import css from 'highlight.js/lib/languages/css'
  import js from 'highlight.js/lib/languages/javascript'
  import ts from 'highlight.js/lib/languages/typescript'
  import html from 'highlight.js/lib/languages/xml'
  import { common, createLowlight } from 'lowlight'
  // 字数统计
  import CharacterCount from '@tiptap/extension-character-count'
  import Heading from '@tiptap/extension-heading'
  import StarterKit from '@tiptap/starter-kit'
  import Placeholder from '@tiptap/extension-placeholder'
  import { UndoRound, MoreHorizOutlined } from '@vicons/material'
  import TaskItem from '@tiptap/extension-task-item'
  import TaskList from '@tiptap/extension-task-list'
  import Outline from './Outline/index.vue'
    // 使用Pinia
  import { useEditorStore } from '@/store'
  import EditorMenu from './EditorMenu/index.vue'
  import { defineStore } from 'pinia'
  import { ElMessage } from 'element-plus';


  const lowlight = createLowlight()
  lowlight.register({ html, ts, css, js })

    const aipolish = ref("")
    const filecont = ref(null);
    const aicontinuation = ref("")
    const visiblemenu = ref(false)
    const position = ref({
          top: 0,
          left: 0
    })
    var hasmove=ref(false);
    var hisstring:any;
    var selection:any;
    //进行润色的函数
    const polish=()=>{
          ailoading.value=true
          visiblemenu.value = false;
          let formData = new FormData();
          formData.append("username","123456");
          formData.append("key","xxxxxxx");
          formData.append("cont",hisstring);
          let url = 'http://127.0.0.1:5000/getpolish' //访问后端接口的url
          let method = 'post'
          axios({
            method,
            url,
            data: formData,
          }).then(res => {
            console.log(res.data);
            var tpcard1={"title":"ai辅助评审","cont":hisstring,"review":res.data}
            ailist.value.push(tpcard1)
            navigator.clipboard.writeText(res.data)
            showMessage()
            ailoading.value=false
          });
      }
    //进行aireview
    const continuation=()=>{
          ailoading.value=true
          visiblemenu.value = false;
          let formData = new FormData();
          formData.append("username","123456");
          formData.append("key","xxxxxxx");
          formData.append("cont",hisstring);
          let url = 'http://127.0.0.1:5000/getpolish' //访问后端接口的url
          let method = 'post'
          axios({
            method,
            url,
            data: formData,
          }).then(res => {
            console.log(res.data);
          
            showMessage()
            ailoading.value=false
          });
      }
    // 获取选中的文字
    const selecttext= (e:MouseEvent)=>{
            selection = window.getSelection();
            if(selection!=null&&hisstring!=selection){
              var content = selection.toString();
              if(content!=""){
                  var rect = filecont.value.getBoundingClientRect();
                  visiblemenu.value = true
                  // alert(e.clientY)
                  // alert(e.clientX)
                  position.value.top =  e.clientY;
                  position.value.left =e.clientX;
                  hisstring=content
                }
              // alert(content)
            }
            else{
              hisstring=""
            }
      }
    //鼠标移动
    const mousemove=()=>{
            hasmove.value=true;
      }
    //鼠标点击
    const notsee=()=>{
            visiblemenu.value = false;
            // selection.value="";
      }
    const see=()=>{
            visiblemenu.value = true;
            // selection.value="";
      }
    //滚轮滚动
    const hasscroll=()=>{
            visiblemenu.value = false;
            // window.getSelection().removeAllRanges()
      }
    const editorStore = useEditorStore()
     // 加载headings
    const loadHeadings = () => {
          const headings = [] as any[]
          if (!editor.value) return
          const transaction = editor.value.state.tr
          if (!transaction) return

          editor.value?.state.doc.descendants((node, pos) => {
            if (node.type.name === 'heading') {
              console.log(pos, node)
              const start = pos
              const end = pos + node.content.size
              // const end = pos + node
              const id = `heading-${headings.length + 1}`
              if (node.attrs.id !== id) {
                transaction?.setNodeMarkup(pos, undefined, {
                  ...node.attrs,
                  id
                })
              }

              headings.push({
                level: node.attrs.level,
                text: node.textContent,
                start,
                end,
                id
              })
            }
          })

          transaction?.setMeta('addToHistory', false)
          transaction?.setMeta('preventUpdate', true)

          editor.value?.view.dispatch(transaction)
          editorStore.setHeadings(headings)
      }
    // 使用ref创建可变的响应式引用
    // 编辑器初始化
    const editor = useEditor({
          content: ``,
          extensions: [
          StarterKit.configure({
              heading: {
                levels: [1, 2, 3,4,5],
              },
            }),
            TaskList,
            TaskItem,
            Placeholder.configure({
              placeholder: '开始输入文本 …'
            }),
            OrderedList,
            BulletList,
            ListItem,
            CharacterCount.configure({
            limit: 10000
          })
          ],
          onUpdate({ edit }) {
            loadHeadings()
            editorStore.setEditorInstance(editor.value)
          },
          onCreate({ edit }) {
            loadHeadings()
            editorStore.setEditorInstance(editor.value)
          },
          injectCSS: false,

      })
  </script>
  <style>
  .EditMain{
    position: relative;
    width:100vw;
    height: 100vh;

    display: grid;
    grid-template-columns: 20% 60% 20%;
  
  }
  .lefttools{
    background-color: rgb(111 118 177 / 60%);
    height: 100%;
    width: 100%;
  }
  .righttools{
    background-color: rgb(206 226 117);
    height: 100%;
    width: 100%;
  }
  .editor{
 
  }
  .editorcard{
    position: relative;
    width:95%;
    height: 95%;
    left: 2.5%;
    top:2.5%;
    display: grid;
    grid-template-rows: 5% 92% 3%;
    border: 1px solid #4f5c5765;
  }
  .editorcard .editor{
    position: relative;
    width:100%;
    height: 100%;
    left: 0;
    top:0;
    display: grid;
    grid-template-rows: 10% 90%;
  }
  .editorcard .editor{
    position: relative;
    width:100%;
    height: 100%;
    left: 0;
    top:0;
    display: grid;
    grid-template-rows: 10% 90%;
  }
  .toptools{
    background-color: rgba(207, 220, 245, 0.199);
    border-bottom: 1px dashed #9ca19f65;
  }
  .bottomcount{
    background-color: rgba(207, 220, 245, 0.199);
    border-top: 1px dashed #9ca19f65;
    height: 100%;
    width: 100%;
    display: grid;
    grid-template-columns: 100%;
    grid-template-rows: 100%;
    justify-items: center;
    align-items: center;
  }
  .editcont{
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
  </style>
  
  <style lang="scss">
  b {
    font-weight: bold;
  }
  .ProseMirror {
    overflow-y: scroll;
  }
  .ProseMirror p {
    margin: 0;
  }
  .ProseMirror:focus {
    outline: none;
  }
  .tiptap p.is-editor-empty:first-child::before {
    color: #adb5bd;
    content: attr(data-placeholder);
    float: left;
    height: 0;
    pointer-events: none;
  }
  
  .tiptap {
    > * + * {
      margin-top: 0.75em;
    }
  
    ul {
      padding: 0 2rem;
      list-style: square;
    }
    ol {
      padding: 0 2rem;
      list-style: decimal;
    }
    table {
      border-collapse: collapse;
      table-layout: fixed;
      width: 100%;
      margin: 0;
      overflow: hidden;
  
      td,
      th {
        min-width: 1em;
        border: 2px solid #ced4da;
        padding: 3px 5px;
        vertical-align: top;
        box-sizing: border-box;
        position: relative;
  
        > * {
          margin-bottom: 0;
        }
      }
  
      th {
        font-weight: bold;
        text-align: left;
        background-color: #f1f3f5;
      }
  
      .selectedCell:after {
        z-index: 2;
        position: absolute;
        content: '';
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        background: rgba(200, 200, 255, 0.4);
        pointer-events: none;
      }
  
      .column-resize-handle {
        position: absolute;
        right: -2px;
        top: 0;
        bottom: -2px;
        width: 4px;
        background-color: #adf;
        pointer-events: none;
      }
  
      p {
        margin: 0;
      }
    }
    pre {
      background: #0d0d0d;
      color: #fff;
      font-family: 'JetBrainsMono', monospace;
      padding: 0.75rem 1rem;
      border-radius: 0.5rem;
  
      code {
        color: inherit;
        padding: 0;
        background: none;
        font-size: 0.8rem;
      }
  
      .hljs-comment,
      .hljs-quote {
        color: #616161;
      }
  
      .hljs-variable,
      .hljs-template-variable,
      .hljs-attribute,
      .hljs-tag,
      .hljs-name,
      .hljs-regexp,
      .hljs-link,
      .hljs-name,
      .hljs-selector-id,
      .hljs-selector-class {
        color: #f98181;
      }
      .hljs-number,
      .hljs-meta,
      .hljs-built_in,
      .hljs-builtin-name,
      .hljs-literal,
      .hljs-type,
      .hljs-params {
        color: #fbbc88;
      }
  
      .hljs-string,
      .hljs-symbol,
      .hljs-bullet {
        color: #b9f18d;
      }
  
      .hljs-title,
      .hljs-section {
        color: #faf594;
      }
  
      .hljs-keyword,
      .hljs-selector-tag {
        color: #70cff8;
      }
  
      .hljs-emphasis {
        font-style: italic;
      }
  
      .hljs-strong {
        font-weight: 700;
      }
    }
  }
  
  .tableWrapper {
    overflow-x: auto;
  }
  
  .resize-cursor {
    cursor: ew-resize;
    cursor: col-resize;
  }
  .contextmenu {
    width: 120px;
    margin: 0;
    background: #fff;
    z-index: 3000;
    position: absolute;
    list-style-type: none;
    padding:5px;
    padding-left: 15px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 400;
    color: #333;
    box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.3);
    display: grid;
    grid-template-columns:50% 50%;

  }
  .contextmenu .item {
      height: 35px;
      width:100%;
      line-height: 35px;
      color: rgb(29, 33, 41);
      cursor: pointer;
    }
    .contextmenu .item {
      height: 35px;
      width:100%;
      line-height: 35px;
      color: rgb(29, 33, 41);
      cursor: pointer;
    }

    .contextmenu .item:hover {
      background: rgb(229, 230, 235);
    }
  </style>

实现效果如下:
image.png

2、erniebot安装与使用

接下来配置后端的接口。打开之前配置好的pycharm,选择合适的虚拟环境后(也可以是base环境),需要安装erniebot。

pip install erniebot==0.4.0 -i https://pypi.tuna.tsinghua.edu.cn/simple

安装好后需要获取访问令牌,打开飞桨https://aistudio.baidu.com/account/accessToken,这个页面。注册登录后获取访问令牌,下面备用,更多详细的操作可以参考下面这个说明文档:https://ernie-bot-agent.readthedocs.io/zh-cn/latest/
image.png
image.png
先创建一个test.py,测试代码如下,访问令牌替换为自己的,运行测试:

import erniebot

import erniebot

erniebot.api_type = 'aistudio'
erniebot.access_token = '输入您的token'

response = erniebot.ChatCompletion.create(
    model='ernie-bot',
    messages=[{'role': 'user', 'content': "你是?"}],
)
print(response.get_result())

测试结果,代表可以接入erniebot。
image.png

3、润色接口配置

回到main.py文件,编写如下代码:

from flask import Flask, json, request, jsonify
from flask_cors import CORS
import pymysql
DEBUG = True
app = Flask(__name__)
app.config.from_object(__name__)
CORS(app, resource={r'/*': {'origins': '*'}})
import erniebot

erniebot.api_type = 'aistudio'

@app.route('/getpolish', methods=["GET", "POST"])
def getpolish():
    # 获取用户名
    username= request.form.get("username")
    # 获取用户的访问令牌
    key = request.form.get("key")
    # 获取用户提问内容
    quescont = request.form.get("cont")
    askcont="帮我润色下面这段话:"+quescont

    erniebot.access_token = key
    try:
        response = erniebot.ChatCompletion.create(
            model='ernie-bot',
            messages=[{'role': 'user', 'content':askcont}],
        )
        restext = response['result']
        webdict = {'answer': restext}
        return jsonify(webdict)
    except:
        return "error"

@app.route('/getcontinuation', methods=["GET", "POST"])
def getcontinuation():
    # 获取用户名
    username= request.form.get("username")
    # 获取用户的访问令牌
    key = request.form.get("key")
    # 获取用户提问内容
    quescont = request.form.get("cont")
    askcont="帮我续写下面这段话:"+quescont

    erniebot.access_token = key
    try:
        response = erniebot.ChatCompletion.create(
            model='ernie-bot',
            messages=[{'role': 'user', 'content':askcont}],
        )
        restext = response['result']
        webdict = {'answer': restext}
        return jsonify(webdict)
    except:
        return "error"


if __name__ == '__main__':
    app.run(host="127.0.0.1", port=5000, debug=True)

运行。
image.png

4、润色接口调用

后端运行,之后,在前端进行调用,修改polish函数和continuation函数(注意修改key为自己的访问令牌)。

import axios from 'axios'
//进行润色的函数
const polish=()=>{
  visiblemenu.value = false;
  let formData = new FormData();
  formData.append("username","xxxxxx");
  formData.append("key","xxxxxx");
  formData.append("cont",hisstring);
  let url = 'http://127.0.0.1:5000/getpolish' //访问后端接口的url
  let method = 'post'
  axios({
    method,
    url,
    data: formData,
  }).then(res => {
    alert(res.data.answer)
    console.log(res.data.answer);
  });
}
//进行aiaireview
const continuation=()=>{
  visiblemenu.value = false;
  let formData = new FormData();
  formData.append("username","123456");
  formData.append("key","xxxxxxx");
  formData.append("cont",hisstring);
  let url = 'http://127.0.0.1:5000/getcontinuation' //访问后端接口的url
  let method = 'post'
  axios({
    method,
    url,
    data: formData,
  }).then(res => {
    alert(res.data.answer)
    console.log(res.data.answer);
  });
}

image.png
点击润色,润色完成后弹出结果,至此,完成接口调用。
image.png

  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
PaddlePaddle是一个开源的深度学习平台,可以用于构建和训练深度学习模型。如果你想使用PaddlePaddle,可以通过源码编译的方式来安装。首先,你需要在Git Bash中执行以下两条命令来将PaddlePaddle的源码克隆到本地,并进入Paddle目录: ``` git clone https://github.com/PaddlePaddle/Paddle.git cd Paddle ``` 接下来,你可以根据自己的需求进行编译。如果你使用的是Windows系统,可以使用源码编译来安装符合你需求的PaddlePaddle版本。具体的编译步骤可以参考官方文档中的Windows下源码编译部分\[2\]。 如果你想在docker镜像中编译PaddlePaddle,可以使用以下命令启动docker镜像并进行编译。如果你需要编译CPU版本,可以使用以下命令: ``` sudo docker run --name paddle-test -v $PWD:/paddle --network=host -it hub.baidubce.com/paddlepaddle/paddle:latest-dev /bin/bash ``` 如果你需要编译GPU版本,可以使用以下命令: ``` sudo nvidia-docker run --name paddle-test -v $PWD:/paddle --network=host -it hub.baidubce.com/paddlepaddle/paddle:latest-dev /bin/bash ``` 以上是关于使用源码编译PaddlePaddle的一些基本步骤和命令。你可以根据自己的需求和操作系统选择适合的方式来安装PaddlePaddle。 #### 引用[.reference_title] - *1* *2* *3* [《PaddlePaddle从入门到炼丹》一——新版本PaddlePaddle的安装](https://blog.csdn.net/qq_33200967/article/details/83052060)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值