360 lines
6.3 KiB
Vue
360 lines
6.3 KiB
Vue
<template>
|
|
<div :style="{ '--editor-height': editorHeight }">
|
|
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur" @focus="onFocus"></ckeditor>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
ClassicEditor,
|
|
Alignment,
|
|
AutoImage,
|
|
Autoformat,
|
|
BlockQuote,
|
|
Bold,
|
|
CodeBlock,
|
|
DataFilter,
|
|
DataSchema,
|
|
Essentials,
|
|
FindAndReplace,
|
|
FontBackgroundColor,
|
|
FontColor,
|
|
FontFamily,
|
|
FontSize,
|
|
GeneralHtmlSupport,
|
|
Heading,
|
|
Highlight,
|
|
HorizontalLine,
|
|
Image,
|
|
ImageCaption,
|
|
ImageInsert,
|
|
ImageResize,
|
|
ImageStyle,
|
|
ImageToolbar,
|
|
ImageUpload,
|
|
Indent,
|
|
IndentBlock,
|
|
Italic,
|
|
Link,
|
|
LinkImage,
|
|
List,
|
|
MediaEmbed,
|
|
MediaEmbedToolbar,
|
|
Mention,
|
|
Paragraph,
|
|
PasteFromOffice,
|
|
RemoveFormat,
|
|
SelectAll,
|
|
ShowBlocks,
|
|
SourceEditing,
|
|
SpecialCharacters,
|
|
SpecialCharactersArrows,
|
|
SpecialCharactersCurrency,
|
|
SpecialCharactersEssentials,
|
|
SpecialCharactersLatin,
|
|
SpecialCharactersMathematical,
|
|
SpecialCharactersText,
|
|
Style,
|
|
Subscript,
|
|
Superscript,
|
|
Table,
|
|
TableCaption,
|
|
TableCellProperties,
|
|
TableColumnResize,
|
|
TableProperties,
|
|
TableToolbar,
|
|
TextTransformation,
|
|
TodoList,
|
|
Underline,
|
|
Undo,
|
|
WordCount,
|
|
} from 'ckeditor5'
|
|
import { Ckeditor } from '@ckeditor/ckeditor5-vue'
|
|
import { UploadAdapterPlugin } from './UploadAdapter.js'
|
|
|
|
import { ref, computed, watch } from 'vue'
|
|
import { useCurrentInstance } from '@/utils/tool'
|
|
|
|
import coreTranslations from 'ckeditor5/translations/zh-cn.js'
|
|
import 'ckeditor5/ckeditor5.css'
|
|
|
|
const { proxy } = useCurrentInstance()
|
|
|
|
// 组件名称
|
|
defineOptions({
|
|
name: 'scCkeditor',
|
|
})
|
|
|
|
// Props 定义
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
placeholder: {
|
|
type: String,
|
|
default: '请输入内容……',
|
|
},
|
|
toolbar: {
|
|
type: String,
|
|
default: 'basic',
|
|
},
|
|
height: {
|
|
type: String,
|
|
default: '400px',
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
// Emits 定义
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
// 工具栏配置常量
|
|
const TOOLBARS = {
|
|
full: [
|
|
'sourceEditing',
|
|
'undo',
|
|
'redo',
|
|
'heading',
|
|
'style',
|
|
'|',
|
|
'superscript',
|
|
'subscript',
|
|
'removeFormat',
|
|
'bold',
|
|
'italic',
|
|
'underline',
|
|
'link',
|
|
'fontBackgroundColor',
|
|
'fontFamily',
|
|
'fontSize',
|
|
'fontColor',
|
|
'|',
|
|
'outdent',
|
|
'indent',
|
|
'alignment',
|
|
'bulletedList',
|
|
'numberedList',
|
|
'todoList',
|
|
'|',
|
|
'blockQuote',
|
|
'insertTable',
|
|
'imageInsert',
|
|
'mediaEmbed',
|
|
'highlight',
|
|
'horizontalLine',
|
|
'selectAll',
|
|
'showBlocks',
|
|
'specialCharacters',
|
|
'codeBlock',
|
|
'findAndReplace',
|
|
],
|
|
basic: [
|
|
'sourceEditing',
|
|
'undo',
|
|
'redo',
|
|
'heading',
|
|
'|',
|
|
'removeFormat',
|
|
'bold',
|
|
'italic',
|
|
'underline',
|
|
'link',
|
|
'fontBackgroundColor',
|
|
'fontFamily',
|
|
'fontSize',
|
|
'fontColor',
|
|
'|',
|
|
'outdent',
|
|
'indent',
|
|
'alignment',
|
|
'bulletedList',
|
|
'numberedList',
|
|
'todoList',
|
|
'|',
|
|
'insertTable',
|
|
'imageInsert',
|
|
'mediaEmbed',
|
|
],
|
|
simple: ['undo', 'redo', 'heading', '|', 'removeFormat', 'bold', 'italic', 'underline', 'link', 'fontBackgroundColor', 'fontFamily', 'fontSize', 'fontColor', '|', 'insertTable', 'imageInsert', 'mediaEmbed'],
|
|
}
|
|
|
|
// 插件配置常量
|
|
const PLUGINS = [
|
|
Alignment,
|
|
AutoImage,
|
|
Autoformat,
|
|
BlockQuote,
|
|
Bold,
|
|
CodeBlock,
|
|
DataFilter,
|
|
DataSchema,
|
|
Essentials,
|
|
FindAndReplace,
|
|
FontBackgroundColor,
|
|
FontColor,
|
|
FontFamily,
|
|
FontSize,
|
|
GeneralHtmlSupport,
|
|
Heading,
|
|
Highlight,
|
|
HorizontalLine,
|
|
Image,
|
|
ImageCaption,
|
|
ImageInsert,
|
|
ImageResize,
|
|
ImageStyle,
|
|
ImageToolbar,
|
|
ImageUpload,
|
|
Indent,
|
|
IndentBlock,
|
|
Italic,
|
|
Link,
|
|
LinkImage,
|
|
List,
|
|
MediaEmbed,
|
|
MediaEmbedToolbar,
|
|
Mention,
|
|
Paragraph,
|
|
PasteFromOffice,
|
|
RemoveFormat,
|
|
SelectAll,
|
|
ShowBlocks,
|
|
SourceEditing,
|
|
SpecialCharacters,
|
|
SpecialCharactersArrows,
|
|
SpecialCharactersCurrency,
|
|
SpecialCharactersEssentials,
|
|
SpecialCharactersLatin,
|
|
SpecialCharactersMathematical,
|
|
SpecialCharactersText,
|
|
Style,
|
|
Subscript,
|
|
Superscript,
|
|
Table,
|
|
TableCaption,
|
|
TableCellProperties,
|
|
TableColumnResize,
|
|
TableProperties,
|
|
TableToolbar,
|
|
TextTransformation,
|
|
TodoList,
|
|
Underline,
|
|
Undo,
|
|
WordCount,
|
|
UploadAdapterPlugin,
|
|
]
|
|
|
|
// 响应式数据
|
|
const editorData = ref('')
|
|
const editorHeight = ref(props.height)
|
|
const editor = ClassicEditor
|
|
|
|
// 编辑器配置
|
|
const editorConfig = computed(() => ({
|
|
language: { ui: 'zh-cn', content: 'zh-cn' },
|
|
translations: [coreTranslations],
|
|
plugins: PLUGINS,
|
|
toolbar: {
|
|
shouldNotGroupWhenFull: true,
|
|
items: TOOLBARS[props.toolbar] || TOOLBARS.basic,
|
|
},
|
|
placeholder: props.placeholder,
|
|
image: {
|
|
styles: ['alignLeft', 'alignCenter', 'alignRight'],
|
|
toolbar: ['imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight', '|', 'linkImage'],
|
|
},
|
|
mediaEmbed: {
|
|
previewsInData: true,
|
|
providers: [
|
|
{
|
|
name: 'mp4',
|
|
url: /\.(mp4|avi|mov|flv|wmv|mkv)$/i,
|
|
html: (match) => {
|
|
const url = match['input']
|
|
return '<video controls width="100%" height="100%" src="' + url + '"></video>'
|
|
},
|
|
},
|
|
],
|
|
},
|
|
fontSize: {
|
|
options: [10, 12, 14, 16, 18, 20, 22, 24, 26, 30, 32, 36],
|
|
},
|
|
style: {
|
|
definitions: [
|
|
{
|
|
name: 'Article category',
|
|
element: 'h3',
|
|
classes: ['category'],
|
|
},
|
|
{
|
|
name: 'Info box',
|
|
element: 'p',
|
|
classes: ['info-box'],
|
|
},
|
|
],
|
|
},
|
|
upload: {
|
|
uploadUrl: proxy?.$API?.common?.upload?.url || '',
|
|
withCredentials: false,
|
|
extendData: { type: 'images' },
|
|
headers: {
|
|
Authorization: 'Bearer ' + proxy?.$TOOL?.data?.get('TOKEN'),
|
|
},
|
|
},
|
|
}))
|
|
|
|
// 监听 modelValue 变化
|
|
watch(
|
|
() => props.modelValue,
|
|
(newVal) => {
|
|
editorData.value = newVal ?? ''
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
// 监听 height 变化
|
|
watch(
|
|
() => props.height,
|
|
(newVal) => {
|
|
editorHeight.value = newVal
|
|
},
|
|
)
|
|
|
|
// 移除图片宽高的正则替换函数
|
|
const stripImageDimensions = (html) => {
|
|
return html.replace(/<img[^>]*>/gi, (match) => {
|
|
return match.replace(/width="[^"]*"/gi, '').replace(/height="[^"]*"/gi, '')
|
|
})
|
|
}
|
|
|
|
// 失去焦点事件 - 移除图片的固定宽高,避免响应式布局问题
|
|
const onBlur = () => {
|
|
const cleanedData = stripImageDimensions(editorData.value)
|
|
editorData.value = cleanedData
|
|
emit('update:modelValue', cleanedData)
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
:root {
|
|
--ck-z-panel: 9999;
|
|
}
|
|
|
|
.ck-content {
|
|
height: var(--editor-height);
|
|
}
|
|
|
|
.ck-source-editing-area,
|
|
.ck-source-editing-area textarea {
|
|
height: var(--editor-height);
|
|
}
|
|
|
|
.ck-source-editing-area textarea {
|
|
overflow-y: scroll !important;
|
|
}
|
|
</style>
|