一、功能亮点
✅ 深度集成 CKEditor5 最新版本 (v45.0.0)
✅ 支持中文字体、字号、颜色等排版功能
✅ 实现图片本地上传并自动插入编辑器
✅ 完整 Markdown 兼容支持
✅ 实时字数统计功能
✅ 完美适配 Vue3 组合式 API
二、环境准备
1.安装核心依赖
npm install ckeditor5 @ckeditor/ckeditor5-vue
三、核心实现
1. 编辑器初始化配置
<script setup>
// 按需引入 45+ 个功能插件
import { ClassicEditor, FontFamily, ImageUpload /* ... */ } from 'ckeditor5'
const config = ref({
plugins: [FontFamily, ImageUpload, /* ... */ ],
toolbar: {
items: [
'fontFamily', 'fontSize', 'uploadImage',
'|', 'bold', 'italic', 'codeBlock'
]
},
image: {
toolbar: ['toggleImageCaption', 'imageTextAlternative'],
upload: {
types: ['jpeg', 'png', 'webp'] // 限制图片类型
}
}
})
</script>
2. 自定义上传适配器
// UploadAdapter.js
import axios from "axios"
export default class UploadAdapter {
constructor(loader) {
this.loader = loader;
}
async upload() {
const data = new FormData();
data.append('typeOption', 'upload_image');
data.append('file', await this.loader.file);
return new Promise((resolve,reject) => {
axios({
url: 'http://localhost:3000/upload',
method: 'post',
data,
headers: {
Authorization: sessionStorage.getItem("Authorization")
},
// withCredentials: true
})
.then(res => {
// 自定义接口返回参数
console.log(res)
resolve({
'uploaded': 0,
'default': res.data.url // 根据后端返回格式
});
})
.catch(err => {
console.log('上传失败');
reject(err)
})
})
}
abort() {
// The upload process can be aborted by calling abort()
}
}
3. 注册上传适配器
<script setup>
const onEditorReady = (editor) => {
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new UploadAdapter(loader)
}
}
</script>
四、后端接口实现(Node.js)
1.安装依赖
npm init -y
npm install express multer cors
// server.js
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const app = express();
// 允许跨域
app.use(cors({
origin: '*',
methods: ['POST'],
allowedHeaders: ['Content-Type']
}));
// 创建上传目录
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
// 配置 multer 存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
cb(null, uniqueName);
}
});
const upload = multer({ storage });
// 上传接口
app.post('/upload', upload.single('file'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
uploaded: 0,
error: { message: '未收到文件' }
});
}
const fileUrl = `http://localhost:3000/uploads/${req.file.filename}`;
// 返回 CKEditor 要求的格式
res.json({
uploaded: 1,
url: fileUrl
});
} catch (err) {
console.error('上传错误:', err);
res.status(500).json({
uploaded: 0,
error: { message: '服务器处理失败' }
});
}
});
// 启动服务
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务已启动:http://localhost:${PORT}`);
});
五、完整代码
1.ckeditor.vue
<script setup>
import { ref, computed, useTemplateRef } from 'vue'
import { Ckeditor } from '@ckeditor/ckeditor5-vue'
import {
ClassicEditor,
Alignment,
Autoformat,
AutoImage,
AutoLink,
Autosave,
Base64UploadAdapter,
BlockQuote,
Bold,
Bookmark,
Code,
CodeBlock,
Essentials,
FindAndReplace,
FontBackgroundColor,
FontColor,
FontFamily,
FontSize,
Fullscreen,
GeneralHtmlSupport,
Heading,
Highlight,
HorizontalLine,
HtmlComment,
HtmlEmbed,
ImageBlock,
ImageCaption,
ImageInline,
ImageInsert,
ImageInsertViaUrl,
ImageResize,
ImageStyle,
ImageTextAlternative,
ImageToolbar,
ImageUpload,
Indent,
IndentBlock,
Italic,
Link,
List,
ListProperties,
Markdown,
MediaEmbed,
PageBreak,
Paragraph,
PasteFromMarkdownExperimental,
PasteFromOffice,
PlainTableOutput,
RemoveFormat,
ShowBlocks,
SourceEditing,
SpecialCharacters,
SpecialCharactersArrows,
SpecialCharactersCurrency,
SpecialCharactersEssentials,
SpecialCharactersLatin,
SpecialCharactersMathematical,
SpecialCharactersText,
Strikethrough,
Subscript,
Superscript,
Table,
TableCaption,
TableCellProperties,
TableColumnResize,
TableLayout,
TableProperties,
TableToolbar,
TextTransformation,
TodoList,
Underline,
WordCount
} from 'ckeditor5'
import MyUploadAdapter from "./UploadAdapter";
import translations from 'ckeditor5/translations/zh-cn.js'
import 'ckeditor5/ckeditor5.css'
const data = ref('<p>Hello world!</p>')
const config = ref(computed(() => {
return {
licenseKey: 'GPL',
plugins: [
Alignment,
Autoformat,
AutoImage,
AutoLink,
Autosave,
Base64UploadAdapter,
BlockQuote,
Bold,
Bookmark,
Code,
CodeBlock,
Essentials,
FindAndReplace,
FontBackgroundColor,
FontColor,
FontFamily,
FontSize,
Fullscreen,
GeneralHtmlSupport,
Heading,
Highlight,
HorizontalLine,
HtmlComment,
HtmlEmbed,
ImageBlock,
ImageCaption,
ImageInline,
ImageInsert,
ImageInsertViaUrl,
ImageResize,
ImageStyle,
ImageTextAlternative,
ImageToolbar,
ImageUpload,
Indent,
IndentBlock,
Italic,
Link,
List,
ListProperties,
Markdown,
MediaEmbed,
PageBreak,
Paragraph,
PasteFromMarkdownExperimental,
PasteFromOffice,
PlainTableOutput,
RemoveFormat,
ShowBlocks,
SourceEditing,
SpecialCharacters,
SpecialCharactersArrows,
SpecialCharactersCurrency,
SpecialCharactersEssentials,
SpecialCharactersLatin,
SpecialCharactersMathematical,
SpecialCharactersText,
Strikethrough,
Subscript,
Superscript,
Table,
TableCaption,
TableCellProperties,
TableColumnResize,
TableLayout,
TableProperties,
TableToolbar,
TextTransformation,
TodoList,
Underline,
WordCount
],
toolbar: {
items: [
'sourceEditing',
'showBlocks',
'findAndReplace',
'fullscreen',
'|',
'heading',
'|',
'fontSize',
'fontFamily',
'fontColor',
'fontBackgroundColor',
'|',
'bold',
'italic',
'underline',
'strikethrough',
'subscript',
'superscript',
'code',
'removeFormat',
'|',
'specialCharacters',
'horizontalLine',
'pageBreak',
'link',
'bookmark',
// 'insertImage',
'uploadImage',
'mediaEmbed',
'insertTable',
'insertTableLayout',
'highlight',
'blockQuote',
'codeBlock',
'htmlEmbed',
'|',
'alignment',
'|',
'bulletedList',
'numberedList',
'todoList',
'outdent',
'indent'
],
shouldNotGroupWhenFull: false
},
fontFamily: {
supportAllValues: true
},
fontSize: {
options: [10, 12, 14, 'default', 18, 20, 22],
supportAllValues: true
},
fullscreen: {
onEnterCallback: container =>
container.classList.add(
'editor-container',
'editor-container_classic-editor',
'editor-container_include-word-count',
'editor-container_include-fullscreen',
'main-container'
)
},
heading: {
options: [
{
model: 'paragraph',
title: 'Paragraph',
class: 'ck-heading_paragraph'
},
{
model: 'heading1',
view: 'h1',
title: 'Heading 1',
class: 'ck-heading_heading1'
},
{
model: 'heading2',
view: 'h2',
title: 'Heading 2',
class: 'ck-heading_heading2'
},
{
model: 'heading3',
view: 'h3',
title: 'Heading 3',
class: 'ck-heading_heading3'
},
{
model: 'heading4',
view: 'h4',
title: 'Heading 4',
class: 'ck-heading_heading4'
},
{
model: 'heading5',
view: 'h5',
title: 'Heading 5',
class: 'ck-heading_heading5'
},
{
model: 'heading6',
view: 'h6',
title: 'Heading 6',
class: 'ck-heading_heading6'
}
]
},
htmlSupport: {
allow: [
{
name: /^.*$/,
styles: true,
attributes: true,
classes: true
}
]
},
image: {
toolbar: [
'toggleImageCaption',
'imageTextAlternative',
'|',
'imageStyle:inline',
'imageStyle:wrapText',
'imageStyle:breakText',
'|',
'resizeImage'
]
},
initialData: '<h2>Congratulations on setting up CKEditor 5! 🎉</h2>',
link: {
addTargetToExternalLinks: true,
defaultProtocol: 'https://',
decorators: {
toggleDownloadable: {
mode: 'manual',
label: 'Downloadable',
attributes: {
download: 'file'
}
}
}
},
list: {
properties: {
styles: true,
startIndex: true,
reversed: true
}
},
placeholder: '在这里输入或粘贴您的内容!',
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties']
},
translations: [translations]
}
}))
const editorWordCount = useTemplateRef('editorWordCountElement')
const onReady = (editor) => {
editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
return new MyUploadAdapter(loader)
}
[...editorWordCount.value.children].forEach(child => child.remove());
const wordCount = editor.plugins.get('WordCount');
editorWordCount.value.appendChild(wordCount.wordCountContainer);
}
</script>
<template>
<div class="container">
<ckeditor v-model="data" :editor="ClassicEditor" :config="config" @ready="onReady" />
<div class="editor_container__word-count" ref="editorWordCountElement"></div>
</div>
</template>
<style>
.container {
width: 100%;
}
.ck-content {
height: 500px;
}
</style>
2.UploadAdapter.js
import axios from "axios"
export default class UploadAdapter {
constructor(loader) {
this.loader = loader;
}
async upload() {
const data = new FormData();
data.append('typeOption', 'upload_image');
data.append('file', await this.loader.file);
return new Promise((resolve,reject) => {
axios({
url: 'http://localhost:3000/upload',
method: 'post',
data,
headers: {
Authorization: sessionStorage.getItem("Authorization")
},
// withCredentials: true
})
.then(res => {
// 自定义接口返回参数
console.log(res)
resolve({
'uploaded': 0,
'default': res.data.url // 根据后端返回格式
});
})
.catch(err => {
console.log('上传失败');
reject(err)
})
})
}
abort() {
// The upload process can be aborted by calling abort()
}
}
本方案已通过以下环境验证:
Vue 3.5.13
CKEditor 45.0.0
ckeditor5-vue 7.3.0
Node.js v20.17.0