1
This commit is contained in:
280
src/components/RichEditor.vue
Normal file
280
src/components/RichEditor.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="rich-editor">
|
||||
<div class="rich-editor__toolbar">
|
||||
<ButtonGroup size="small" class="rich-editor__toolbar-group">
|
||||
<Button
|
||||
v-for="btn in toolbarButtons"
|
||||
:key="btn.key"
|
||||
:title="btn.label"
|
||||
:icon="btn.icon"
|
||||
@click="handleToolbar(btn)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button size="small" icon="ios-image" type="primary" class="rich-editor__upload" :loading="uploading"
|
||||
@click="triggerUpload">
|
||||
插入图片
|
||||
</Button>
|
||||
<input ref="fileInput" class="rich-editor__file" type="file" accept="image/*" @change="handleUpload">
|
||||
</div>
|
||||
<div ref="editor" class="rich-editor__content" :data-placeholder="placeholder" :style="{ minHeight: `${height}px` }"
|
||||
contenteditable
|
||||
@input="handleInput" @focus="saveSelection" @keyup="saveSelection" @mouseup="saveSelection" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { upload } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'RichEditor',
|
||||
props: {
|
||||
// 富文本初始内容
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 提示语,用于空状态展示
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入内容',
|
||||
},
|
||||
// 文本区域高度
|
||||
height: {
|
||||
type: Number,
|
||||
default: 280,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
innerHTML: this.value,
|
||||
currentRange: null,
|
||||
uploading: false,
|
||||
toolbarButtons: [{
|
||||
key: 'bold',
|
||||
icon: 'ios-bold',
|
||||
label: '加粗',
|
||||
command: 'bold',
|
||||
},
|
||||
{
|
||||
key: 'italic',
|
||||
icon: 'ios-italic',
|
||||
label: '斜体',
|
||||
command: 'italic',
|
||||
},
|
||||
{
|
||||
key: 'underline',
|
||||
icon: 'ios-underline',
|
||||
label: '下划线',
|
||||
command: 'underline',
|
||||
},
|
||||
// 段落排版按钮:左/中/右对齐与分割线,覆盖常用排版需求
|
||||
{
|
||||
key: 'align-left',
|
||||
icon: 'ios-arrow-back',
|
||||
label: '左对齐',
|
||||
command: 'justifyLeft',
|
||||
},
|
||||
{
|
||||
key: 'align-center',
|
||||
icon: 'ios-remove',
|
||||
label: '居中对齐',
|
||||
command: 'justifyCenter',
|
||||
},
|
||||
{
|
||||
key: 'align-right',
|
||||
icon: 'ios-arrow-forward',
|
||||
label: '右对齐',
|
||||
command: 'justifyRight',
|
||||
},
|
||||
{
|
||||
key: 'horizontal-rule',
|
||||
icon: 'md-more',
|
||||
label: '分割线',
|
||||
command: 'insertHorizontalRule',
|
||||
},
|
||||
{
|
||||
key: 'clear',
|
||||
icon: 'md-trash',
|
||||
label: '清空内容',
|
||||
action: 'clear',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
if (val !== this.innerHTML) {
|
||||
this.innerHTML = val || ''
|
||||
this.setContent(this.innerHTML)
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setContent(this.innerHTML)
|
||||
},
|
||||
methods: {
|
||||
// 将外部内容同步到编辑器
|
||||
setContent(content) {
|
||||
if (this.$refs.editor) {
|
||||
this.$refs.editor.innerHTML = content || ''
|
||||
}
|
||||
},
|
||||
// 执行基础格式化命令
|
||||
execCommand(command) {
|
||||
this.focusEditor()
|
||||
document.execCommand(command, false, null)
|
||||
this.syncContent()
|
||||
},
|
||||
// 点击工具栏按钮时统一处理,支持命令或自定义方法
|
||||
handleToolbar(btn) {
|
||||
if (btn.action === 'clear') {
|
||||
this.clearContent()
|
||||
return
|
||||
}
|
||||
if (btn.command) {
|
||||
this.execCommand(btn.command)
|
||||
}
|
||||
},
|
||||
// 清空内容
|
||||
clearContent() {
|
||||
this.setContent('')
|
||||
this.syncContent()
|
||||
},
|
||||
// 编辑区域获得焦点,方便触发格式化命令
|
||||
focusEditor() {
|
||||
if (this.$refs.editor) {
|
||||
this.$refs.editor.focus()
|
||||
this.restoreSelection()
|
||||
}
|
||||
},
|
||||
// 监听输入事件,将HTML回写给父组件
|
||||
handleInput() {
|
||||
this.syncContent()
|
||||
},
|
||||
// 记录当前的光标选区,便于插入图片或文本
|
||||
saveSelection() {
|
||||
const selection = window.getSelection()
|
||||
if (selection.rangeCount > 0) {
|
||||
this.currentRange = selection.getRangeAt(0)
|
||||
}
|
||||
},
|
||||
// 当外部操作打断焦点时,恢复光标
|
||||
restoreSelection() {
|
||||
if (!this.currentRange) {
|
||||
return
|
||||
}
|
||||
const selection = window.getSelection()
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(this.currentRange)
|
||||
},
|
||||
// 同步内容到父组件
|
||||
syncContent() {
|
||||
if (!this.$refs.editor) {
|
||||
return
|
||||
}
|
||||
this.innerHTML = this.$refs.editor.innerHTML
|
||||
this.$emit('input', this.innerHTML)
|
||||
this.$emit('change', this.innerHTML)
|
||||
},
|
||||
// 点击上传按钮
|
||||
triggerUpload() {
|
||||
if (this.uploading) {
|
||||
return
|
||||
}
|
||||
this.$refs.fileInput.value = ''
|
||||
this.$refs.fileInput.click()
|
||||
},
|
||||
// 选择图片后上传,并插入富文本
|
||||
async handleUpload(event) {
|
||||
const file = event.target.files && event.target.files[0]
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
await this.uploadImage(file)
|
||||
event.target.value = ''
|
||||
},
|
||||
// 将图片上传到后端
|
||||
async uploadImage(file) {
|
||||
const isImage = file.type && file.type.indexOf('image/') === 0
|
||||
if (!isImage) {
|
||||
this.$Message.error('仅支持上传图片文件')
|
||||
return
|
||||
}
|
||||
this.uploading = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await upload(formData)
|
||||
const fileUrl = res.data
|
||||
if (!fileUrl) {
|
||||
throw new Error('上传接口未返回图片地址')
|
||||
}
|
||||
this.insertImage(fileUrl)
|
||||
this.$Message.success('图片上传成功')
|
||||
} catch (error) {
|
||||
this.$Message.error(error.msg || '图片上传失败,请稍后重试')
|
||||
} finally {
|
||||
this.uploading = false
|
||||
}
|
||||
},
|
||||
// 插入图片到当前光标位置
|
||||
insertImage(url) {
|
||||
this.focusEditor()
|
||||
document.execCommand('insertImage', false, url)
|
||||
this.syncContent()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.rich-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rich-editor__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rich-editor__content {
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 8px;
|
||||
line-height: 1.6;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.rich-editor__content:focus {
|
||||
outline: none;
|
||||
border-color: #2d8cf0;
|
||||
box-shadow: 0 0 0 2px rgba(45, 140, 240, 0.1);
|
||||
}
|
||||
|
||||
.rich-editor__content:empty:before {
|
||||
content: attr(data-placeholder);
|
||||
color: #c5c8ce;
|
||||
}
|
||||
|
||||
.rich-editor__upload {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.rich-editor__toolbar-group .ivu-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.rich-editor__toolbar-group .ivu-btn .ivu-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.rich-editor__file {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user