3.富文本编辑器ckeditor5

CkEditor5官网地址:CKEditor 5 documentation

ckeditor5一款现代JavaScript富文本编辑器,有多种安装方法-本文主要使用npm的安装方式

因为我的项目是vue3,所以按照介绍安装对应版本的编辑器(属于是打包好的编辑器,扩展性不高)

1.介绍:ckeditor5包括五种类型

javascript 复制代码
经典,传统工具栏在顶部 ClassicEditor 
内联,工具栏在选中内容附近 InlineEditor 
气球,浮动工具栏 BalloonEditor 
气球块,块级浮动工具栏 BalloonBlockEditor     
文档,类似 Word 的文档编辑器 DocumentEditor 
javascript 复制代码
// Vue 的 CKEditor 5 所见即所得编辑器组件。
npm install @ckeditor/ckeditor5-vue

//经典编辑器
npm install @ckeditor/ckeditor5-build-classic


//"@ckeditor/ckeditor5-build-classic": "^41.4.2",
//"@ckeditor/ckeditor5-vue": "^7.3.0",

//写文档时安装的版本,可能时间间隔太久有些规则就会发生变化,以官网为准
javascript 复制代码
<template>
  <ckeditor
      v-model="data"
      :editor="ClassicEditor"
  />
</template>

<script setup>
import { ref, computed } from 'vue';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import { Ckeditor } from '@ckeditor/ckeditor5-vue';

import 'ckeditor5/ckeditor5.css';

const data = ref( '<p>Hello world!</p>' );

</script>
javascript 复制代码
// 其他类型安装指令

//内联
// import InlineEditor from '@ckeditor/ckeditor5-build-inline';

npm install @ckeditor/ckeditor5-build-inline


//气球
// import BalloonEditor from '@ckeditor/ckeditor5-build-balloon';

npm install @ckeditor/ckeditor5-build-balloon


//气球块
// import BalloonBlockEditor from '@ckeditor/ckeditor5-build-balloon-block';

npm install @ckeditor/ckeditor5-build-balloon-block


//文档
//import  DecoupledEditor from '@ckeditor/ckeditor5-build-decoupled-document';

npm install @ckeditor/ckeditor5-build-decoupled-document

内联:点击文字会出现

气球:点击文字会出现

气球块:

文档类编辑器需手动挂载:

javascript 复制代码
<template>
  <div class="doc-editor-wrapper">
    <!-- 工具栏占位符 -->
    <div id="toolbar-container"></div>
    
    <!-- 编辑器 -->
    <div id="editor">
      <ckeditor
        v-model="content"
        :editor="DocumentEditor"
        @ready="handleEditorReady"
      />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import DocumentEditor from '@ckeditor/ckeditor5-build-decoupled-document';
import { Ckeditor  } from '@ckeditor/ckeditor5-vue';

const content = ref('<p>文档内容</p>');

const handleEditorReady = (editor) => {
  // 插入工具栏到指定位置
  const toolbarContainer = document.querySelector('#toolbar-container');
  if (toolbarContainer) {
    toolbarContainer.appendChild(editor.ui.view.toolbar.element);
  }
  
  // 聚焦编辑器
  editor.editing.view.focus();
};
</script>

<style>

</style>

2.实现上传图片

上传图片接口

javascript 复制代码
<template>
  <div class="editor-container">
    <ckeditor
      v-model="content"
      :editor="editor"
      :config="editorConfig"
      @ready="onEditorReady"
    />
    
    <!-- 上传状态提示 -->
    <div v-if="uploadStatus.show" class="upload-status" :class="uploadStatus.type">
      <span v-if="uploadStatus.type === 'uploading'">
        ⏳ 上传中... {{ uploadStatus.progress }}%
      </span>
      <span v-else-if="uploadStatus.type === 'success'">
        ✅ 上传成功
      </span>
      <span v-else-if="uploadStatus.type === 'error'">
        ❌ {{ uploadStatus.message }}
      </span>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, shallowRef } from 'vue';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import { Ckeditor } from '@ckeditor/ckeditor5-vue';

// 使用 shallowRef 避免不必要的响应式开销
const editor = shallowRef(ClassicEditor);
const content = ref('<p>Hello world!</p>');

// 上传状态
const uploadStatus = reactive({
  show: false,
  type: '', // uploading, success, error
  progress: 0,
  message: ''
});

// ==================== 自定义上传适配器 ====================
class CustomUploadAdapter {
  constructor(loader) {
    this.loader = loader;
    this.xhr = null;
    console.log('📤 上传适配器创建');
  }

  async upload() {
    console.log('🚀 开始上传流程');
    
    try {
      const file = await this.loader.file;
      console.log('📄 上传文件:', {
        name: file.name,
        size: formatFileSize(file.size),
        type: file.type
      });
      
      // 验证文件
      const validationError = this.validateFile(file);
      if (validationError) {
        throw new Error(validationError);
      }
      
      return new Promise((resolve, reject) => {
        this._uploadToServer(file, resolve, reject);
      });
      
    } catch (error) {
      console.error('❌ 上传错误:', error);
      this.showUploadStatus('error', error.message);
      throw error;
    }
  }

  abort() {
    console.log('⏹️ 上传中止');
    if (this.xhr) {
      this.xhr.abort();
    }
    this.showUploadStatus('', '');
  }

  // 文件验证
  validateFile(file) {
    const maxSize = 5 * 1024 * 1024; // 5MB
    const allowedTypes = [
      'image/jpeg',
      'image/jpg', 
      'image/png',
      'image/gif',
      'image/webp',
      'image/bmp'
    ];
    
    if (file.size > maxSize) {
      return `文件大小不能超过 5MB (当前: ${formatFileSize(file.size)})`;
    }
    
    if (!allowedTypes.includes(file.type)) {
      return `不支持的文件格式,请上传 JPG、PNG、GIF、WebP、BMP 格式的图片`;
    }
    
    return null;
  }

  // 上传到服务器
  _uploadToServer(file, resolve, reject) {
    const formData = new FormData();
    
    // 注意:很多后端接口期望字段名为 'file' 或 'upload'
    formData.append('file', file);
    formData.append('upload', file); // 双重保险
    
    // 添加额外参数
    formData.append('source', 'ckeditor');
    formData.append('timestamp', Date.now());
    formData.append('culture', 'zh-CN');
    
    this.xhr = new XMLHttpRequest();
    
    // 进度监听
    this.xhr.upload.addEventListener('progress', (evt) => {
      if (evt.lengthComputable) {
        const progress = Math.round((evt.loaded / evt.total) * 100);
        this.showUploadStatus('uploading', '', progress);
      }
    });
    
    // 完成监听
    this.xhr.addEventListener('load', () => {
      console.log('📨 服务器响应状态:', this.xhr.status);
      
      try {
        let response;
        try {
          response = JSON.parse(this.xhr.responseText);
        } catch {
          response = this.xhr.responseText;
        }
        
        console.log('📨 服务器响应:', response);
        
        if (this.xhr.status >= 200 && this.xhr.status < 300) {
          // 根据你的 API 响应格式调整这里
          let imageUrl = '';
          
          if (typeof response === 'string' && response.startsWith('http')) {
            // 如果直接返回 URL 字符串
            imageUrl = response;
          } else if (response && response.url) {
            // 标准格式: { url: '...' }
            imageUrl = response.url;
          } else if (response && response.data && response.data.url) {
            // 嵌套格式: { data: { url: '...' } }
            imageUrl = response.data.url;
          } else if (response && response.success && response.data) {
            // 成功格式: { success: true, data: '...' }
            imageUrl = response.data;
          } else {
            throw new Error('服务器返回格式不正确');
          }
          
          console.log('✅ 上传成功,图片URL:', imageUrl);
          this.showUploadStatus('success', '上传成功');
          
          resolve({
            default: imageUrl
          });
          
        } else {
          const errorMsg = response?.message || response?.error || '上传失败';
          this.showUploadStatus('error', errorMsg);
          reject(new Error(errorMsg));
        }
        
      } catch (error) {
        console.error('❌ 响应解析错误:', error);
        this.showUploadStatus('error', '响应格式错误');
        reject(error);
      }
    });
    
    // 错误监听
    this.xhr.addEventListener('error', () => {
      console.error('❌ 网络错误');
      this.showUploadStatus('error', '网络错误,请检查连接');
      reject(new Error('网络错误'));
    });
    
    // 中止监听
    this.xhr.addEventListener('abort', () => {
      console.log('⏹️ 请求被中止');
      this.showUploadStatus('', '');
      reject(new Error('上传被取消'));
    });
    
    // 发送请求
    this.xhr.open('POST', 上传图片接口', true);
    
    // 设置请求头(根据你的后端要求)
    // this.xhr.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem('token'));
    // this.xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    
    console.log('🌐 发送上传请求到:', '上传文件接口');
    this.xhr.send(formData);
  }

  // 显示上传状态
  showUploadStatus(type, message = '', progress = 0) {
    uploadStatus.type = type;
    uploadStatus.message = message;
    uploadStatus.progress = progress;
    uploadStatus.show = type !== '';
    
    // 成功状态3秒后消失
    if (type === 'success') {
      setTimeout(() => {
        uploadStatus.show = false;
      }, 3000);
    }
  }
}

// ==================== 自定义上传插件 ====================
function CustomUploadPlugin(editor) {
  console.log('🔌 注册自定义上传插件');
  
  editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
    console.log('🔧 创建上传适配器实例');
    return new CustomUploadAdapter(loader);
  };
}

// ==================== 编辑器配置 ====================
const editorConfig = ref({
  placeholder: '请在此输入内容...',
  
  // 关键:添加自定义上传插件
  extraPlugins: [CustomUploadPlugin],
  
  // 简化工具栏,只保留确实存在的功能
  toolbar: {
    items: [
      'heading', '|',
      'bold', 'italic', 'underline', 'strikethrough', '|',
      'fontColor', 'fontBackgroundColor', '|',
      'alignment', '|',
      'numberedList', 'bulletedList', '|',
      'link', 'imageUpload', 'mediaEmbed', '|',
      'blockQuote', 'insertTable', '|',
      'codeBlock', '|',
      'undo', 'redo'
    ],
    shouldNotGroupWhenFull: true
  },
  
  // 图片配置
  image: {
    // 工具栏配置
    toolbar: [
      'imageTextAlternative',  // 图片描述
      'toggleImageCaption',    // 切换标题
      'imageStyle:inline',     // 行内样式
      'imageStyle:block',      // 块样式
      'imageStyle:side',       // 侧边样式
      'linkImage'              // 图片链接
    ],
    
    // 上传配置
    upload: {
      types: ['jpeg', 'jpg', 'png', 'gif', 'webp', 'bmp'],
      maxFileSize: '5MB'
    }
  },
  
  // 表格配置
  table: {
    contentToolbar: [
      'tableColumn',
      'tableRow',
      'mergeTableCells'
    ]
  },
  
  // 链接配置
  link: {
    addTargetToExternalLinks: true,
    defaultProtocol: 'https://'
  },
  
  language: 'zh-cn'
});

// ==================== 编辑器就绪回调 ====================
const onEditorReady = (editor) => {
  console.log('🎉 编辑器已就绪');
  
  // 检查可用功能
  const availableItems = Array.from(editor.ui.componentFactory.names());
  console.log('🔧 可用的工具栏项:', availableItems);
  
  // 验证图片上传是否可用
  if (availableItems.includes('imageUpload')) {
    console.log('✅ 图片上传功能可用');
  } else {
    console.warn('⚠️ 图片上传功能不可用,请检查插件');
  }
  
  // 添加图片上传成功后的回调
  editor.plugins.get('FileRepository').on('change:uploaded', (evt, data) => {
    console.log('📊 上传完成数据:', data);
  });
};

// ==================== 工具函数 ====================
function formatFileSize(bytes) {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

// 如果还是不行,提供一个备用的模拟上传函数
function createMockUploadAdapter(loader) {
  return {
    upload: () => {
      return loader.file.then(file => {
        console.log('🎭 模拟上传文件:', file.name);
        
        // 模拟上传延迟
        return new Promise((resolve) => {
          setTimeout(() => {
            // 使用图床服务作为备选方案
            const mockUrl = `https://via.placeholder.com/800x600/4CAF50/FFFFFF?text=${encodeURIComponent('图片已上传')}`;
            console.log('🎭 模拟上传成功:', mockUrl);
            
            resolve({
              default: mockUrl
            });
          }, 1000);
        });
      });
    },
    abort: () => {
      console.log('🎭 模拟上传取消');
    }
  };
}
</script>

<style scoped>
.editor-container {
  position: relative;
}

.upload-status {
  position: fixed;
  bottom: 20px;
  right: 20px;
  padding: 12px 20px;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  z-index: 1000;
  max-width: 300px;
  animation: slideIn 0.3s ease;
}

@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.upload-status.uploading {
  background: linear-gradient(135deg, #2196F3, #21CBF3);
  color: white;
}

.upload-status.success {
  background: linear-gradient(135deg, #4CAF50, #8BC34A);
  color: white;
}

.upload-status.error {
  background: linear-gradient(135deg, #F44336, #FF9800);
  color: white;
}

:deep(.ck-editor__editable) {
  min-height: 400px;
  padding: 20px !important;
  border: 1px solid #e0e0e0 !important;
  border-radius: 4px !important;
}

:deep(.ck-toolbar) {
  border: 1px solid #e0e0e0 !important;
  border-radius: 4px 4px 0 0 !important;
  background: #f8f9fa !important;
}
</style>

没有图片上传接口-Base64

javascript 复制代码
<template>
  <ckeditor
    v-model="data"
    :editor="ClassicEditor"
    :config="editorConfig"
  />
</template>

<script setup>
import { ref } from 'vue';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import { Ckeditor } from '@ckeditor/ckeditor5-vue';

const data = ref('<p>Hello world!</p>');

// Base64 上传适配器
function Base64UploadPlugin(editor) {
  editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
    return {
      upload: () => {
        return loader.file.then(file => {
          return new Promise((resolve, reject) => {
            const reader = new FileReader();
            
            reader.onload = () => {
              // 生成 Base64 数据 URL
              const base64Data = reader.result;
              console.log('Base64 图片数据长度:', base64Data.length);
              
              // 限制大小(Base64 会比原文件大 33%)
              if (base64Data.length > 5 * 1024 * 1024) { // 约 3.75MB 原文件
                reject('图片太大,请压缩后上传');
                return;
              }
              
              resolve({
                default: base64Data
              });
            };
            
            reader.onerror = () => {
              reject('读取文件失败');
            };
            
            reader.readAsDataURL(file);
          });
        });
      },
      
      abort: () => {
        console.log('上传取消');
      }
    };
  };
}

const editorConfig = ref({
  placeholder: '请在此输入内容',
  
  extraPlugins: [Base64UploadPlugin],
  
  toolbar: {
    items: [
      'heading',
      '|',
      'bold', 'italic',
      '|',
      'imageUpload', // 测试这个按钮
      '|',
      'undo', 'redo'
    ]
  },
  
  language: 'zh-cn'
});
</script>

3.支持上传文件

ckeditor5-filemanager 是一个用于 CKEditor 5 的文件管理插件,它允许用户在编辑器中上传、浏览和管理服务器上的文件(如图片、文档等)

javascript 复制代码
npm install ckeditor5-filemanager@2.0.8

npm install @ckeditor/ckeditor5-vue@5.1.0

封装

javascript 复制代码
<template>
  <!-- 使用 ckeditor 组件 -->
  <ckeditor
    :editor="state.editor"
    v-model="editorData"
    :config="state.editorConfig"
  />
</template>

<script lang="ts" setup>
import { reactive, computed } from 'vue';
// 导入 CKEditor Vue 组件
import CKEditor from '@ckeditor/ckeditor5-vue';
// 导入你需要的编辑器(注意:ckeditor5-filemanager 可能不是完整的编辑器)
import FileManagerEditor from 'ckeditor5-filemanager';

// 创建 CKEditor Vue 组件实例
const { component: ckeditor } = CKEditor;

const props = defineProps({
  value: {
    type: String,
    default: '',
  },
  placeholder: {
    type: String,
    default: '',
  },
});

const emit = defineEmits(['change', 'update:value']);

const state = reactive({
  editor: FileManagerEditor,
  editorConfig: {
    toolbar: {
      items: [
        'undo',
        'redo',
        '|',
        'removeFormat',
        '|',
        'fontSize',
        'fontFamily',
        'bold',
        'italic',
        'underline',
        'strikethrough',
        'fontColor',
        'fontBackgroundColor',
        'highlight',
        '|',
        'alignment',
        'outdent',
        'indent',
        'lineHeight',
        'bulletedList',
        'numberedList',
        '|',
        'FileManager', // 文件管理器按钮
        'insertTable',
        'mediaEmbed',
        'link',
        'blockQuote',
        'horizontalLine',
        'specialCharacters',
        'subscript',
        'superscript',
        '|',
        'sourceEditing',
      ],
      shouldNotGroupWhenFull: true,
    },
    placeholder: props.placeholder ? props.placeholder : '请输入内容!',
    fileManager: {
      uploadUrl: 'uploadUrl文件上传接口地址',
      headers: {
        Authorization: 'Bearer Token',
      },
    },
    htmlSupport: {
      allow: [
        {
          name: /.*/,
          attributes: true,
          classes: true,
          styles: true,
        },
      ],
    },
  },
});

const editorData = computed({
  get: () => props.value,
  set: (val) => emit('update:value', val),
});
</script>

<style>
</style>

导出

javascript 复制代码
import ckeditor from './src/Editor.vue';

export const CKeditor = ckeditor;

使用

javascript 复制代码
<template>
  <div>
    <CKeditor
      style="min-height: 150px"
      v-model:value="data"
      placeholder="请输入内容"
    />
  </div>
</template>

<script setup lang="ts">
  import {ref}from 'vue'
  import { CKeditor } from './CKeditor/index';
  const data = ref('33')
</script>

<style scoped>

</style>

其他详细内容介绍:https://blog.csdn.net/qq_46490956/article/details/136671138?fromshare=blogdetail&sharetype=blogdetail&sharerId=136671138&sharerefer=PC&sharesource=qq_46490956&sharefrom=from_link

相关推荐
lwprain8 个月前
ckeditor4.22版本 ckfinder php8版本下,上传提示400的问题
ckeditor·ckfinder