一、功能亮點(diǎn)
? 深度集成 CKEditor5 最新版本 (v45.0.0)
? 支持中文字體、字號(hào)、顏色等排版功能
? 實(shí)現(xiàn)圖片本地上傳并自動(dòng)插入編輯器
? 完整 Markdown 兼容支持
? 實(shí)時(shí)字?jǐn)?shù)統(tǒng)計(jì)功能
? 完美適配 Vue3 組合式 API
二、環(huán)境準(zhǔn)備
1.安裝核心依賴
npm install ckeditor5 @ckeditor/ckeditor5-vue
三、核心實(shí)現(xiàn)
1. 編輯器初始化配置
<script setup>
// 按需引入 45+ 個(gè)功能插件
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 => {
// 自定義接口返回參數(shù)
console.log(res)
resolve({
'uploaded': 0,
'default': res.data.url // 根據(jù)后端返回格式
});
})
.catch(err => {
console.log('上傳失敗');
reject(err)
})
})
}
abort() {
// The upload process can be aborted by calling abort()
}
}
3. 注冊(cè)上傳適配器
<script setup>
const onEditorReady = (editor) => {
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new UploadAdapter(loader)
}
}
</script>
四、后端接口實(shí)現(xiàn)(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']
}));
// 創(chuàng)建上傳目錄
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
// 配置 multer 存儲(chǔ)
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('上傳錯(cuò)誤:', err);
res.status(500).json({
uploaded: 0,
error: { message: '服務(wù)器處理失敗' }
});
}
});
// 啟動(dòng)服務(wù)
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服務(wù)已啟動(dòng):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: '在這里輸入或粘貼您的內(nèi)容!',
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 => {
// 自定義接口返回參數(shù)
console.log(res)
resolve({
'uploaded': 0,
'default': res.data.url // 根據(jù)后端返回格式
});
})
.catch(err => {
console.log('上傳失敗');
reject(err)
})
})
}
abort() {
// The upload process can be aborted by calling abort()
}
}
本方案已通過以下環(huán)境驗(yàn)證:
Vue 3.5.13
CKEditor 45.0.0
ckeditor5-vue 7.3.0
Node.js v20.17.0