791 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="uebox">
<div v-if="editorError" class="editor-error">
<p>编辑器加载失败: {{ editorError }}</p>
<button @click="retryLoad">重试加载</button>
</div>
<vue-ueditor-wrap
v-model="content"
:editor-id="props.id"
:config="editorConfig"
:editorDependencies="editorDependencies"
@ready="onEditorReady"
@error="onEditorError"
/>
</div>
</template>
<script setup>
import { VueUeditorWrap } from 'vue-ueditor-wrap'
import { computed, reactive, onMounted, ref } from 'vue'
import { useUserStore } from '/@/store/modules/system/user.js'
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const'
// 解析后端API基地址按环境变量/全局变量/本地存储,依次回退)
const resolveApiBase = () => {
const env = import.meta.env || {}
const candidates = [
env.VITE_APP_API_URL, // 使用正确的环境变量名
env.VITE_GLOB_API_URL,
env.VITE_API_BASE,
env.VITE_API_URL,
env.VITE_BASE_API,
// 可选的全局变量/本地存储回退
typeof window !== 'undefined' ? window.__API_BASE__ : '',
typeof localStorage !== 'undefined' ? localStorage.getItem('API_BASE_URL') : ''
].filter(Boolean)
const base = candidates.length > 0 ? candidates[0] : ''
return base ? base.replace(/\/+$/,'') : ''
}
const apiBase = resolveApiBase()
const uploadEndpoint = '/support/file/upload'
const resolvedServerUrl = `${apiBase}${uploadEndpoint}`
// 获取用户store实例
const userStore = useUserStore()
// 根据环境动态设置UEditor路径
const isProduction = computed(() => {
return apiBase.includes('dev-casedata.igandan.com') || apiBase.includes('casedata.igandan.com')
})
const ueditorBasePath = computed(() => {
if (isProduction.value) {
// 线上环境确保包含UEditorPlus层级
return '/admin/web/UEditorPlus/'
}
// 本地环境
return '/UEditorPlus/'
})
// 调试信息
console.log('UEditor环境配置:', {
apiBase: apiBase.value,
isProduction: isProduction.value,
ueditorBasePath: ueditorBasePath.value
})
const props = defineProps({
// 内容
modelValue: {
type: String,
default: ''
},
// 必须得有如果要在一个页面内写两个或以上的组件需要用id去区分
id: {
type: String,
default: 'common_editor'
}
})
const emits = defineEmits(['update:modelValue'])
const content = computed({
get() {
return props.modelValue || ''
},
set(value) {
emits('update:modelValue', value)
return value
}
})
// 编辑器状态
const editorError = ref('')
const editorReady = ref(false)
// UEditor依赖文件配置
const editorDependencies = computed(() => {
if (isProduction.value) {
// 线上环境:使用完整路径
return [
'/admin/web/UEditorPlus/ueditor.config.js',
'/admin/web/UEditorPlus/ueditor.all.js',
'/admin/web/UEditorPlus/lang/zh-cn/zh-cn.js'
]
}
// 本地环境:使用相对路径
return [
'ueditor.config.js',
'ueditor.all.js',
'lang/zh-cn/zh-cn.js'
]
})
const editorConfig = reactive({
// 基础路径配置 - 必须正确设置,避免路径拼接问题
UEDITOR_HOME_URL: ueditorBasePath.value,
UEDITOR_CORS_URL: ueditorBasePath.value,
// 不从服务器加载配置,使用本地配置
loadConfigFromServer: false,
// 使用项目中的文件上传接口按环境解析直接包含folder参数
serverUrl: `${resolvedServerUrl}?folder=${FILE_FOLDER_TYPE_ENUM.ARTICLE.value}`,
serverHeaders: computed(() => ({
'Authorization': 'Bearer ' + userStore.getToken
})),
// 编辑器基本配置
initialFrameHeight: 500,
initialFrameWidth: '100%',
autoHeightEnabled: false,
catchRemoteImageEnable: false,
// 编辑器功能配置
enableAutoSave: true,
autoSaveInterval: 60000, // 60秒自动保存
enableContextMenu: true,
// 图片上传配置 - 简化配置
imageActionName: 'uploadimage',
imageFieldName: 'file',
imageMaxSize: 2048000, // 2MB
imageAllowFiles: ['.png', '.jpg', '.jpeg', '.gif', '.bmp'],
imageCompressEnable: false, // 禁用图片压缩避免webuploader错误
imageCompressBorder: 1600,
imageInsertAlign: 'none',
imageUrlPrefix: '',
// 视频上传配置
videoActionName: 'uploadvideo',
videoFieldName: 'file',
videoMaxSize: 102400000, // 100MB
videoAllowFiles: ['.flv', '.swf', '.mkv', '.avi', '.rm', '.rmvb', '.mpeg', '.mpg', '.ogg', '.ogv', '.mov', '.wmv', '.mp4', '.webm', '.wav', '.mid'],
videoUrlPrefix: '',
// 附件上传配置
fileActionName: 'uploadfile',
fileFieldName: 'file',
fileMaxSize: 51200000, // 50MB
fileAllowFiles: ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.flv', '.swf', '.mkv', '.avi', '.rm', '.rmvb', '.mpeg', '.mpg', '.ogg', '.ogv', '.mov', '.wmv', '.mp4', '.webm', '.wav', '.mid', '.rar', '.zip', '.tar', '.gz', '.7z', '.bz2', '.cab', '.iso', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf', '.txt', '.md', '.xml'],
fileUrlPrefix: '',
// 完整的工具栏配置
toolbars:[
[
"fullscreen", // 全屏
"source", // 源代码
"|",
"undo", // 撤销
"redo", // 重做
"|",
"bold", // 加粗
"italic", // 斜体
"underline", // 下划线
"fontborder", // 字符边框
"strikethrough",// 删除线
"superscript", // 上标
"subscript", // 下标
"removeformat", // 清除格式
"formatmatch", // 格式刷
"autotypeset", // 自动排版
"blockquote", // 引用
"pasteplain", // 纯文本粘贴模式
"|",
"forecolor", // 字体颜色
"backcolor", // 背景色
"insertorderedlist", // 有序列表
"insertunorderedlist", // 无序列表
"selectall", // 全选
"cleardoc", // 清空文档
"|",
"rowspacingtop",// 段前距
"rowspacingbottom", // 段后距
"lineheight", // 行间距
"|",
"customstyle", // 自定义标题
"paragraph", // 段落格式
"fontfamily", // 字体
"fontsize", // 字号
"|",
"directionalityltr", // 从左向右输入
"directionalityrtl", // 从右向左输入
"indent", // 首行缩进
"|",
"justifyleft", // 居左对齐
"justifycenter", // 居中对齐
"justifyright",
"justifyjustify", // 两端对齐
"|",
"touppercase", // 字母大写
"tolowercase", // 字母小写
"|",
"link", // 超链接
"unlink", // 取消链接
"anchor", // 锚点
"|",
"imagenone", // 图片默认
"imageleft", // 图片左浮动
"imageright", // 图片右浮动
"imagecenter", // 图片居中
"|",
"simpleupload", // 单图上传
"insertimage", // 多图上传
"emotion", // 表情
"scrawl", // 涂鸦
"insertvideo", // 视频
"attachment", // 附件
"insertframe", // 插入Iframe
"insertcode", // 插入代码
"pagebreak", // 分页
"template", // 模板
"background", // 背景
"formula", // 公式
"|",
"horizontal", // 分隔线
"date", // 日期
"time", // 时间
"spechars", // 特殊字符
"wordimage", // Word图片转存
"|",
"inserttable", // 插入表格
"deletetable", // 删除表格
"insertparagraphbeforetable", // 表格前插入行
"insertrow", // 前插入行
"deleterow", // 删除行
"insertcol", // 前插入列
"deletecol", // 删除列
"mergecells", // 合并多个单元格
"mergeright", // 右合并单元格
"mergedown", // 下合并单元格
"splittocells", // 完全拆分单元格
"splittorows", // 拆分成行
"splittocols", // 拆分成列
"contentimport", // 内容导入支持Word、Markdown
"|",
"print", // 打印
"preview", // 预览
"searchreplace", // 查询替换
"help", // 帮助
]
],
// 图片上传配置
imageConfig: {
disableUpload: false,
disableOnline: false,
selectCallback: null
},
// 视频上传配置
videoConfig: {
disableUpload: false,
selectCallback: null
},
// 附件上传配置
attachmentConfig: {
disableUpload: false,
selectCallback: null
},
// 其他配置
debug: false,
autoSaveEnable: true,
autoSaveRestore: false,
maximumWords: 10000,
maxUndoCount: 20,
minFrameHeight: 220,
autoFloatEnabled: false,
topOffset: 0,
toolbarTopOffset: 0,
// 文件处理配置 - 避免webuploader错误
enableContextMenu: true,
catchRemoteImageEnable: false,
autoHeightEnabled: false,
// 错误处理
tipError: function(message, title) {
console.error('UEditor Error:', message);
editorError.value = message;
}
})
// 编辑器就绪事件
const onEditorReady = (editorInstance) => {
console.log('UEditor已就绪:', editorInstance);
editorReady.value = true;
editorError.value = '';
// 确保编辑器可编辑
if (editorInstance && editorInstance.setEnabled) {
try {
editorInstance.setEnabled(true);
} catch (error) {
console.error('启用编辑器失败:', error);
}
}
// 设置初始内容
if (content.value) {
try {
editorInstance.setContent(content.value);
} catch (error) {
console.error('设置初始内容失败:', error);
}
}
// 监听图片上传完成事件
if (editorInstance && editorInstance.addListener) {
try {
// 监听图片上传完成
editorInstance.addListener('afterimagepaste', function(type, data) {
console.log('图片粘贴完成:', type, data);
});
editorInstance.addListener('afterimageinsert', function(type, data) {
console.log('图片插入完成:', type, data);
});
// 尝试监听上传完成事件
editorInstance.addListener('afterimageupload', function(type, data) {
console.log('图片上传完成:', type, data);
});
// 监听内容变化,检查是否有图片插入
editorInstance.addListener('contentchange', function() {
console.log('编辑器内容变化');
const content = editorInstance.getContent();
console.log('当前内容:', content);
// 检查内容中是否包含图片
if (content.includes('<img')) {
console.log('检测到图片标签');
// 检查是否有undefined的图片src如果有则尝试修复
if (content.includes('src="undefined"')) {
console.log('检测到undefined图片src尝试修复');
// 延迟执行,等待图片上传完成
setTimeout(() => {
// 这里可以尝试从其他地方获取正确的图片URL
console.log('尝试修复undefined图片src');
}, 2000);
}
}
});
} catch (error) {
console.error('添加事件监听器失败:', error);
}
}
// 尝试重写图片上传成功的处理
if (editorInstance && editorInstance.execCommand) {
try {
// 保存原始的execCommand方法
const originalExecCommand = editorInstance.execCommand;
// 重写execCommand方法拦截图片插入
editorInstance.execCommand = function(command, ...args) {
console.log('执行命令:', command, args);
if (command === 'inserthtml' && args[0] && args[0].includes('<img')) {
console.log('检测到图片插入命令:', args[0]);
}
// 调用原始方法
return originalExecCommand.call(this, command, ...args);
};
console.log('已重写execCommand方法');
} catch (error) {
console.error('重写execCommand失败:', error);
}
}
}
// 编辑器错误事件
const onEditorError = (error) => {
console.error('UEditor错误:', error);
editorError.value = error.message || '编辑器加载失败';
editorReady.value = false;
}
// 重试加载
const retryLoad = () => {
editorError.value = '';
editorReady.value = false;
// 强制重新渲染组件
location.reload();
}
// 组件挂载后的处理
onMounted(() => {
// 在UEditor加载前设置全局配置避免路径问题
window.UEDITOR_HOME_URL = ueditorBasePath.value;
window.UEDITOR_CORS_URL = ueditorBasePath.value;
// 设置UEditor的全局上传成功回调
window.UEDITOR_CONFIG = window.UEDITOR_CONFIG || {};
// 图片上传成功后的处理 - 关键配置
window.UEDITOR_CONFIG.onImageUploadSuccess = function(result) {
console.log('=== 图片上传成功回调开始 ===');
console.log('原始返回数据:', result);
console.log('数据类型:', typeof result);
console.log('是否为字符串:', typeof result === 'string');
let parsedResult;
try {
// 如果result是字符串尝试解析JSON
if (typeof result === 'string') {
parsedResult = JSON.parse(result);
} else {
parsedResult = result;
}
console.log('解析后的数据:', parsedResult);
} catch (e) {
console.error('JSON解析失败:', e);
parsedResult = result;
}
// 确保返回正确的数据格式
if (parsedResult && parsedResult.data && parsedResult.data.fileUrl) {
const returnData = {
url: parsedResult.data.fileUrl,
title: parsedResult.data.fileName || '图片',
alt: parsedResult.data.fileName || '图片',
state: 'SUCCESS'
};
console.log('返回给UEditor的数据:', returnData);
return returnData;
}
// 如果数据格式不对,返回错误状态
console.error('数据格式错误无法提取fileUrl');
console.log('完整数据:', parsedResult);
return {
state: 'ERROR',
message: '上传失败:数据格式错误'
};
};
// 视频上传成功后的处理
window.UEDITOR_CONFIG.onVideoUploadSuccess = function(result) {
console.log('视频上传成功回调:', result);
if (result && result.data && result.data.fileUrl) {
return {
url: result.data.fileUrl,
title: result.data.fileName,
state: 'SUCCESS'
};
}
return {
state: 'ERROR',
message: '上传失败:数据格式错误'
};
};
// 附件上传成功后的处理
window.UEDITOR_CONFIG.onFileUploadSuccess = function(result) {
console.log('附件上传成功回调:', result);
if (result && result.data && result.data.fileUrl) {
return {
url: result.data.fileUrl,
title: result.data.fileName,
state: 'SUCCESS'
};
}
return {
state: 'ERROR',
message: '上传失败:数据格式错误'
};
};
// 检查依赖文件是否存在
editorDependencies.value.forEach(dep => {
fetch(dep)
.then(response => {
if (!response.ok) {
console.error(`依赖文件加载失败: ${dep}`);
editorError.value = `依赖文件加载失败: ${dep}`;
}
})
.catch(error => {
console.error(`依赖文件请求失败: ${dep}`, error);
editorError.value = `依赖文件请求失败: ${dep}`;
});
});
// 延迟强制刷新编辑器配置
setTimeout(() => {
if (window.UE && window.UEDITOR_CONFIG) {
// 强制覆盖配置
Object.assign(window.UEDITOR_CONFIG, editorConfig);
// 确保上传成功回调被正确设置
if (window.UEDITOR_CONFIG.onImageUploadSuccess) {
console.log('UEditor图片上传成功回调已设置');
}
if (window.UEDITOR_CONFIG.onVideoUploadSuccess) {
console.log('UEditor视频上传成功回调已设置');
}
if (window.UEDITOR_CONFIG.onFileUploadSuccess) {
console.log('UEditor附件上传成功回调已设置');
}
// 尝试监听UEditor的图片上传完成事件
if (window.UE && window.UE.getEditor) {
try {
const editor = window.UE.getEditor(props.id);
if (editor && editor.addListener) {
editor.addListener('afterimagepaste', function(type, data) {
console.log('图片粘贴事件:', type, data);
});
editor.addListener('afterimageinsert', function(type, data) {
console.log('图片插入事件:', type, data);
});
editor.addListener('afterimageupload', function(type, data) {
console.log('图片上传事件:', type, data);
});
}
} catch (e) {
console.log('编辑器实例获取失败,可能还未初始化:', e);
}
}
// 尝试重写UEditor的图片上传处理逻辑
if (window.UE && window.UE.getEditor) {
try {
const editor = window.UE.getEditor(props.id);
if (editor) {
// 重写图片上传成功后的处理
const originalInsertImage = editor.insertImage;
if (originalInsertImage) {
editor.insertImage = function(url, title, alt) {
console.log('重写的insertImage被调用:', url, title, alt);
// 如果URL是undefined尝试从其他地方获取
if (!url || url === 'undefined') {
console.error('图片URL无效:', url);
return false;
}
// 调用原始方法
return originalInsertImage.call(this, url, title, alt);
};
console.log('已重写insertImage方法');
}
}
} catch (e) {
console.log('重写insertImage失败:', e);
}
}
// 尝试拦截网络请求,在图片上传成功后强制处理
if (window.fetch) {
const originalFetch = window.fetch;
window.fetch = function(url, options) {
// 如果是图片上传请求
if (url && url.includes('/support/file/upload') && options && options.method === 'POST') {
console.log('拦截到图片上传请求:', url);
return originalFetch(url, options).then(response => {
// 克隆响应以便多次读取
const clonedResponse = response.clone();
// 读取响应内容
clonedResponse.json().then(data => {
console.log('图片上传响应:', data);
// 如果上传成功,尝试强制插入图片
if (data && data.code === 0 && data.data && data.data.fileUrl) {
console.log('图片上传成功,尝试强制插入:', data.data.fileUrl);
// 延迟执行,确保编辑器已就绪
setTimeout(() => {
if (window.UE && window.UE.getEditor) {
try {
const editor = window.UE.getEditor(props.id);
if (editor && editor.execCommand) {
const imgHtml = `<img src="${data.data.fileUrl}" title="${data.data.fileName}" alt="${data.data.fileName}" />`;
console.log('插入图片HTML:', imgHtml);
editor.execCommand('inserthtml', imgHtml);
}
} catch (e) {
console.error('强制插入图片失败:', e);
}
}
}, 1000);
}
}).catch(e => {
console.error('读取响应失败:', e);
});
return response;
});
}
// 其他请求正常处理
return originalFetch(url, options);
};
console.log('已拦截fetch请求');
}
// 拦截XMLHttpRequest这是UEditor可能使用的另一种请求方式
if (window.XMLHttpRequest) {
const originalXHROpen = window.XMLHttpRequest.prototype.open;
const originalXHRSend = window.XMLHttpRequest.prototype.send;
window.XMLHttpRequest.prototype.open = function(method, url, ...args) {
this._ueditorUrl = url;
this._ueditorMethod = method;
return originalXHROpen.call(this, method, url, ...args);
};
window.XMLHttpRequest.prototype.send = function(data) {
const xhr = this;
const url = this._ueditorUrl;
const method = this._ueditorMethod;
// 如果是图片上传请求
if (url && url.includes('/support/file/upload') && method === 'POST') {
console.log('XMLHttpRequest拦截到图片上传请求:', url);
// 监听响应
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
console.log('XMLHttpRequest图片上传响应:', response);
// 如果上传成功,尝试强制插入图片
if (response && response.code === 0 && response.data && response.data.fileUrl) {
console.log('XMLHttpRequest图片上传成功尝试强制插入:', response.data.fileUrl);
// 延迟执行,确保编辑器已就绪
setTimeout(() => {
if (window.UE && window.UE.getEditor) {
try {
const editor = window.UE.getEditor(props.id);
if (editor && editor.execCommand) {
// 先删除所有undefined的图片
const currentContent = editor.getContent();
if (currentContent.includes('src="undefined"')) {
console.log('删除undefined图片');
editor.setContent(currentContent.replace(/<img[^>]*src="undefined"[^>]*>/g, ''));
}
// 插入正确的图片
const imgHtml = `<img src="${response.data.fileUrl}" title="${response.data.fileName}" alt="${response.data.fileName}" />`;
console.log('插入图片HTML:', imgHtml);
editor.execCommand('inserthtml', imgHtml);
}
} catch (e) {
console.error('XMLHttpRequest强制插入图片失败:', e);
}
}
}, 1000);
}
} catch (e) {
console.error('XMLHttpRequest响应解析失败:', e);
}
}
});
}
return originalXHRSend.call(this, data);
};
console.log('已拦截XMLHttpRequest');
}
}
}, 3000);
// 额外的图片上传处理 - 监听全局事件
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'ueditor-image-upload-success') {
console.log('收到图片上传成功消息:', event.data);
// 这里可以处理图片上传成功后的逻辑
}
});
})
</script>
<style scoped>
.uebox {
width: 100%;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
}
.editor-error {
padding: 20px;
background-color: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 6px;
margin-bottom: 10px;
text-align: center;
}
.editor-error button {
margin-top: 10px;
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.editor-error button:hover {
background-color: #40a9ff;
}
/* 编辑器容器样式优化 */
.uebox :deep(.edui-editor) {
border: none !important;
width: 100% !important;
height: auto !important;
}
.uebox :deep(.edui-editor-toolbarbox) {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-bottom: 1px solid #e8e8e8;
width: 100% !important;
display: block !important;
}
.uebox :deep(.edui-editor-iframeholder) {
border: 1px solid #e8e8e8;
border-top: none;
width: 100% !important;
height: 500px !important;
}
.uebox :deep(.edui-editor-iframeholder iframe) {
width: 100% !important;
height: 100% !important;
pointer-events: auto !important;
}
.uebox :deep(.edui-editor-iframeholder .edui-body-container) {
pointer-events: auto !important;
width: 100% !important;
height: 100% !important;
}
/* 响应式设计 */
@media (max-width: 768px) {
.uebox {
margin: 0 -8px;
}
.uebox :deep(.edui-editor-toolbarbox) {
padding: 4px;
}
}
</style>