component-富文本实现(WangEditor)

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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值