diff --git a/.workbuddy/memory/2026-08-11.md b/.workbuddy/memory/2026-08-11.md index 742a0d0..3651e0f 100644 --- a/.workbuddy/memory/2026-08-11.md +++ b/.workbuddy/memory/2026-08-11.md @@ -113,3 +113,18 @@ - **潜在坑**:若 HTML content 里含裸 `.jpg` 链接(非 `` 包裹,如 ``),`parseContent` 的兜底会把该 URL 误转成 `` 破坏链接。WangEditor 产出的图片都是 ``,正常商品详情极少触发,低概率。 - `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(仅在删块中使用)。 +- 新增/改写: + - 内容详情改用 ``。 + - 编辑器下方按钮区:`📷从图片库选择`/`🎬从视频库选择`(调 `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 域名白名单同商品模块,需真机确认。 diff --git a/src/views/cms/cmsArticle/components/articleEdit.vue b/src/views/cms/cmsArticle/components/articleEdit.vue index ee3a296..eb55b67 100644 --- a/src/views/cms/cmsArticle/components/articleEdit.vue +++ b/src/views/cms/cmsArticle/components/articleEdit.vue @@ -80,58 +80,22 @@ /> - -
- -
- 💡 - 提示:工具栏"图片"按钮从图片库选择,"上传"按钮快速上传图片;"视频"按钮从视频库选择,"上传视频"按钮快速上传视频;"一键排版"按钮自动优化文章格式;"首行缩进"按钮切换段落缩进 -
+ +
+ 💡 提示:支持直接粘贴/拖拽上传图片;点击下方按钮从图片库/视频库选择,或使用「排版」「首行缩进」优化文章
- - -
- -
- - 📷 从图片库选择 - - - 🎬 从视频库选择 - -
- - -
- 💡 - 提示:支持Markdown语法,可以使用工具栏按钮从文件库选择图片/视频,也可以直接拖拽上传文件 -
-
- + + 📷 从图片库选择 + 🎬 从视频库选择 + 🎨 一键排版 + 📐 首行缩进 + (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 !== '


' - ) { - 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>/gi, '# $1\n\n') - .replace(/]*>(.*?)<\/h2>/gi, '## $1\n\n') - .replace(/]*>(.*?)<\/h3>/gi, '### $1\n\n') - .replace(/]*>(.*?)<\/h4>/gi, '#### $1\n\n') - .replace(/]*>(.*?)<\/h5>/gi, '##### $1\n\n') - .replace(/]*>(.*?)<\/h6>/gi, '###### $1\n\n') - .replace(/]*>(.*?)<\/strong>/gi, '**$1**') - .replace(/]*>(.*?)<\/b>/gi, '**$1**') - .replace(/]*>(.*?)<\/em>/gi, '*$1*') - .replace(/]*>(.*?)<\/i>/gi, '*$1*') - .replace(/]*>(.*?)<\/code>/gi, '`$1`') - .replace(/]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)') - .replace( - /]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi, - '![$2]($1)' - ) - .replace(/]*src="([^"]*)"[^>]*>/gi, '![图片]($1)') - .replace(/]*>(.*?)<\/p>/gi, '$1\n\n') - .replace(/]*>/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, '

$1

') - .replace(/^## (.*$)/gim, '

$1

') - .replace(/^### (.*$)/gim, '

$1

') - .replace(/^#### (.*$)/gim, '

$1

') - .replace(/^##### (.*$)/gim, '
$1
') - .replace(/^###### (.*$)/gim, '
$1
') - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/\*(.*?)\*/g, '$1') - .replace(/`(.*?)`/g, '$1') - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '
$1') - .replace( - /!\[([^\]]*)\]\(([^)]+)\)/g, - '$1' - ) - .replace(/\n\n/g, '

') - .replace(/\n/g, '
'); - - // 包装在段落标签中 - if (htmlContent && !htmlContent.startsWith('<')) { - htmlContent = '

' + htmlContent + '

'; - } - - 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 | null>(null); + const editorRef = ref(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 = `![图片](${url})`; - insertMarkdownText(imageMarkdown); - }; - showFileSelector.value = true; - }; - - // 📝 Markdown编辑器视频选择器 - const openMarkdownVideoSelector = () => { - videoSelectCallback.value = (url: string) => { - // 在当前光标位置插入Markdown视频语法(使用HTML标签) - const videoMarkdown = ``; - 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( - `图片` - ); - }; - 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( - `${file.name}` - ); - 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(` - - `); - }; - 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(` - - `); - 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( + `图片` + ); + }; + showFileSelector.value = true; + }; + + // 从视频库选择视频:插入到 WangEditor 当前光标处 + const openVideoSelector = () => { + videoSelectCallback.value = (url: string) => { + editorRef.value?.insertHtml( + `` + ); + }; + 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%; - } - } - } } // 文件选择提示