# eeed-editor

`eeed-editor` 是一个面向业务文档编辑场景的 Vue 3 富文本编辑器，基于 Tiptap 构建，适合在后台系统、知识库、合同/报告编辑、审批表单、A4 文档和技术文档页面中使用。

它内置文档级编辑体验、A4 页面、大纲、代码块、数学公式、Mermaid、图片处理、表格、Markdown 粘贴、保存/上传回调，以及 Word 和 Markdown 导出能力。

## 特性

- Vue 3 组件化接入
- 只需 `import eeedEditor from "eeed-editor"`，样式会自动注入
- 支持可编辑与只读预览
- 支持普通文档、A4 文档、表单富文本、移动端精简工具栏等场景
- 支持标题大纲、当前章节高亮、点击大纲定位
- A4 / defaultA4 编辑模式内置字数统计
- 支持代码块高亮与一键复制
- 支持数学公式、Mermaid 图表、表格、图片、引用、分割线、任务列表
- 支持 Markdown 内容初始化与 Markdown 粘贴
- 支持保存、上传、内容变更三个业务回调
- 支持导出 Word 与 Markdown

## 安装

```bash
npm install eeed-editor
```

或者：

```bash
pnpm add eeed-editor
```

`vue` 和 `element-plus` 是 peer dependencies，请确保业务项目中已经安装：

```bash
npm install vue element-plus
```

## 快速开始

> 注意：`eeedEditor` 外层必须包一个稳定容器，并设置 `position: relative` 和明确的宽高。编辑器内部默认会根据父容器定位和计算高度，尤其是 A4 文档模式、工具栏固定布局、大纲滚动等场景。

```vue
<template>
  <div class="editor-wrapper">
    <eeedEditor
      title="产品方案"
      :content="content"
      :config="config"
      :onSave="handleSave"
      :onUploadFile="handleUploadFile"
      :onChange="handleChange"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import eeedEditor from "eeed-editor";

const content = ref("<h1>产品方案</h1><p>这里是正文内容。</p>");

const latestHtml = ref("");
const latestText = ref("");
const latestMarkdown = ref("");

const config = {
  mode: "doc",
  editable: true,
  doc: {
    page: {
      mode: "default",
      style: {
        position: "absolute",
        backgroundColor: "#f3f6f9",
      },
    },
    headerBar: {
      visible: true,
      home: { visible: true, icon: "home", path: "/" },
      export: { visible: true },
      title: { visible: true },
    },
    toolBar: {
      visible: true,
      align: "start",
    },
  },
} as const;

const handleSave = async (title: string, html: string) => {
  // 这里接入你的保存接口
  await Promise.resolve({ title, html });
  return true;
};

const handleUploadFile = async (file: File) => {
  const formData = new FormData();
  formData.append("file", file);

  // 替换成你的上传接口
  // const res = await fetch("/api/upload", { method: "POST", body: formData });
  // const data = await res.json();
  // return data.url;

  return URL.createObjectURL(file);
};

const handleChange = (html: string, text: string, markdown: string) => {
  latestHtml.value = html;
  latestText.value = text;
  latestMarkdown.value = markdown;
};
</script>

<style scoped>
.editor-wrapper {
  position: relative;
  width: 100%;
  height: 720px;
}
</style>
```

## Props

| 名称 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `title` | `string` | 否 | 文档标题，保存和导出时会使用 |
| `content` | `string` | 否 | 编辑器内容，推荐传 HTML，也支持 Markdown 文本 |
| `config` | `EditorConfig` | 否 | 编辑器模式、页面、头部栏、工具栏等配置 |
| `onSave` | `(title: string, html: string) => boolean \| Promise<boolean>` | 否 | 保存回调，返回 `true` 时显示保存成功提示 |
| `onUploadFile` | `(file: File) => string \| Promise<string>` | 否 | 文件上传回调，返回可访问的文件 URL |
| `onChange` | `(html: string, text: string, markdown: string) => void` | 否 | 内容变更回调 |

## Config 类型

```ts
type ToolBarItem =
  | "history"
  | "formatBrush"
  | "clearFormatting"
  | "fontFamily"
  | "heading"
  | "fontSize"
  | "fontColor"
  | "highlightColor"
  | "bold"
  | "italic"
  | "underline"
  | "strike"
  | "blockquote"
  | "horizontalRule"
  | "inlineCode"
  | "table"
  | "list"
  | "textAlign"
  | "indent"
  | "lineHeight"
  | "insert";

type EditorConfig = {
  mode?: "doc";
  editable?: boolean;
  doc?: {
    page?: {
      mode?: "default" | "defaultA4" | "A4";
      style?: {
        position?: "absolute" | "relative";
        backgroundColor?: string;
      };
    };
    headerBar?: {
      visible?: boolean;
      home?: {
        visible?: boolean;
        icon?: "back" | "home";
        path?: string;
      };
      export?: {
        visible?: boolean;
      };
      title?: {
        visible?: boolean;
      };
    };
    toolBar?: {
      visible?: boolean;
      align?: "start" | "center";
      items?: ToolBarItem[];
    };
    readOnly?: {
      theme?: "article" | "ai" | "plain";
      backgroundColor?: string;
      contentBackgroundColor?: string;
      maxWidth?: string;
    };
    content?: {
      sanitize?: boolean;
      streamDebounce?: number;
      streaming?: boolean;
      preserveScroll?: boolean;
      autoScroll?: boolean;
      pauseWhileSelecting?: boolean;
    };
    placeholder?: string;
  };
};
```

## 常用配置说明

| 路径 | 默认值 | 说明 |
| --- | --- | --- |
| `mode` | `"doc"` | 当前主要支持文档模式 |
| `editable` | `true` | 是否可编辑，设置为 `false` 时进入只读预览 |
| `doc.page.mode` | `"default"` | 页面模式，可选普通文档、默认 A4、A4 文档 |
| `doc.page.style.position` | `"absolute"` | 编辑器页面定位方式。默认要求父容器 `position: relative` |
| `doc.page.style.backgroundColor` | `"#f3f6f9"` | 页面背景色；设置后同样作用于只读预览 |
| `doc.readOnly.theme` | `"article"` | 只读主题：`article` 文章卡片、`ai` AI 回答、`plain` 无装饰嵌入 |
| `doc.readOnly.backgroundColor` | 按主题决定 | 只读区域背景色，优先级高于页面背景色 |
| `doc.readOnly.contentBackgroundColor` | 按主题决定 | 只读正文表面颜色 |
| `doc.readOnly.maxWidth` | `"860px"` | 只读正文最大宽度 |
| `doc.content.sanitize` | `true` | 清洗外部 HTML/Markdown，移除脚本、危险标签、事件属性和危险 CSS |
| `doc.content.streamDebounce` | 流式 AI 主题为 `80` | 合并流式内容更新，单位为毫秒；非流式内容默认为 `0` |
| `doc.content.streaming` | `false` | AI 正在生成时设为 `true`，展示生成状态并延迟公式、Mermaid 等重型渲染 |
| `doc.content.preserveScroll` | `true` | 内容更新后保持当前阅读位置 |
| `doc.content.autoScroll` | `true` | 流式生成且用户位于底部时自动跟随新内容；向上阅读后不会强制拉回底部 |
| `doc.content.pauseWhileSelecting` | `true` | 用户选中文字时暂停内容替换，取消选择后自动应用最新内容 |
| `doc.headerBar.visible` | `false` | 是否展示顶部标题栏 |
| `doc.headerBar.home.visible` | `true` | 是否展示返回/首页按钮 |
| `doc.headerBar.home.icon` | `"home"` | 顶部左侧图标，可选 `home` 或 `back` |
| `doc.headerBar.home.path` | `"/"` | 点击返回/首页按钮后的跳转路径 |
| `doc.headerBar.export.visible` | `true` | 是否展示导出入口 |
| `doc.headerBar.title.visible` | `true` | 是否展示标题输入框 |
| `doc.toolBar.visible` | `true` | 是否展示工具栏 |
| `doc.toolBar.align` | `"start"` | 工具栏对齐方式，可选 `start`、`center` |
| `doc.toolBar.items` | `undefined` | 精简工具栏配置，适合移动端或表单字段 |
| `doc.placeholder` | `"输入正文内容"` | 编辑器空白时的占位提示文字 |

## 业务回调

### onSave

点击保存或快捷键保存时触发。

```ts
const handleSave = async (title: string, html: string) => {
  await saveDocument({
    title,
    content: html,
  });

  return true;
};
```

说明：

- `title` 是当前文档标题
- `html` 是编辑器 HTML 内容
- 返回 `true` 会显示保存成功提示
- 返回 `false` 或抛出异常时不会显示成功提示

### onUploadFile

插入图片、粘贴图片或上传文件时触发。

```ts
const handleUploadFile = async (file: File) => {
  const formData = new FormData();
  formData.append("file", file);

  const res = await fetch("/api/upload", {
    method: "POST",
    body: formData,
  });
  const data = await res.json();

  return data.url;
};
```

说明：

- 必须返回一个可访问的 URL
- 如果没有传 `onUploadFile`，图片会尝试以 base64 方式插入
- 生产环境建议接入自己的文件服务或对象存储

### onChange

编辑器内容变化时触发。

```ts
const handleChange = (html: string, text: string, markdown: string) => {
  form.content = html;
  form.text = text;
  form.markdown = markdown;
};
```

说明：

- `html` 是完整 HTML 内容
- `text` 是纯文本内容
- `markdown` 是 Markdown 格式内容（v0.1.13+ 新增）
- 如果业务侧需要自动保存，建议自行做 debounce

## 场景示例

### A4 文档编辑

适合合同、报告、方案、正式文档编辑。

```ts
const config = {
  mode: "doc",
  editable: true,
  doc: {
    page: {
      mode: "A4",
      style: {
        position: "absolute",
        backgroundColor: "#eef2f7",
      },
    },
    headerBar: {
      visible: true,
      home: { visible: true, icon: "back", path: "/" },
      export: { visible: true },
      title: { visible: true },
    },
    toolBar: {
      visible: true,
      align: "center",
    },
  },
} as const;
```

### 表单富文本字段

适合嵌入业务表单、审批表单、弹窗表单。

```ts
const config = {
  mode: "doc",
  editable: true,
  doc: {
    page: {
      mode: "default",
      style: {
        position: "absolute",
        backgroundColor: "#ffffff",
      },
    },
    headerBar: {
      visible: false,
    },
    toolBar: {
      visible: true,
      align: "start",
      items: [
        "history",
        "heading",
        "bold",
        "italic",
        "underline",
        "fontColor",
        "list",
        "insert",
      ],
    },
  },
} as const;
```

### 移动端精简编辑

适合手机端内容维护，只保留必要工具。

```ts
const config = {
  mode: "doc",
  editable: true,
  doc: {
    page: {
      mode: "default",
      style: {
        position: "absolute",
        backgroundColor: "#ffffff",
      },
    },
    headerBar: {
      visible: false,
    },
    toolBar: {
      visible: true,
      align: "start",
      items: ["history", "bold", "italic", "underline", "list", "insert"],
    },
  },
} as const;
```

### 只读预览

适合详情页、审批查看、归档文档展示。

只读预览模式不会展示头部栏、工具栏、大纲和字数统计。默认使用 `article` 主题：浅灰页面背景、白色正文卡片和适合长文阅读的内容宽度。公式、图片和 Mermaid 不会进入编辑状态，图片仍可双击放大查看。

```ts
const config = {
  mode: "doc",
  editable: false,
  doc: {
    readOnly: {
      theme: "article",
    },
  },
} as const;
```

如果希望沿用旧版的透明、无间距嵌入效果，使用 `plain` 主题：

```vue
<template>
  <div class="preview-wrapper">
    <eeedEditor :content="content" :config="previewConfig" />
  </div>
</template>

<script setup lang="ts">
import eeedEditor from "eeed-editor";

const previewConfig = {
  mode: "doc",
  editable: false,
  doc: {
    readOnly: {
      theme: "plain",
    },
    page: {
      style: {
        position: "relative",
      },
    },
  },
} as const;
</script>

<style scoped>
.preview-wrapper {
  position: relative;
  width: 100%;
}
</style>
```

### AI 问答渲染

`content` 可以直接传入 AI 返回的 Markdown，支持标题、列表、表格、代码块、KaTeX 公式和 Mermaid。流式回答过程中持续更新 `content` 即可，编辑器只会应用最后一次完成的异步解析结果。

```ts
import { computed, ref } from "vue";

const isStreaming = ref(true);

const aiPreviewConfig = computed(() => ({
  mode: "doc",
  editable: false,
  doc: {
    page: { style: { position: "relative" } },
    readOnly: {
      theme: "ai",
      maxWidth: "100%",
    },
    content: {
      sanitize: true,
      streamDebounce: 80,
      streaming: isStreaming.value,
      preserveScroll: true,
      autoScroll: true,
      pauseWhileSelecting: true,
    },
  },
} as const));

// aiMarkdown 可以是完整回答，也可以随着流式输出持续更新
const aiMarkdown = `## 分析结果

核心公式：$E = mc^2$

\`\`\`ts
const answer = "支持代码高亮";
\`\`\`

\`\`\`mermaid
graph LR
  Q[问题] --> A[AI 回答]
\`\`\``;

// 流式响应结束后触发公式和 Mermaid 的最终渲染
isStreaming.value = false;
```

外部内容默认会经过安全清洗。只有在内容已经由可信服务端完成严格清洗、并且确实需要保留自定义 HTML 时，才考虑设置 `sanitize: false`。流式输出无需每个 token 都立即重绘，`80ms` 通常能兼顾跟手感和渲染开销。用户停留在底部时会自动跟随生成内容；向上滚动阅读或选中文字后，编辑器会尊重当前阅读状态。

### 技术文档

适合代码块、公式、Mermaid 图表、知识库文章。

```ts
const content = `
<h1>接口说明</h1>
<p>这里是技术文档内容。</p>
<pre><code class="language-typescript">const message: string = "hello";</code></pre>
<p>行内公式：$a^2 + b^2 = c^2$</p>
<pre><code class="language-mermaid">graph TD
  A[开始] --> B[结束]</code></pre>
`;
```

## 内容格式

`content` 推荐传 HTML：

```ts
const content = "<h1>标题</h1><p>正文内容</p>";
```

也可以传 Markdown 文本，编辑器会自动转成 HTML：

```ts
const content = `
# 标题

这是一段 Markdown 内容。

\`\`\`ts
const value = "eeed-editor";
\`\`\`
`;
```

## 数学公式

支持行内公式和块级公式：

```html
<p>行内公式：$a^2 + b^2 = c^2$</p>

<p>块级公式：</p>
<p>$$F(\omega) = \int_{-\infty}^{\infty} f(t) e^{-i\omega t} dt$$</p>
```

## 代码块

支持常见语言代码高亮，并提供复制按钮。

```html
<pre><code class="language-typescript">import { ref } from "vue";

const title = ref("eeed-editor");</code></pre>
```

## Mermaid

可以在技术文档场景中插入 Mermaid 图表内容：

```html
<pre><code class="language-mermaid">graph TD
  A[需求] --> B[开发]
  B --> C[上线]</code></pre>
```

## 导出

当 `doc.headerBar.export.visible` 为 `true` 时，顶部栏会展示导出入口。

当前支持：

- 导出 Word
- 导出 Markdown

```ts
const config = {
  mode: "doc",
  editable: true,
  doc: {
    headerBar: {
      visible: true,
      export: { visible: true },
      title: { visible: true },
    },
  },
} as const;
```

## 大纲

在 A4 / defaultA4 文档模式下，编辑器会根据标题自动生成大纲。

A4 / defaultA4 编辑模式下会在编辑器右下角展示实时字数统计，统计口径为去除空白字符后的正文字符数。

支持：

- 自动提取标题一到标题六
- 点击大纲滚动到对应标题
- 滚动时高亮当前章节
- 空标题显示为“未命名标题”
- 可收起/展开大纲

## 工具栏精简

通过 `doc.toolBar.items` 可以保留部分工具，适合移动端或表单场景。

```ts
const config = {
  mode: "doc",
  editable: true,
  doc: {
    toolBar: {
      visible: true,
      align: "start",
      items: ["history", "bold", "italic", "underline", "list", "insert"],
    },
  },
} as const;
```

可选工具项：

```ts
[
  "history",
  "formatBrush",
  "clearFormatting",
  "fontFamily",
  "heading",
  "fontSize",
  "fontColor",
  "highlightColor",
  "bold",
  "italic",
  "underline",
  "strike",
  "blockquote",
  "horizontalRule",
  "inlineCode",
  "table",
  "list",
  "textAlign",
  "indent",
  "lineHeight",
  "insert",
]
```

## 样式说明

从当前版本开始，只需要：

```ts
import eeedEditor from "eeed-editor";
```

组件样式会随 JS 自动注入页面，不需要再额外写：

```ts
import "eeed-editor/dist/index.css";
```

为了兼容旧项目，包内仍然保留 `dist/index.css` 文件。

## 容器要求

编辑器依赖父容器计算布局，请确保外层容器稳定：

```css
.editor-wrapper {
  position: relative;
  width: 100%;
  height: 720px;
}
```

如果父容器没有明确高度，可能会出现：

- 编辑器不可见
- 工具栏和正文区域重叠
- A4 页面定位异常
- 大纲滚动区域异常

## 常见问题

### 为什么打开后高度不对？

请检查外层容器是否设置了 `position: relative` 和明确高度。

### 为什么保存成功没有提示？

`onSave` 需要返回 `true`：

```ts
const handleSave = async () => {
  await save();
  return true;
};
```

### 为什么上传图片没有变成线上地址？

请传入 `onUploadFile`，并返回上传后的 URL。

### 为什么样式没有生效？

新版会自动注入样式。如果你使用了非常特殊的构建工具或 SSR 环境，可以临时手动引入：

```ts
import "eeed-editor/dist/index.css";
```

## 浏览器支持

推荐在现代浏览器中使用：

- Chrome / Edge 最新版
- Firefox 最新版
- Safari 最新版

## 链接

- 官网：https://editor.eeed.cn
- GitHub：https://github.com/zhangmingxin0120/eeed-editor
- npm：https://www.npmjs.com/package/eeed-editor

## License

MIT
