feat(shop): 商品管理编辑弹窗替换为WangEditor富文本编辑器

- 用户反馈编辑弹窗的Markdown编辑器体验差,采用website-admin的富文本编辑器替代
- 新增依赖@wangeditor/editor、@wangeditor/editor-for-vue、markdown-it及类型声明
- 新建RichTextEditor组件,去除Nuxt专用包装,适配现有文件上传接口
- shopGoodsEdit.vue移除MdEditor及相关冗余代码,改用RichTextEditor显示内容
- 实现编辑时Markdown内容转换为HTML,保持数据库存储格式新旧兼容
- 保存逻辑保持不变,提交内容为HTML格式
- 确认小程序端可兼容新旧内容格式,潜在HTML图片链接误解析风险
- build验证通过,文件产出正常,图片上传小程序域名白名单需上线后确认
This commit is contained in:
2026-08-11 18:52:40 +08:00
parent b6ae3d71ef
commit cf7d065379
5 changed files with 658 additions and 160 deletions
@@ -106,11 +106,11 @@
v-model:value="form.buyingPrice"
/>
</a-form-item>
<a-form-item :label="form.canExpress === 1 ? '运费模板(必填)' : '运费模板'" v-if="!merchantId" :required="form.canExpress === 1">
<a-form-item :label="form.canExpress === 1 ? '运费模板' : '运费模板'" v-if="!merchantId" :required="form.canExpress === 1">
<a-select
v-model:value="form.expressTemplateId"
style="width: 240px"
:placeholder="form.canExpress === 1 ? '请选择运费模板(必选)' : '请选择运费模板'"
:placeholder="form.canExpress === 1 ? '请选择运费模板' : '请选择运费模板'"
>
<a-select-option
v-for="(item, index) in expressTemplateList"
@@ -212,18 +212,12 @@
</a-form-item>
</a-tab-pane>
<a-tab-pane tab="商品详情" key="content">
<!-- Markdown 编辑器 -->
<MdEditor
<RichTextEditor
v-if="active === 'content'"
v-model="content"
:disabled="disabled"
height="650px"
placeholder="支持 Markdown 语法,可直接粘贴或拖拽图片..."
:toolbars="markdownToolbars"
:onUploadImg="onMarkdownUploadImg"
:height="520"
placeholder="请输入商品详情,支持图文混排..."
/>
<div class="file-selector-tip">
💡 提示支持 Markdown 语法可直接拖拽上传图片也可以使用工具栏按钮从文件库选择图片
</div>
</a-tab-pane>
<a-tab-pane tab="商品规格" key="spec">
<a-form-item label="规格类型" name="specs">
@@ -674,23 +668,6 @@
</a-tabs>
</a-form>
</a-drawer>
<!-- 文件库选择弹窗 -->
<SelectData
v-model:visible="showFileSelector"
title="选择图片"
type="image"
class="file-selector-modal"
@done="onFileSelected"
/>
<!-- 视频库选择弹窗 -->
<SelectData
v-model:visible="showVideoSelector"
title="选择视频"
type="video"
class="file-selector-modal"
@done="onVideoSelected"
/>
</template>
@@ -703,7 +680,7 @@ import {
SettingOutlined,
UploadOutlined
} from '@ant-design/icons-vue';
import {ref, reactive, watch, nextTick} from 'vue';
import {ref, reactive, watch} from 'vue';
import {useRouter} from 'vue-router';
import {Form, message} from 'ant-design-vue';
import {assignObject, messageLoading, uuid} from 'ele-admin-pro';
@@ -713,12 +690,11 @@ import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
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 {ShopMerchant} from '@/api/shop/shopMerchant/model';
import {uploadFile, uploadOss} from '@/api/system/file';
import { MdEditor } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import 'md-editor-v3/lib/preview.css';
import {FileRecord} from '@/api/system/file/model';
import RichTextEditor from '@/components/RichTextEditor.vue';
import markdownit from 'markdown-it';
import {ShopSpec} from '@/api/shop/shopSpec/model';
import {ShopGoodsSku} from '@/api/shop/shopGoodsSku/model';
import {ShopGoodsSpec} from '@/api/shop/shopGoodsSpec/model';
@@ -731,7 +707,6 @@ import {ShopExpressTemplate} from '@/api/shop/shopExpressTemplate/model';
import {listShopExpressTemplate} from '@/api/shop/shopExpressTemplate';
import {listShopCommissionRole} from '@/api/shop/shopCommissionRole';
import {listShopGoodsRoleCommission} from '@/api/shop/shopGoodsRoleCommission';
import SelectData from "@/components/SelectFile/components/select-data.vue";
// 是否是修改
const isUpdate = ref(false);
@@ -762,7 +737,21 @@ const maxable = ref(true);
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const content = ref('');
const disabled = ref(false);
// Markdown -> HTML 转换(编辑回显旧数据时,把 Markdown 内容转成富文本可识别的 HTML)
const md = markdownit();
/**
* 把后端 content 转为富文本编辑器可用的 HTML:
* - 已是 HTML(含标签)则原样返回,避免二次转义;
* - 否则按 Markdown 渲染为 HTML。
*/
const toEditorContent = (raw?: string): string => {
if (!raw) return '';
const s = raw.trim();
if (/<[a-z][\s\S]*>/i.test(s)) return raw;
return md.render(s);
};
// 当前选项卡
const active = ref('base');
@@ -969,34 +958,6 @@ const onDeleteEnsureTag = (tag: string) => {
ensureTag.value.splice(index, 1);
};
// 从文件库选择图片的回调
const onFileSelected = (data: FileRecord) => {
if (fileSelectCallback.value) {
// 使用文件的完整URL,确保有值
const imageUrl = data.url || data.path || '';
if (imageUrl) {
fileSelectCallback.value(imageUrl);
message.success('图片插入成功');
}
fileSelectCallback.value = null;
}
showFileSelector.value = false;
};
// 从视频库选择视频的回调
const onVideoSelected = (data: FileRecord) => {
if (videoSelectCallback.value) {
// 使用文件的完整URL,确保有值
const videoUrl = data.path || data.downloadUrl || '';
if (videoUrl) {
videoSelectCallback.value(videoUrl);
message.success('视频插入成功');
}
videoSelectCallback.value = null;
}
showVideoSelector.value = false;
};
// 🎨 智能一键排版 - 人性化设计
const handleAutoFormat = (editor: any) => {
try {
@@ -1740,88 +1701,6 @@ const batchApply = () => {
};
const editorRef = ref(null);
// 文件库选择弹窗状态
const showFileSelector = ref(false);
const fileSelectCallback = ref<((url: string) => void) | null>(null);
// 视频库选择弹窗状态
const showVideoSelector = ref(false);
const videoSelectCallback = ref<((url: string) => void) | null>(null);
// 编辑器可见性控制
// 删除了 TinyMCE 相关代码,改用 MdEditor
const editorVisible = ref(false);
// 当 Tab 切换到商品详情时,启用编辑器
watch(
() => active.value,
(key) => {
if (key === 'content') {
// 切换到商品详情 Tab 时,等待 DOM 渲染完成后启用编辑器
nextTick(() => {
setTimeout(() => {
editorVisible.value = true;
console.log('[shopGoodsEdit] Switched to content tab, editorVisible = true');
}, 100);
});
}
},
{ immediate: true }
);
const markdownToolbars = [
'bold',
'underline',
'italic',
'-',
'title',
'strikeThrough',
'quote',
'unorderedList',
'orderedList',
'task',
'-',
'codeRow',
'code',
'link',
'image',
'table',
'-',
'revoke',
'next',
'=',
'pageFullscreen',
'fullscreen',
'preview',
'previewOnly'
];
// Markdown 图片上传
const onMarkdownUploadImg = async (
files: File[],
callback: (urls: string[]) => void
) => {
try {
const uploadPromises = files.map(async (file) => {
if (file.size > 10 * 1024 * 1024) {
message.error(`图片 ${file.name} 大小超过10MB`);
return null;
}
const res = await uploadOss(file);
return res.url || res.path;
});
const results = await Promise.all(uploadPromises);
callback(results.filter(Boolean) as string[]);
} catch (err: any) {
message.error(err.message || '图片上传失败');
}
};
// TinyMCE config 已移除,改用 MdEditor
const config = ref({});
const {resetFields} = useForm(form, rules);
const COMMISSION_PERCENT_FIELDS = [
@@ -2164,7 +2043,7 @@ watch(
category.value = JSON.parse(props.data.category);
}
if (props.data.content) {
content.value = props.data.content;
content.value = toEditorContent(props.data.content);
}
isUpdate.value = true;
@@ -2200,17 +2079,4 @@ watch(
</script>
<style scoped>
/* 修复 MdEditor 工具栏图标过大 */
:deep(.md-editor-toolbar-wrapper .md-editor-icon) {
font-size: 16px !important;
width: 16px !important;
height: 16px !important;
}
:deep(.md-editor-toolbar-wrapper svg) {
width: 16px !important;
height: 16px !important;
}
:deep(.md-editor-toolbar-item) {
font-size: 16px !important;
}
</style>