Files
vueadmin/src/components/scEditor/index.vue
2026-01-26 09:44:48 +08:00

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>