This commit is contained in:
MeSHard
2025-11-24 15:25:50 +08:00
parent 186a92e834
commit e228528c96
14 changed files with 2624 additions and 768 deletions

View 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>