feat(@vben/plugins): tiptap 组件新增 maxHeight 属性支持及文档 (#7897)

* feat(@vben/plugins): tiptap 组件新增 maxHeight 属性支持

- 新增 maxHeight prop 支持限制编辑器最大高度
- 编辑器内容区超出最大高度时自动滚动
- 移除 style.css 中未使用的 CSS 规则

* docs(@vben/plugins): 添加 tiptap 富文本编辑器组件文档

- 新增中文文档 docs/src/components/common-ui/vben-tiptap.md
- 新增英文文档 docs/src/en/components/common-ui/vben-tiptap.md
- 新增基础用法示例 demos/vben-tiptap/basic
- 新增图片上传示例 demos/vben-tiptap/image-upload
- 更新文档侧边栏配置

* fix(@vben/plugins): extract URL from response in image upload callback

The upload callback was incorrectly passing the AxiosResponse object
to resolve() instead of extracting the actual image URL string.
This caused the editor to insert [object Object] as image src,
resulting in broken images.

* chore: add changeset for tiptap maxHeight feature

* Revert "chore: add changeset for tiptap maxHeight feature"

This reverts commit a28fc4441f14641f6af6c1a143aa6959591315b2.
master^2
JyQAQ 2026-05-10 10:38:21 +08:00 committed by GitHub
parent 0868deb82b
commit f5feddc6c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 506 additions and 9 deletions

View File

@ -198,6 +198,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
link: 'common-ui/vben-ellipsis-text',
text: 'EllipsisText',
},
{
link: 'common-ui/vben-tiptap',
text: 'Tiptap RichTextEditor',
},
],
},
];

View File

@ -196,6 +196,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
link: 'common-ui/vben-ellipsis-text',
text: 'EllipsisText 省略文本',
},
{
link: 'common-ui/vben-tiptap',
text: 'Tiptap 富文本编辑器',
},
],
},
];

View File

@ -0,0 +1,199 @@
---
outline: deep
---
# Vben Tiptap 富文本编辑器
基于 [Tiptap](https://tiptap.dev/) 构建的富文本编辑器组件,支持丰富的文本格式化、图片插入、图片上传等功能。
> 如果文档内没有参数说明,可以尝试在在线示例内寻找
::: info 写在前面
如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
:::
## 基础用法
<DemoPreview dir="demos/vben-tiptap/basic" />
## 组件列表
### VbenTiptap
富文本编辑器主组件。
### VbenTiptapPreview
富文本内容预览组件,用于只读展示编辑器内容。
## API
### Props
| 属性名 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| `modelValue` (v-model) | 编辑器内容HTML字符串 | `string` | `''` |
| `editable` | 是否可编辑 | `boolean` | `true` |
| `toolbar` | 是否显示工具栏 | `boolean` | `true` |
| `previewable` | 是否显示预览按钮 | `boolean` | `true` |
| `placeholder` | 占位提示文本 | `string` | - |
| `minHeight` | 最小高度 | `number \| string` | `240` |
| `maxHeight` | 最大高度 | `number \| string` | `400` |
| `extensions` | 自定义 Tiptap 扩展配置 | `Extensions` | - |
| `imageUpload` | 图片上传配置 | `ImageUploadOptions` | - |
### Events
| 事件名 | 说明 | 参数类型 |
| -------- | -------------- | ----------------------- |
| `change` | 内容变化时触发 | `VbenTiptapChangeEvent` |
#### VbenTiptapChangeEvent
```ts
interface VbenTiptapChangeEvent {
html: string; // HTML 内容
json: JSONContent; // JSON 结构内容
text: string; // 纯文本内容
}
```
### ImageUploadOptions
图片上传配置项:
```ts
interface ImageUploadOptions {
/** 允许的文件类型,默认 'image/*' */
accept?: string;
/** 最大文件大小(字节),默认 5MB */
maxSize?: number;
/** 上传失败回调,未提供时使用 alert 弹窗提示 */
onUploadError?: (error: unknown) => void;
/** 上传函数,返回图片 URL */
upload: (
file: File,
onProgress?: (percent: number) => void,
) => Promise<string>;
}
```
### VbenTiptapPreview Props
| 属性名 | 说明 | 类型 | 默认值 |
| ----------- | ------------------ | ------------------ | ------ |
| `content` | 要预览的 HTML 内容 | `string` | `''` |
| `minHeight` | 最小高度 | `number \| string` | `160` |
| `class` | 自定义类名 | `any` | - |
## 工具栏功能
编辑器工具栏提供以下功能:
### 格式化
- **撤销/重做** - 撤销或重做编辑操作
- **清除格式** - 清除选中文本的所有格式
- **粗体** - 加粗文本
- **斜体** - 斜体文本
- **下划线** - 下划线文本
- **删除线** - 删除线文本
- **行内代码** - 行内代码标记
### 结构
- **标题** - 段落、H1-H4 标题切换
- **有序列表** - 有序编号列表
- **无序列表** - 无序符号列表
- **引用块** - 引用块样式
- **代码块** - 多行代码块
### 链接与图片
- **插入链接** - 插入或编辑超链接
- **移除链接** - 移除选中文本的链接
- **插入图片** - 通过 URL 插入图片
### 样式
- **文字颜色** - 设置文字颜色(预设色板)
- **背景颜色** - 设置文字背景高亮颜色
### 对齐
- **左对齐** - 文本左对齐
- **居中对齐** - 文本居中对齐
- **右对齐** - 文本右对齐
### 其他
- **预览** - 在弹窗中预览编辑内容
## 图片上传
<DemoPreview dir="demos/vben-tiptap/image-upload" />
当配置 `imageUpload`工具栏的图片按钮会变为下拉菜单包含「本地上传」和「URL 插入」两个选项。
### 上传方式
支持三种图片上传方式:
1. **文件选择** - 点击工具栏本地上传按钮
2. **拖拽上传** - 直接拖拽图片到编辑器区域
3. **粘贴上传** - 粘贴图片到编辑器
### 上传进度显示
上传过程中会显示:
- **加载指示器** - 旋转动画指示上传进行中
- **进度条** - 当上传函数提供 `onProgress` 回调时,显示进度条
### 文件校验
- `accept` - 指定允许的文件类型MIME类型
- `maxSize` - 最大文件大小限制(字节)
- 校验失败时会触发 `onUploadError` 回调或默认 alert 提示
::: warning 注意事项
- 仅支持单张图片上传,多图拖拽/粘贴时会提示并仅处理第一张
- 上传中不要保存编辑器内容(`getHTML()`),因为此时图片 URL 为临时 blob URL
- 自定义 `extensions` 时,图片上传功能将不显示(因为可能缺少 uploadImage 命令)
:::
## 自定义扩展
通过 `extensions` 属性可以传入自定义的 Tiptap 扩展配置:
```vue
<script setup lang="ts">
import { VbenTiptap } from '@vben/plugins/tiptap';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
const extensions = [
StarterKit,
Underline,
// 其他扩展...
];
</script>
<template>
<VbenTiptap v-model="content" :extensions="extensions" />
</template>
```
::: warning 自定义扩展注意事项
使用自定义 `extensions` 时:
- 默认扩展配置将不会生效
- 图片上传功能不可用(工具栏不显示上传选项)
- 需自行配置所需的编辑器功能
:::

View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { VbenTiptap } from '@vben/plugins/tiptap';
const content = ref('<p>开始编辑你的内容...</p>');
</script>
<template>
<div>
<VbenTiptap v-model="content" />
<div class="mt-4">
<p class="text-sm text-gray-500">当前内容:</p>
<pre
class="mt-2 p-2 bg-gray-100 rounded text-xs overflow-auto max-h-40"
>{{ content }}</pre
>
</div>
</div>
</template>

View File

@ -0,0 +1,45 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { type ImageUploadOptions, VbenTiptap } from '@vben/plugins/tiptap';
const content = ref('');
// Mock upload function with progress simulation
const imageUpload: ImageUploadOptions = {
accept: 'image/jpeg,image/png,image/gif,image/webp',
maxSize: 5 * 1024 * 1024, // 5MB
upload: async (_file, onProgress) => {
// Simulate upload progress
for (let i = 0; i <= 100; i += 10) {
await new Promise((resolve) => setTimeout(resolve, 100));
onProgress?.(i);
}
// Return a mock image URL (using picsum for demo)
return `https://picsum.photos/seed/${Date.now()}/800/400`;
},
onUploadError: (error) => {
console.error('Upload error:', error);
},
};
</script>
<template>
<div>
<VbenTiptap
v-model="content"
:image-upload="imageUpload"
placeholder="尝试拖拽或粘贴图片..."
/>
<div class="mt-4">
<p class="text-sm text-gray-500">提示:</p>
<ul class="mt-2 text-xs text-gray-400 list-disc pl-4">
<li>点击工具栏图片按钮可选择本地上传或 URL 插入</li>
<li>拖拽图片到编辑器区域可直接上传</li>
<li>粘贴图片也会触发上传</li>
<li>上传过程中会显示进度条</li>
</ul>
</div>
</div>
</template>

View File

@ -0,0 +1,199 @@
---
outline: deep
---
# Vben Tiptap Rich Text Editor
A rich text editor component built on [Tiptap](https://tiptap.dev/), supporting rich text formatting, image insertion, and image upload features.
> If some details are not obvious from the docs, check the live demos as well.
::: info Note
If you feel the current component implementation doesn't meet your needs, you can use native components directly or create your own component. The components provided by the framework are not constraints - use them at your discretion.
:::
## Basic Usage
<DemoPreview dir="demos/vben-tiptap/basic" />
## Component List
### VbenTiptap
Main rich text editor component.
### VbenTiptapPreview
Read-only preview component for displaying editor content.
## API
### Props
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| `modelValue` (v-model) | Editor content (HTML string) | `string` | `''` |
| `editable` | Whether the editor is editable | `boolean` | `true` |
| `toolbar` | Whether to show the toolbar | `boolean` | `true` |
| `previewable` | Whether to show the preview button | `boolean` | `true` |
| `placeholder` | Placeholder text | `string` | - |
| `minHeight` | Minimum height | `number \| string` | `240` |
| `maxHeight` | Maximum height | `number \| string` | `400` |
| `extensions` | Custom Tiptap extensions | `Extensions` | - |
| `imageUpload` | Image upload configuration | `ImageUploadOptions` | - |
### Events
| Event | Description | Parameters |
| -------- | ------------------------------ | ----------------------- |
| `change` | Triggered when content changes | `VbenTiptapChangeEvent` |
#### VbenTiptapChangeEvent
```ts
interface VbenTiptapChangeEvent {
html: string; // HTML content
json: JSONContent; // JSON structure
text: string; // Plain text content
}
```
### ImageUploadOptions
Image upload configuration:
```ts
interface ImageUploadOptions {
/** Allowed file types, default 'image/*' */
accept?: string;
/** Max file size in bytes, default 5MB */
maxSize?: number;
/** Upload error callback, uses alert if not provided */
onUploadError?: (error: unknown) => void;
/** Upload function, returns image URL */
upload: (
file: File,
onProgress?: (percent: number) => void,
) => Promise<string>;
}
```
### VbenTiptapPreview Props
| Property | Description | Type | Default |
| ----------- | ----------------------- | ------------------ | ------- |
| `content` | HTML content to preview | `string` | `''` |
| `minHeight` | Minimum height | `number \| string` | `160` |
| `class` | Custom class name | `any` | - |
## Toolbar Features
The editor toolbar provides the following features:
### Formatting
- **Undo/Redo** - Undo or redo editing operations
- **Clear Formatting** - Remove all formatting from selected text
- **Bold** - Bold text
- **Italic** - Italic text
- **Underline** - Underline text
- **Strikethrough** - Strikethrough text
- **Inline Code** - Inline code mark
### Structure
- **Headings** - Paragraph, H1-H4 heading switching
- **Ordered List** - Numbered list
- **Bullet List** - Bulleted list
- **Blockquote** - Quote block style
- **Code Block** - Multi-line code block
### Links & Images
- **Insert Link** - Insert or edit hyperlinks
- **Remove Link** - Remove link from selected text
- **Insert Image** - Insert image via URL
### Style
- **Text Color** - Set text color (preset palette)
- **Highlight Color** - Set text background highlight color
### Alignment
- **Align Left** - Left align text
- **Align Center** - Center align text
- **Align Right** - Right align text
### Other
- **Preview** - Preview content in a modal
## Image Upload
<DemoPreview dir="demos/vben-tiptap/image-upload" />
When `imageUpload` is configured, the toolbar image button becomes a dropdown menu with "Upload" and "URL" options.
### Upload Methods
Three image upload methods are supported:
1. **File Selection** - Click the upload button in toolbar
2. **Drag & Drop** - Drag images directly into the editor
3. **Paste** - Paste images into the editor
### Upload Progress Display
During upload:
- **Loading Indicator** - Spinner animation indicating upload in progress
- **Progress Bar** - Shows progress bar when upload function provides `onProgress` callback
### File Validation
- `accept` - Specify allowed file types (MIME types)
- `maxSize` - Maximum file size limit (bytes)
- Validation failure triggers `onUploadError` callback or default alert
::: warning Important Notes
- Only single image upload is supported; multi-image drag/paste will show a prompt and process only the first image
- Do not save editor content (`getHTML()`) during upload as image URLs are temporary blob URLs
- When using custom `extensions`, the image upload feature will not be available (toolbar won't show upload option)
:::
## Custom Extensions
Pass custom Tiptap extension configurations via the `extensions` property:
```vue
<script setup lang="ts">
import { VbenTiptap } from '@vben/plugins/tiptap';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
const extensions = [
StarterKit,
Underline,
// Other extensions...
];
</script>
<template>
<VbenTiptap v-model="content" :extensions="extensions" />
</template>
```
::: warning Custom Extensions Note
When using custom `extensions`:
- Default extension configuration will not take effect
- Image upload feature is not available (toolbar won't show upload option)
- You need to configure all required editor features yourself
:::

View File

@ -1,9 +1,5 @@
@reference "@vben/tailwind-config/theme";
.vben-tiptap-content > * + * {
@apply mt-3;
}
.vben-tiptap-content h1 {
@apply text-2xl font-bold leading-[1.4];
}
@ -50,8 +46,6 @@
.vben-tiptap-content img,
.vben-tiptap-content .vben-tiptap__image {
@apply my-4 block h-auto rounded-2xl border border-border;
max-width: min(100%, 640px);
}

View File

@ -28,6 +28,7 @@ const props = withDefaults(defineProps<TipTapProps>(), {
extensions: undefined,
imageUpload: undefined,
minHeight: 240,
maxHeight: 400,
placeholder: $t('ui.tiptap.placeholder'),
previewable: true,
toolbar: true,
@ -41,9 +42,14 @@ const contentMinHeight = computed(() =>
? `${props.minHeight}px`
: props.minHeight,
);
const contentMaxHeight = computed(() =>
typeof props.maxHeight === 'number'
? `${props.maxHeight}px`
: props.maxHeight,
);
const tiptapContentClass = cn(
'vben-tiptap-content vben-tiptap__content',
'text-foreground min-h-(--vben-tiptap-min-height) leading-7 outline-none',
'text-foreground max-h-(--vben-tiptap-max-height) min-h-(--vben-tiptap-min-height) overflow-auto leading-7 outline-none',
);
const blobUrlTracker = new Set<string>();
const editor = useEditor({
@ -154,7 +160,10 @@ onBeforeUnmount(() => {
<template>
<div
:style="{ '--vben-tiptap-min-height': contentMinHeight }"
:style="{
'--vben-tiptap-min-height': contentMinHeight,
'--vben-tiptap-max-height': contentMaxHeight,
}"
class="vben-tiptap overflow-hidden rounded-xl border border-border bg-card"
>
<div

View File

@ -30,6 +30,7 @@ export interface TipTapProps {
extensions?: Extensions;
imageUpload?: ImageUploadOptions;
minHeight?: number | string;
maxHeight?: number | string;
placeholder?: string;
previewable?: boolean;
toolbar?: boolean;

View File

@ -73,6 +73,8 @@ import { isEmpty } from '@vben/utils';
import { VbenCollapsibleParams } from '@vben-core/shadcn-ui';
import { message, Modal, notification } from 'ant-design-vue';
import { upload_file } from '#/api/examples/upload';
type AdapterUploadProps = UploadProps & {
aspectRatio?: string;
crop?: boolean;
@ -722,7 +724,27 @@ async function initComponentAdapter() {
RadioGroup,
RangePicker,
Rate,
RichEditor: withDefaultPlaceholder(VbenTiptap, 'input'),
RichEditor: withDefaultPlaceholder(VbenTiptap, 'input', {
imageUpload: {
upload: (file: any, onProgress: any) => {
return new Promise((resolve, reject) => {
upload_file({
file,
onProgress({ percent }) {
onProgress?.(percent);
},
onSuccess(response) {
// 从响应中提取图片URL
resolve(response?.data?.url ?? response?.url ?? '');
},
onError() {
reject(new Error($t('ui.tiptap.upload.uploadFailed')));
},
});
});
},
},
}),
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,