1.富文本
富文本是指的是在文本内容中嵌入格式,样式,图像,链接等多媒体元素的文本格式。
2.WangEditor开源富文本编辑器
开源的富文本编辑器,在vue3前端项目中的引入如下
npm install @wangeditor/editor @wangeditor/editor-for-vue
对应的官网如下
https://www.wangeditor.com/
https://www.wangeditor.com/
3.应用与引入
3.1父组件
<template>
<div class="header">
<div class="header-title">父组件</div>
<div
v-if="!isEditing"
class="edit-button"
@click="handleEdit"
>
编辑
</div>
<div
v-if="isEditing"
class="edit-button"
@click="handleSave"
>
保存
</div>
<div
v-if="isEditing"
class="edit-button-cancel"
@click="handCancel"
>
取消
</div>
</div>
<el-divider />
<div
v-if="!isEditing"
class="content"
>
<RichContent
v-model="richTextContent"
:readonly="true"
:show-toolbar="false"
/>
</div>
<!-- 编辑状态 -->
<div
v-else
class="content"
>
<RichContent
v-model="tempRichText"
:readonly="false"
/>
</div>
</template>
<script setup lang="tsx">
import { onMounted, ref, watch } from 'vue'
import RichContent from '@/views/digital-matrix/components/RichContent.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const isEditing = ref(false)
// const richContent = ref()
// 存储最终保存的富文本内容(HTML 格式)
const richTextContent = ref('')
const id = ref(null)
const system_link = ref(null)
// 编辑时的临时内容(避免未保存就修改原内容)
const tempRichText = ref('')
const handleEdit = () => {
isEditing.value = true
tempRichText.value = richTextContent.value
}
watch(
() => tempRichText,
(newValue) => {
console.log('富文本标签', newValue)
}
)
const handleSave = async () => {
try {
await ElMessageBox.confirm(`确定保存并覆盖原来内容`, '', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// await deleteMaterialApi(row.id)
if (!tempRichText.value.trim()) {
ElMessage.warning('请输入内容后再保存')
} else {
console.log('富文本标签', tempRichText.value)
richTextContent.value = tempRichText.value
// await saveRichTextApi({
// type: 'ORG',
// content: richTextContent.value,
// id: id.value
// })
isEditing.value = false
}
ElMessage.success('修改成功')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('修改失败')
}
}
}
const handCancel = () => {
tempRichText.value = ''
isEditing.value = false
}
// 图片上传接口,返回图片url
const handleImageUpload = async (file: File): Promise<string> => {
// 自定义图片上传逻辑
return 'https://example.com/image.jpg'
}
onMounted(async () => {
// await getRichTextApi({ type: 'ORG' }).then((response) => {
// console.log('~~~~~~~~~~~~~~response', response)
// richTextContent.value = response.data.content || '<p>暂无内容</p >'
// id.value = response.data.id || null
// system_link.value = response.data.system_link || null
// })
})
</script>
<style lang="scss" scoped>
@use '@/styles/mixins' as *;
@function vh($px) {
@return calc($px / 1080) * 100vh;
}
.header {
display: flex;
gap: 12px;
&-title {
@include text-style(var(--font-20), var(--el-font-family-bold), rgba(255, 255, 255, 1));
}
&-href {
cursor: pointer;
user-select: none;
@include text-style(var(--font-20), var(--el-font-family-bold), #409eff);
}
}
.edit-button {
@include panel;
min-width: 80px;
height: vh(32);
margin-left: auto;
line-height: vh(32);
text-align: center;
cursor: pointer;
background: rgb(79 172 254 / 50%) !important;
border: 1px solid #409eff !important;
box-shadow: inset 0 0 20px 1px #0093f2 !important;
@include text-style(var(--font-16), var(--el-font-family-regular), #fff);
}
.edit-button-cancel {
@include panel;
min-width: 80px;
height: vh(32);
margin-left: 12px;
line-height: vh(32);
text-align: center;
cursor: pointer;
background: rgb(79 172 254 / 50%) !important;
border: 1px solid #409eff !important;
box-shadow: inset 0 0 20px 1px #0093f2 !important;
@include text-style(var(--font-16), var(--el-font-family-regular), #fff);
}
.content {
display: flex;
flex: 1;
gap: 12px;
padding: 12px;
overflow: hidden;
}
</style>
3.2 富文本子组件
<template>
<div class="rich-text-editor">
<Toolbar
v-if="showToolbar"
class="toolbar"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="mode"
/>
<Editor
v-model="valueHtml"
class="editor"
:default-config="editorConfig"
:mode="mode"
@on-created="handleCreated"
@on-change="handleChange"
/>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, ref, shallowRef, watch, nextTick } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import type { IDomEditor, IEditorConfig } from '@wangeditor/editor'
import '@wangeditor/editor/dist/css/style.css'
// 定义 Props
interface Props {
modelValue?: string
placeholder?: string
readonly?: boolean
showToolbar?: boolean
uploadImage?: (file: File) => Promise<string>
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
placeholder: '请输入内容...',
readonly: false,
showToolbar: true,
uploadImage: undefined
})
// 定义 Emits
const emit = defineEmits<{
'update:modelValue': [value: string]
change: [value: string]
created: [editor: IDomEditor]
}>()
// 编辑器实例
const editorRef = shallowRef<IDomEditor>()
const valueHtml = ref(props.modelValue)
const mode = 'default'
// 监听外部值变化
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== valueHtml.value) {
valueHtml.value = newValue
}
}
)
// 监听内部值变化
watch(valueHtml, (newValue) => {
emit('update:modelValue', newValue)
emit('change', newValue)
})
// 工具栏配置
const toolbarConfig = {
excludeKeys: ['group-video', 'fullScreen', 'insertTable']
}
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
placeholder: props.placeholder,
readOnly: props.readonly,
MENU_CONF: {
uploadImage: {
allowedFileTypes: ['image/*'],
maxFileSize: 10 * 1024 * 1024,
maxNumberOfFiles: 10,
async customUpload(file: File, insertFn: (url: string) => void) {
try {
if (props.uploadImage) {
// 使用自定义上传函数
const imageUrl = await props.uploadImage(file)
insertFn(imageUrl)
} else {
// 默认使用本地预览
const imageUrl = URL.createObjectURL(file)
insertFn(imageUrl)
}
} catch (err) {
console.error('图片上传失败', err)
throw err
}
}
}
}
}
// 编辑器创建回调
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
emit('created', editor)
}
// 内容变化回调
const handleChange = (editor: IDomEditor) => {
// 内容变化已经在 watch 中处理
}
// 设置只读状态
const setReadonly = (readonly: boolean) => {
if (editorRef.value) {
if (readonly) {
editorRef.value.disable()
} else {
editorRef.value.enable()
}
}
}
// 销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor) {
editor.destroy()
}
})
// 暴露方法给父组件
defineExpose({
getEditor: () => editorRef.value,
setReadonly,
clear: () => {
valueHtml.value = ''
},
getContent: () => valueHtml.value,
setContent: (content: string) => {
valueHtml.value = content
}
})
</script>
<style scoped lang="scss">
.rich-text-editor {
flex: 1;
overflow: hidden;
border-radius: 4px;
// border: 1px solid #fff;
.toolbar {
border-bottom: 1px solid #dcdfe6;
}
.editor {
flex: 1;
overflow-y: auto;
}
}
// 编辑器样式调整
:deep(.w-e-text-container) {
height: 100% !important;
// 富文本默认颜色
color: #fff;
background: transparent;
// 代码块
pre > code {
background-color: rgb(0 0 0 / 40%);
}
h1 {
font-size: 36px;
}
h2 {
font-size: 30px;
}
h3 {
font-size: 26px;
}
h4 {
font-size: 20px;
}
}
// toolBar样式
:deep(.w-e-bar) {
background-color: transparent;
svg {
fill: #fff;
}
}
:deep(.w-e-bar-item) {
color: #fff;
}
// 滚动条
:deep(.w-e-scroll) {
&::-webkit-scrollbar {
width: 2px;
background: transparent;
}
&::-webkit-scrollbar-track {
background: rgb(54 148 255 / 20%);
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
background: #3694ff;
border-radius: 2px;
}
}
:deep(.w-e-bar-item button) {
color: inherit;
}
:deep(.w-e-bar-item .active) {
background-color: rgba($color: #a4bcff, $alpha: 20%);
}
// 外圈
:deep(.w-e-bar-item:hover) {
background-color: rgba($color: #a4bcff, $alpha: 20%);
}
// 内圈
:deep(.w-e-bar-item button:hover) {
background-color: transparent;
}
:deep(.w-e-bar-item.active) {
color: #409eff;
background-color: #ecf5ff;
}
// 下箭头呼出的容器
:deep(.w-e-drop-panel) {
background: transparent;
}
:deep(.w-e-select-list) {
background: transparent;
&::-webkit-scrollbar {
width: 2px;
background: transparent;
}
&::-webkit-scrollbar-track {
background: rgb(54 148 255 / 20%);
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
background: #3694ff;
border-radius: 2px;
}
}
:deep(.w-e-select-list ul li:hover) {
background-color: rgba($color: #a4bcff, $alpha: 20%);
}
:deep(.w-e-select-list ul .selected) {
background: transparent;
}
:deep(.w-e-bar-item-group .w-e-bar-item-menus-container) {
background: transparent;
}
</style>
1万+

被折叠的 条评论
为什么被折叠?



