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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user