refactor(cmsArticle): 统一文章编辑器为单一 WangEditor 富文本模式
- 删除原有 TinyMCE 富文本和 MdEditor Markdown 双模式切换及相关逻辑 - 替换文章编辑弹窗内容详情为 RichTextEditor 组件 - 新增图片库和视频库选择按钮,支持插入图片和视频 HTML 标签 - 实现一键排版和首行缩进按钮,直接操作内容 HTML 字符串 - 回显时 Markdown 内容转换为 HTML,保存时固定 editor 类型为 1 - 移除无用的配置和样式,简化代码逻辑,提升编辑体验 - 留下文章编辑器类型设置字段待后续清理,展示端保持一致 HTML 渲染方式
This commit is contained in:
@@ -113,3 +113,18 @@
|
||||
- **潜在坑**:若 HTML content 里含裸 `.jpg` 链接(非 `<img>` 包裹,如 `<a href="https://x/x.jpg">`),`parseContent` 的兜底会把该 URL 误转成 `<img>` 破坏链接。WangEditor 产出的图片都是 `<img>`,正常商品详情极少触发,低概率。
|
||||
- `uploadFile` 返回的 `url` 是否为小程序域名白名单内完整 URL 需上线后在真机确认(影响商品详情页图片显示)。
|
||||
- 老商品仅在「重新编辑」时转 HTML;从未编辑过的老 Markdown 商品保持 Markdown,仍由小程序正确渲染。
|
||||
|
||||
## 文章管理编辑弹窗:统一换成 WangEditor 富文本(双模式→单模式)
|
||||
- 用户要求文章模块编辑器"也换了",与商品模块一致。经确认采用「统一 WangEditor 单模式」,**删除原有的 TinyMCE 富文本 + MdEditor Markdown 双模式切换**。
|
||||
- 改动文件:`src/views/cms/cmsArticle/components/articleEdit.vue`(复用已建的 `src/components/RichTextEditor.vue`)。
|
||||
- 删除:双模式 `v-if="editor==1/2"` 渲染分支、`editor` ref、`onEditorTypeChange/performEditorSwitch/convertContentFormat/onEditor` 切换逻辑、`markdownToolbars`、`onMarkdownUploadImg`、`openMarkdownImageSelector/openMarkdownVideoSelector`、`insertMarkdownText`、TinyMCE `config`(含 custom_image_selector/quick_upload/auto_format/toggle_indent 等自定义按钮 setup)、`.mce-content-body` 样式、`uploadOss` import(仅在删块中使用)。
|
||||
- 新增/改写:
|
||||
- 内容详情改用 `<RichTextEditor ref="editorRef" v-model="content" :height="500">`。
|
||||
- 编辑器下方按钮区:`📷从图片库选择`/`🎬从视频库选择`(调 `editorRef.value.insertHtml` 插入 img/video,复用 SelectData 弹窗)、`🎨一键排版`/`📐首行缩进`(直接操作 `content.value` HTML 字符串,底层 smartFormatContent/addIndent/removeIndent 纯函数保留)。
|
||||
- `md = markdownit()` + `toEditorContent(raw)`:回显时 Markdown→HTML,已是 HTML 原样返回。
|
||||
- `save()` 固定 `editor: 1`(统一 HTML)。
|
||||
- 验证:`pnpm build` 通过(exit 0)。
|
||||
- 风险/待办:
|
||||
- `src/views/cms/cmsWebsiteSettingEdit.vue` 仍有"编辑器类型 1 md-editor-v3, 2 tinymce-editor"设置字段,文章编辑已不再读取,**该设置现在对文章编辑无效**,建议后续清理(改动涉及系统设置暂未动)。
|
||||
- 展示端 `guilixu-taro/src/pages/index/article-detail.tsx` 用 Taro `RichText` 渲染 HTML(目前是占位页),统一 HTML 安全。
|
||||
- `uploadFile` 的 url 域名白名单同商品模块,需真机确认。
|
||||
|
||||
@@ -80,58 +80,22 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容详情" name="content">
|
||||
<!-- 富文本编辑器 -->
|
||||
<div v-if="editor == 1">
|
||||
<tinymce-editor
|
||||
<RichTextEditor
|
||||
ref="editorRef"
|
||||
class="editor-content"
|
||||
v-model:value="content"
|
||||
:disabled="disabled"
|
||||
:init="config"
|
||||
placeholder="支持直接粘贴或拖拽图片,也可点击工具栏图片按钮从文件库选择"
|
||||
/>
|
||||
<div class="file-selector-tip">
|
||||
💡
|
||||
提示:工具栏"图片"按钮从图片库选择,"上传"按钮快速上传图片;"视频"按钮从视频库选择,"上传视频"按钮快速上传视频;"一键排版"按钮自动优化文章格式;"首行缩进"按钮切换段落缩进
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markdown编辑器 -->
|
||||
<div v-if="editor == 2">
|
||||
<!-- 📝 Markdown编辑器工具栏扩展 -->
|
||||
<div class="markdown-toolbar-extension">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openMarkdownImageSelector"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
📷 从图片库选择
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
size="small"
|
||||
@click="openMarkdownVideoSelector"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
🎬 从视频库选择
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<MdEditor
|
||||
v-model="content"
|
||||
:disabled="disabled"
|
||||
height="500px"
|
||||
:placeholder="'请输入Markdown内容...'"
|
||||
:toolbars="markdownToolbars"
|
||||
:onUploadImg="onMarkdownUploadImg"
|
||||
:height="500"
|
||||
placeholder="支持直接粘贴或拖拽图片,也可点击下方按钮从图片库选择"
|
||||
/>
|
||||
<div class="file-selector-tip">
|
||||
💡
|
||||
提示:支持Markdown语法,可以使用工具栏按钮从文件库选择图片/视频,也可以直接拖拽上传文件
|
||||
💡 提示:支持直接粘贴/拖拽上传图片;点击下方按钮从图片库/视频库选择,或使用「排版」「首行缩进」优化文章
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-space class="editor-toolbar-ext mt-2" wrap>
|
||||
<a-button size="small" @click="openImageSelector">📷 从图片库选择</a-button>
|
||||
<a-button size="small" @click="openVideoSelector">🎬 从视频库选择</a-button>
|
||||
<a-button size="small" @click="handleAutoFormat">🎨 一键排版</a-button>
|
||||
<a-button size="small" @click="toggleParagraphIndent">📐 首行缩进</a-button>
|
||||
</a-space>
|
||||
<a-space
|
||||
class="py-2 flex items-center text-gray-400"
|
||||
v-if="lang == 'zh_CN'"
|
||||
@@ -317,11 +281,8 @@
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance, RuleObject } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
import TinymceEditor from '@/components/TinymceEditor/index.vue';
|
||||
import { MdEditor } from 'md-editor-v3';
|
||||
import 'md-editor-v3/lib/style.css';
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import { uploadOss } from '@/api/system/file';
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue';
|
||||
import markdownit from 'markdown-it';
|
||||
import { CmsArticleCategory } from '@/api/cms/cmsArticleCategory/model';
|
||||
import { CmsNavigation } from '@/api/cms/cmsNavigation/model';
|
||||
import SourceSelect from '@/views/cms/cmsArticle/dictionary/source-select.vue';
|
||||
@@ -335,8 +296,6 @@
|
||||
const useForm = Form.useForm;
|
||||
const setting = useWebsiteSettingStore();
|
||||
const { locale } = useI18n();
|
||||
const editor = ref<number>(1);
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
@@ -486,138 +445,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 📝 编辑器类型切换处理
|
||||
const onEditorTypeChange = (e: any) => {
|
||||
const newEditorType = e.target.value;
|
||||
const oldEditorType = editor.value;
|
||||
|
||||
// 如果编辑器类型没有变化,直接返回
|
||||
if (newEditorType === oldEditorType) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果当前有内容,提示用户确认切换
|
||||
if (
|
||||
content.value &&
|
||||
content.value.trim() !== '' &&
|
||||
content.value !== '<p><br></p>'
|
||||
) {
|
||||
Modal.confirm({
|
||||
title: '🔄 切换编辑器类型',
|
||||
content: '切换编辑器类型可能会影响内容格式,是否继续?',
|
||||
okText: '确认切换',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
performEditorSwitch(newEditorType, oldEditorType);
|
||||
},
|
||||
onCancel: () => {
|
||||
// 恢复原来的编辑器类型
|
||||
editor.value = oldEditorType;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 没有内容直接切换
|
||||
performEditorSwitch(newEditorType, oldEditorType);
|
||||
}
|
||||
};
|
||||
|
||||
// 执行编辑器切换
|
||||
const performEditorSwitch = (newType: number, oldType: number) => {
|
||||
try {
|
||||
editor.value = newType;
|
||||
|
||||
// 保存编辑器类型偏好到本地存储
|
||||
localStorage.setItem('cms_article_editor_preference', newType.toString());
|
||||
|
||||
// 显示切换成功提示
|
||||
const editorName = newType === 1 ? '富文本编辑器' : 'Markdown编辑器';
|
||||
message.success({
|
||||
content: `✅ 已切换到${editorName}`,
|
||||
duration: 2
|
||||
});
|
||||
|
||||
// 如果有内容,尝试进行格式转换
|
||||
if (content.value && content.value.trim() !== '') {
|
||||
convertContentFormat(oldType, newType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('编辑器切换失败:', error);
|
||||
message.error('编辑器切换失败,请重试');
|
||||
// 恢复原来的编辑器类型
|
||||
editor.value = oldType;
|
||||
}
|
||||
};
|
||||
|
||||
// 内容格式转换
|
||||
const convertContentFormat = (fromType: number, toType: number) => {
|
||||
if (!content.value) return;
|
||||
|
||||
try {
|
||||
if (fromType === 1 && toType === 2) {
|
||||
// 富文本转Markdown(简单转换)
|
||||
let markdownContent = content.value
|
||||
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
|
||||
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
|
||||
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
|
||||
.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n')
|
||||
.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1\n\n')
|
||||
.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1\n\n')
|
||||
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
||||
.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
|
||||
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
|
||||
.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
|
||||
.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
|
||||
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
||||
.replace(
|
||||
/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi,
|
||||
''
|
||||
)
|
||||
.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, '')
|
||||
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
|
||||
.replace(/<br[^>]*>/gi, '\n')
|
||||
.replace(/<[^>]+>/g, '') // 移除其他HTML标签
|
||||
.replace(/\n{3,}/g, '\n\n') // 清理多余换行
|
||||
.trim();
|
||||
|
||||
content.value = markdownContent;
|
||||
message.info('已尝试将富文本内容转换为Markdown格式');
|
||||
} else if (fromType === 2 && toType === 1) {
|
||||
// Markdown转富文本(基本转换)
|
||||
let htmlContent = content.value
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^#### (.*$)/gim, '<h4>$1</h4>')
|
||||
.replace(/^##### (.*$)/gim, '<h5>$1</h5>')
|
||||
.replace(/^###### (.*$)/gim, '<h6>$1</h6>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
||||
.replace(
|
||||
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
||||
'<img src="$2" alt="$1" style="max-width: 100%;" />'
|
||||
)
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
// 包装在段落标签中
|
||||
if (htmlContent && !htmlContent.startsWith('<')) {
|
||||
htmlContent = '<p>' + htmlContent + '</p>';
|
||||
}
|
||||
|
||||
content.value = htmlContent;
|
||||
message.info('已尝试将Markdown内容转换为富文本格式');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('内容格式转换失败:', error);
|
||||
message.warning('内容格式转换失败,请手动调整内容格式');
|
||||
}
|
||||
};
|
||||
|
||||
const onEditor = (value: any) => {
|
||||
editor.value = value.target.value;
|
||||
};
|
||||
|
||||
const onComments = () => {
|
||||
if (form.comments == undefined) {
|
||||
@@ -737,7 +564,16 @@
|
||||
});
|
||||
};
|
||||
|
||||
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
|
||||
const editorRef = ref<any>(null);
|
||||
|
||||
// markdown-it 实例:编辑回显时把老 Markdown 内容转成 HTML 喂给富文本编辑器
|
||||
const md = markdownit();
|
||||
const toEditorContent = (val: string): string => {
|
||||
if (!val) return '';
|
||||
// 已是 HTML(含标签)则原样返回,否则用 markdown-it 转 HTML
|
||||
if (/<[a-z][\s\S]*>/i.test(val)) return val;
|
||||
return md.render(val);
|
||||
};
|
||||
|
||||
// 文件库选择弹窗状态
|
||||
const showFileSelector = ref(false);
|
||||
@@ -747,324 +583,6 @@
|
||||
const showVideoSelector = ref(false);
|
||||
const videoSelectCallback = ref<((url: string) => void) | null>(null);
|
||||
|
||||
// 📝 Markdown编辑器配置
|
||||
const markdownToolbars = [
|
||||
'bold',
|
||||
'underline',
|
||||
'italic',
|
||||
'-',
|
||||
'title',
|
||||
'strikeThrough',
|
||||
'sub',
|
||||
'sup',
|
||||
'quote',
|
||||
'unorderedList',
|
||||
'orderedList',
|
||||
'task',
|
||||
'-',
|
||||
'codeRow',
|
||||
'code',
|
||||
'link',
|
||||
'image',
|
||||
'table',
|
||||
'mermaid',
|
||||
'katex',
|
||||
'-',
|
||||
'revoke',
|
||||
'next',
|
||||
'save',
|
||||
'=',
|
||||
'pageFullscreen',
|
||||
'fullscreen',
|
||||
'preview',
|
||||
'previewOnly',
|
||||
'htmlPreview',
|
||||
'catalog'
|
||||
] as any;
|
||||
|
||||
// 📝 Markdown编辑器图片上传处理
|
||||
const onMarkdownUploadImg = async (
|
||||
files: File[],
|
||||
callback: (urls: string[]) => void
|
||||
) => {
|
||||
try {
|
||||
const uploadPromises = files.map(async (file) => {
|
||||
// 检查文件大小(限制为10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.error(`图片 ${file.name} 大小超过10MB,请选择更小的文件`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error(`文件 ${file.name} 不是有效的图片格式`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadOss(file);
|
||||
return result.url || result.path;
|
||||
} catch (error) {
|
||||
console.error('图片上传失败:', error);
|
||||
message.error(`图片 ${file.name} 上传失败`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(uploadPromises);
|
||||
const successUrls = results.filter((url) => url !== null) as string[];
|
||||
|
||||
if (successUrls.length > 0) {
|
||||
callback(successUrls);
|
||||
message.success(`成功上传 ${successUrls.length} 张图片`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量上传失败:', error);
|
||||
message.error('图片上传失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 📝 Markdown编辑器图片选择器
|
||||
const openMarkdownImageSelector = () => {
|
||||
fileSelectCallback.value = (url: string) => {
|
||||
// 在当前光标位置插入Markdown图片语法
|
||||
const imageMarkdown = ``;
|
||||
insertMarkdownText(imageMarkdown);
|
||||
};
|
||||
showFileSelector.value = true;
|
||||
};
|
||||
|
||||
// 📝 Markdown编辑器视频选择器
|
||||
const openMarkdownVideoSelector = () => {
|
||||
videoSelectCallback.value = (url: string) => {
|
||||
// 在当前光标位置插入Markdown视频语法(使用HTML标签)
|
||||
const videoMarkdown = `<video controls style="max-width: 100%; height: auto;">
|
||||
<source src="${url}" type="video/mp4">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>`;
|
||||
insertMarkdownText(videoMarkdown);
|
||||
};
|
||||
showVideoSelector.value = true;
|
||||
};
|
||||
|
||||
// 📝 在Markdown编辑器中插入文本
|
||||
const insertMarkdownText = (text: string) => {
|
||||
// 简单的文本插入,在内容末尾添加
|
||||
if (content.value) {
|
||||
content.value += '\n\n' + text;
|
||||
} else {
|
||||
content.value = text;
|
||||
}
|
||||
};
|
||||
|
||||
const config = ref({
|
||||
height: 420,
|
||||
paste_data_images: true,
|
||||
automatic_uploads: true,
|
||||
|
||||
// 自定义工具栏,移除默认的image和media按钮,添加自定义按钮 (视频、上传视频按钮 custom_video_selector quick_video_upload)(首行缩进 )
|
||||
toolbar: [
|
||||
'fullscreen preview code codesample emoticons auto_format custom_image_selector quick_upload',
|
||||
'undo redo | forecolor backcolor',
|
||||
'bold italic underline strikethrough',
|
||||
'alignleft aligncenter alignright alignjustify',
|
||||
'outdent indent | numlist bullist',
|
||||
'formatselect fontselect fontsizeselect',
|
||||
'link charmap anchor pagebreak | ltr rtl'
|
||||
].join(' | '),
|
||||
|
||||
// 图片上传处理器 - 支持拖拽和粘贴上传
|
||||
images_upload_handler: (blobInfo, success, error) => {
|
||||
const file = blobInfo.blob();
|
||||
|
||||
// 检查文件大小(限制为10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
error('图片大小不能超过10MB');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
error('只能上传图片文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示上传进度提示
|
||||
const loadingMsg = message.loading('图片上传中...', 0);
|
||||
|
||||
uploadOss(file)
|
||||
.then((res) => {
|
||||
loadingMsg();
|
||||
success(res.url || res.path);
|
||||
message.success('图片上传成功');
|
||||
})
|
||||
.catch((msg) => {
|
||||
loadingMsg();
|
||||
error(msg || '图片上传失败');
|
||||
message.error('图片上传失败:' + msg);
|
||||
});
|
||||
},
|
||||
|
||||
// 图片工具栏
|
||||
image_toolbar:
|
||||
'alignleft aligncenter alignright | rotateleft rotateright | imageoptions',
|
||||
|
||||
// 图片标题
|
||||
image_title: true,
|
||||
|
||||
// 图片描述
|
||||
image_description: true,
|
||||
|
||||
// 图片尺寸
|
||||
image_dimensions: true,
|
||||
|
||||
// 图片类别
|
||||
image_class_list: [
|
||||
{ title: '无样式', value: '' },
|
||||
{ title: '响应式图片', value: 'img-responsive' },
|
||||
{ title: '圆角图片', value: 'img-rounded' },
|
||||
{ title: '圆形图片', value: 'img-circle' }
|
||||
],
|
||||
|
||||
// 自定义按钮设置
|
||||
setup: (editor: any) => {
|
||||
// 添加自定义图片选择按钮
|
||||
editor.ui.registry.addButton('custom_image_selector', {
|
||||
text: '图片',
|
||||
icon: 'image',
|
||||
tooltip: '插入图片(从文件库选择或上传)',
|
||||
onAction: () => {
|
||||
// 打开文件库选择弹窗
|
||||
fileSelectCallback.value = (url: string) => {
|
||||
editor.insertContent(
|
||||
`<img src="${url}" alt="图片" style="max-width: 100%;" />`
|
||||
);
|
||||
};
|
||||
showFileSelector.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加快速上传按钮
|
||||
editor.ui.registry.addButton('quick_upload', {
|
||||
text: '上传',
|
||||
icon: 'upload',
|
||||
tooltip: '快速上传图片',
|
||||
onAction: () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
// 检查文件大小
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.error('图片大小不能超过10MB');
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingMsg = message.loading('图片上传中...', 0);
|
||||
uploadOss(file)
|
||||
.then((res) => {
|
||||
loadingMsg();
|
||||
const imageUrl = res.url || res.path;
|
||||
editor.insertContent(
|
||||
`<img src="${imageUrl}" alt="${file.name}" style="max-width: 100%;" />`
|
||||
);
|
||||
message.success('图片上传成功');
|
||||
})
|
||||
.catch((msg) => {
|
||||
loadingMsg();
|
||||
message.error('图片上传失败:' + msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加自定义视频选择按钮
|
||||
editor.ui.registry.addButton('custom_video_selector', {
|
||||
text: '视频',
|
||||
icon: 'embed',
|
||||
tooltip: '插入视频(从视频库选择)',
|
||||
onAction: () => {
|
||||
// 打开视频库选择弹窗
|
||||
videoSelectCallback.value = (url: string) => {
|
||||
// 插入视频标签,使用HTML5 video元素
|
||||
editor.insertContent(`
|
||||
<video controls style="max-width: 100%; height: auto;">
|
||||
<source src="${url}" type="video/mp4">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
`);
|
||||
};
|
||||
showVideoSelector.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加快速视频上传按钮
|
||||
editor.ui.registry.addButton('quick_video_upload', {
|
||||
text: '上传视频',
|
||||
icon: 'upload',
|
||||
tooltip: '快速上传视频',
|
||||
onAction: () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'video/*';
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
// 检查文件大小(限制为100MB)
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
message.error('视频大小不能超过100MB');
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingMsg = message.loading('视频上传中...', 0);
|
||||
uploadOss(file)
|
||||
.then((res) => {
|
||||
loadingMsg();
|
||||
const videoUrl = res.path || res.downloadUrl;
|
||||
editor.insertContent(`
|
||||
<video controls style="max-width: 100%; height: auto;">
|
||||
<source src="${videoUrl}" type="video/mp4">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
`);
|
||||
message.success('视频上传成功');
|
||||
})
|
||||
.catch((msg) => {
|
||||
loadingMsg();
|
||||
message.error('视频上传失败:' + msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加一键排版按钮
|
||||
editor.ui.registry.addButton('auto_format', {
|
||||
text: '排版',
|
||||
icon: 'template',
|
||||
tooltip: '智能优化文章格式和排版',
|
||||
onAction: () => {
|
||||
// 直接在这里处理排版,因为此时编辑器肯定已经初始化完成
|
||||
handleAutoFormat(editor);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加段落首行缩进切换按钮
|
||||
editor.ui.registry.addButton('toggle_indent', {
|
||||
icon: 'indent',
|
||||
tooltip: '切换段落首行缩进(适合中文排版)',
|
||||
onAction: () => {
|
||||
toggleParagraphIndent(editor);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 从文件库选择图片的回调
|
||||
const onFileSelected = (data: FileRecord) => {
|
||||
if (fileSelectCallback.value) {
|
||||
@@ -1093,11 +611,31 @@
|
||||
showVideoSelector.value = false;
|
||||
};
|
||||
|
||||
// 从图片库选择图片:插入到 WangEditor 当前光标处
|
||||
const openImageSelector = () => {
|
||||
fileSelectCallback.value = (url: string) => {
|
||||
editorRef.value?.insertHtml(
|
||||
`<img src="${url}" alt="图片" style="max-width:100%;"/>`
|
||||
);
|
||||
};
|
||||
showFileSelector.value = true;
|
||||
};
|
||||
|
||||
// 从视频库选择视频:插入到 WangEditor 当前光标处
|
||||
const openVideoSelector = () => {
|
||||
videoSelectCallback.value = (url: string) => {
|
||||
editorRef.value?.insertHtml(
|
||||
`<video controls style="max-width:100%;height:auto;"><source src="${url}" type="video/mp4"></video>`
|
||||
);
|
||||
};
|
||||
showVideoSelector.value = true;
|
||||
};
|
||||
|
||||
// 🎨 智能一键排版 - 人性化设计
|
||||
const handleAutoFormat = (editor: any) => {
|
||||
const handleAutoFormat = () => {
|
||||
try {
|
||||
// 1. 检查内容
|
||||
const content = editor.getContent();
|
||||
const content = content.value;
|
||||
if (
|
||||
!content ||
|
||||
content.trim() === '' ||
|
||||
@@ -1121,7 +659,7 @@
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const optimizedContent = smartFormatContent(content);
|
||||
editor.setContent(optimizedContent);
|
||||
content.value = optimizedContent;
|
||||
|
||||
loadingMsg();
|
||||
|
||||
@@ -1316,9 +854,9 @@
|
||||
};
|
||||
|
||||
// 🔄 段落首行缩进切换功能
|
||||
const toggleParagraphIndent = (editor: any) => {
|
||||
const toggleParagraphIndent = () => {
|
||||
try {
|
||||
const content = editor.getContent();
|
||||
const content = content.value;
|
||||
|
||||
if (
|
||||
!content ||
|
||||
@@ -1351,7 +889,7 @@
|
||||
actionText = '已添加段落首行缩进';
|
||||
}
|
||||
|
||||
editor.setContent(newContent);
|
||||
content.value = newContent;
|
||||
|
||||
message.success({
|
||||
content: `📐 ${actionText}`,
|
||||
@@ -1449,7 +987,7 @@
|
||||
}
|
||||
const formData = {
|
||||
...form,
|
||||
editor: editor.value || 1,
|
||||
editor: 1,
|
||||
status: setting.setting?.articleReview ? 1 : 0,
|
||||
content: content.value
|
||||
};
|
||||
@@ -1480,12 +1018,6 @@
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (localStorage.getItem('Editor')) {
|
||||
editor.value = Number(localStorage.getItem('Editor'));
|
||||
} else {
|
||||
editor.value = 1;
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
category.value = [];
|
||||
@@ -1499,7 +1031,7 @@
|
||||
// 文章详情
|
||||
assignObject(form, data);
|
||||
if (data.content) {
|
||||
content.value = data.content;
|
||||
content.value = toEditorContent(data.content);
|
||||
}
|
||||
if (!data.source) {
|
||||
form.source = undefined;
|
||||
@@ -1509,9 +1041,6 @@
|
||||
} else {
|
||||
form.tags = undefined;
|
||||
}
|
||||
if (data.editor) {
|
||||
editor.value = data.editor;
|
||||
}
|
||||
if (data.files) {
|
||||
const arr = JSON.parse(data.files);
|
||||
arr.map((url: string) => {
|
||||
@@ -1561,26 +1090,6 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
// 编辑器内图片样式
|
||||
:deep(.mce-content-body) {
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
&.img-responsive {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.img-rounded {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&.img-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文件选择提示
|
||||
|
||||
Reference in New Issue
Block a user