editor-memo.vue
<template>
<view>
<view id="title" class="title" data-placeholder="标题...">
</view>
<view id="editor" class="editor" data-placeholder="请输入内容..." :cmd="cmd" :change:cmd="editorModule.handleCmd">
<!-- renderjs 会在这里操作 DOM -->
</view>
<!-- 工具栏在逻辑层 -->
<view class="toolbar">
<button @click="editorModule.formatBold">加粗</button>
<button @click="editorModule.insertImg">插入图片</button>
<button @click="editorModule.insertAudio">插入音频</button>
</view>
</view>
</template>
<script>
export default {
// 组件名称,建议写全,方便调试
name: "EditorMemo",
// 父组件传入的数据
props: {
titleIsShow: {
type: Boolean,
default: true
},
oldFileIds: {
type: Array,
default: []
},
},
data() {
return {
cmd: null, //cmd 和数据
}
},
methods: {
selectFileUpload(map) {
/* let vm = this;
plus.io.chooseFile({
title: '选择文件',
filetypes: ['png', 'jpg', 'jpeg', 'mp3', 'mp4'], // 允许的文件类型
multiple: false, // 是否允许多选
}, (res) => {
// console.log('res', res);
let parts = res.files[0].split('/');
let result = parts.pop();
let listItem = [{
imageOrVideo: '0',
name: result,
uri: res.files[0],
url: res.files[0],
model: 'testTableName',
}]
const userInfo = uni.getStorageSync('USERINFO');
if (userInfo.user && userInfo.user.id) {
const baseDirectory = `${userInfo.user.id}/mmwz_user_digital_treasury`;
// 显示进度条
vm.showProgressBar = true;
vm.uploadProgress = 0;
vm.$huaweiobssignature(
listItem,
'testTableName',
baseDirectory,
(percent, loaded, total) => {
// 更新进度条
//vm.uploadProgress = percent;
console.log(`上传进度:${percent}%`);
}
).then(resa => {
// 隐藏进度条
//vm.showProgressBar = false;
//vm.uploadProgress = 0;
if (resa && resa.length > 0 && resa[0].id) {
console.log(resa);
} else {
uni.hideLoading()
//vm.showProgressBar = false;
vm.$refs.uToast.show({
title: '上传失败',
type: 'error',
})
}
}).catch(err => {
uni.hideLoading()
//vm.showProgressBar = false;
vm.$refs.uToast.show({
title: '上传失败',
type: 'error',
})
})
}
setTimeout(() => {
uni.hideLoading()
}, 10000);
})*/
console.log(map.filetypes);
console.log(map.funcName);
this.cmd = {
type: map.funcName,
data: {
url: 'https://adsmind.ugdtimg.com/ads_svp_video__0bc3faacqaaadiahjntkjjtbukaefauaakca.f1000_0.jpeg?dis_k=ee08351a67d705d7cf721e2ec8906107&dis_t=1718246483&sha256=581060cb59350df0178ad2ea29c8dca50992b5c531293c484dd66c432552a95a&ck=db826460a21fce82c3d36c3776711db9&m=db826460a21fce82c3d36c3776711db9',
id: '1111666'
},
timestamp: Date.now() // 加时间戳确保每次都能触发(即使内容相同)
}
},
contentChanges(html){
this.$emit('content-changes', html);
//console.log(html)
},
receiveNewFileIds(newFileIds){
var that = this;
this.$emit('fileIds-changes', {oldFileIds:that.oldFileIds,newFileIds:newFileIds});
}
}
}
</script>
<script module="editorModule" lang="renderjs">
export default {
data() {
return {
savedRange: null
}
},
mounted() {
const title = document.getElementById('title')
title.contentEditable = 'true'
/*title.innerHTML = '标题...' */
// 初始化编辑器
const editor = document.getElementById('editor')
editor.contentEditable = 'true'
/*editor.innerHTML = '<p>请输入内容...</p>'*/
// 监听输入变化
editor.addEventListener('input', () => {
// 通过 uni.postMessage 或 triggerEvent 通知逻辑层
//this.$ownerInstance.callMethod('selectFileUpload', editor.innerHTML)
this.$ownerInstance.callMethod('contentChanges', editor.innerHTML)
this.getAllDataId();
})
// 统一保存选区的方法
const saveSelection = () => {
const sel = window.getSelection()
if (sel.rangeCount === 0) return
const range = sel.getRangeAt(0)
// 检查选区是否在 editor 或 title 内
if (editor.contains(range.commonAncestorContainer)) {
this.savedRange = range.cloneRange()
//this.currentEditor = 'editor' // 标记当前操作的是哪个编辑器
}
/* else if (title.contains(range.commonAncestorContainer)) {
this.savedRange = range.cloneRange()
this.currentEditor = 'title'
} */
}
document.addEventListener('selectionchange', saveSelection)
},
methods: {
handleCmd(newVal, oldVal, ownerInstance, instance) {
if (!newVal) return
const {
type,
data
} = newVal
if (type === 'insertImgToDom') {
this.insertImgToDom(data, ownerInstance)
} else if (type === 'insertAudioToDom') {
this.insertAudioToDom(data, ownerInstance)
}
},
insertImg(event, ownerInstance) {
ownerInstance.callMethod('selectFileUpload', {
filetypes: ['png', 'jpg', 'jpeg'],
funcName: 'insertImgToDom'
})
},
insertImgToDom(data, ownerInstance) {
console.log(data);
console.log(ownerInstance);
const img = document.createElement('img')
img.src = data.url
img.style.maxWidth = '100%'
img.style.display = 'block'
img.style.margin = '8px 0'
img.style.borderRadius = '4px'
img.setAttribute('data-id', data.id)
img.setAttribute('data-type', 'image')
this.insertDom(img);
},
insertAudio(event, ownerInstance) {
ownerInstance.callMethod('selectFileUpload', {
filetypes: ['mp3'],
funcName: 'insertAudioToDom'
})
},
insertAudioToDom(data, ownerInstance) {
console.log(data);
console.log(ownerInstance);
const audio = document.createElement('audio')
audio.src = 'https://web-ext-storage.dcloud.net.cn/doc/uniapp/ForElise.mp3'
audio.controls = true // 显示播放控件,必须
audio.preload = 'metadata' // 只预加载元数据(时长等),不自动下载完整音频
audio.style.width = '100%' // 音频控件宽度占满
audio.style.display = 'block' // 块级显示
audio.style.margin = '8px 0' // 上下间距
audio.setAttribute('data-id', data.id)
audio.setAttribute('data-type', 'audio')
audio.setAttribute('contenteditable', 'false') // 关键:防止在编辑器内误编辑音频控件本身
this.insertDom(audio);
},
insertDom(dom) {
const editor = document.getElementById('editor')
editor.focus()
// 恢复选区
const sel = window.getSelection()
sel.removeAllRanges()
if (this.savedRange) {
sel.addRange(this.savedRange)
} else {
// 没有保存的选区,创建一个默认选区(放到编辑器末尾)
const range = document.createRange()
range.selectNodeContents(editor)
range.collapse(false) // false = 折叠到末尾
sel.addRange(range)
this.savedRange = range.cloneRange()
}
// 3. 再次安全检查:确保 rangeCount > 0
if (sel.rangeCount === 0) {
console.warn('无法获取选区')
return
}
// 创建图片元素
/* const img = document.createElement('img')
img.src =
'https://adsmind.ugdtimg.com/ads_svp_video__0bc3faacqaaadiahjntkjjtbukaefauaakca.f1000_0.jpeg?dis_k=ee08351a67d705d7cf721e2ec8906107&dis_t=1718246483&sha256=581060cb59350df0178ad2ea29c8dca50992b5c531293c484dd66c432552a95a&ck=db826460a21fce82c3d36c3776711db9&m=db826460a21fce82c3d36c3776711db9'
img.style.maxWidth = '100%'
img.style.display = 'block'
img.style.margin = '8px 0'
img.style.borderRadius = '4px'
img.setAttribute('data-type', 'image') */
// 如果当前选区 collapsed(没有选中文字),直接插入图片
// 如果选中了文字,先删除选中的文字,再插入图片
const range = sel.getRangeAt(0)
range.deleteContents()
range.insertNode(dom)
// 插入后,把光标移到图片后面
range.setStartAfter(dom)
range.setEndAfter(dom)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
// 保存新选区
this.savedRange = range.cloneRange()
this.getAllDataId();
},
getAllDataId(){
const elements = document.querySelectorAll('[data-id]');
const idList = Array.from(elements).map(el => el.dataset.id);
this.$ownerInstance.callMethod('receiveNewFileIds', idList)
}
}
}
</script>
<style>
.title:empty::before {
content: attr(data-placeholder);
color: #999;
pointer-events: none;
/* 防止占位文字阻挡点击 */
}
.title:focus:empty::before {
content: '';
/* 聚焦时隐藏占位文字 */
}
.editor:empty::before {
content: attr(data-placeholder);
color: #999;
pointer-events: none;
/* 防止占位文字阻挡点击 */
}
.editor:focus:empty::before {
content: '';
/* 聚焦时隐藏占位文字 */
}
/* 方法二:如果你用了类名,更精确 */
.title:focus-visible {
outline: none;
border-bottom: 2px solid #ccc;
/* 灰色下边框 */
}
/* 方法三:兼容 :focus-visible(部分浏览器默认使用) */
.editor:focus-visible {
outline: none;
/*border-bottom: 1px solid #ccc; 灰色下边框 */
}
.toolbar {
padding: 10px 0;
border-bottom: 1px solid #ccc;
/* 灰色下边框 */
border-top: 1px solid #ccc;
/* 灰色下边框 */
display: flex;
gap: 16px;
}
.editor {
width: 100%;
min-height: 400px;
height: auto;
}
</style>
使用
fwb3.vue
<template>
<view>
<EditorMemo :titleIsShow="true" :oldFileIds="['999999']" @content-changes="contentChanges" @fileIds-changes="fileIdsChanges"></EditorMemo>
</view>
</template>
<script>
import EditorMemo from '@/components/my-editor/editor-memo.vue';
export default {
// 2. 手动注册
components: {
EditorMemo
},
data() {
return {
}
},
methods: {
contentChanges(html){
console.log(html)
},
fileIdsChanges(fileIds){
console.log(fileIds)
}
}
}
</script>
<style>
</style>
效果示例
