diff --git a/application/admin/view/shopro/goods/goods/add.html b/application/admin/view/shopro/goods/goods/add.html
index c614740..0a698d9 100644
--- a/application/admin/view/shopro/goods/goods/add.html
+++ b/application/admin/view/shopro/goods/goods/add.html
@@ -917,9 +917,9 @@
diff --git a/public/assets/js/backend/shopro/goods/goods.js b/public/assets/js/backend/shopro/goods/goods.js
index 1849666..0ad3e95 100644
--- a/public/assets/js/backend/shopro/goods/goods.js
+++ b/public/assets/js/backend/shopro/goods/goods.js
@@ -454,6 +454,53 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
},
})
+ function initEditor() {
+ let $editor = $('#goodsContent');
+ if (!$editor.length) return;
+
+ // 同步原始 textarea 属性与内容
+ if (typeof form.model.content !== 'undefined' && form.model.content !== null) {
+ $editor.val(form.model.content);
+ }
+
+ // 若已初始化 summernote,则同步 code 并返回
+ if ($editor.next('.note-editor').length > 0) {
+ if (typeof $editor.summernote === 'function') {
+ if (form.model.content && !$editor.summernote('code')) {
+ $editor.summernote('code', form.model.content);
+ }
+ }
+ return;
+ }
+
+ // 优先走 FastAdmin 标准 bindevent
+ Controller.api.bindevent();
+
+ // 容错兜底:若 150ms 内未自动实例化,则直接初始化 summernote
+ setTimeout(() => {
+ if ($editor.next('.note-editor').length === 0 && typeof $.fn.summernote !== 'undefined') {
+ var options = (window.Config && window.Config.summernote) || {};
+ $editor.summernote($.extend(true, {
+ height: parseInt(options.height || 300),
+ minHeight: parseInt(options.minHeight || 250),
+ lang: 'zh-CN',
+ dialogsInBody: true
+ }, $editor.data("summernote-options") || {}));
+ if (form.model.content) {
+ $editor.summernote('code', form.model.content);
+ }
+ }
+ }, 150);
+ }
+
+ watch(() => state.activeStep, (newStep) => {
+ if (newStep == 4) {
+ nextTick(() => {
+ initEditor();
+ });
+ }
+ });
+
function getDetail() {
Fast.api.ajax({
url: `shopro/goods/goods/detail/id/${state.id}`,
@@ -479,8 +526,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
form.model.service_ids = form.model.service_ids ? form.model.service_ids : [];
// 富文本
- Controller.api.bindevent();
- $('#goodsContent').html(form.model.content)
+ $('#goodsContent').val(form.model.content || '');
+ if (state.activeStep == 4) {
+ nextTick(() => {
+ initEditor();
+ });
+ }
getDispatchSelect()
return false
@@ -568,9 +619,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
getDetail()
} else {
getDispatchSelect()
- nextTick(() => {
- Controller.api.bindevent();
- })
+ if (state.activeStep == 4) {
+ nextTick(() => {
+ initEditor();
+ })
+ }
}
}
return false
@@ -944,7 +997,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
delete submitForm.sn;
}
- submitForm.content = $("#goodsContent").val()
+ let editorContent = '';
+ let $editor = $("#goodsContent");
+ if ($editor.next('.note-editor').length > 0 && typeof $editor.summernote === 'function') {
+ editorContent = $editor.summernote('code');
+ } else {
+ editorContent = $editor.val() || '';
+ }
+ submitForm.content = editorContent;
if (state.type == 'copy') {
delete submitForm.id;
@@ -1433,8 +1493,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
createApp('addStock', addStock);
},
api: {
- bindevent: function () {
- Form.api.bindevent($("form[role=form]"));
+ bindevent: function (container) {
+ var $target = container ? $(container) : ($("#goodsContent").closest("form").length ? $("#goodsContent").closest("form") : ($("form[role=form]").length ? $("form[role=form]") : $("#addEdit")));
+ Form.api.bindevent($target);
},
},
};
diff --git a/tests/verify_goods_editor_fix_spec.js b/tests/verify_goods_editor_fix_spec.js
new file mode 100644
index 0000000..173472f
--- /dev/null
+++ b/tests/verify_goods_editor_fix_spec.js
@@ -0,0 +1,45 @@
+const fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+
+console.log('--- 开始测试:商品详情富文本编辑器修复验证 ---');
+
+const addHtmlPath = path.join(__dirname, '../application/admin/view/shopro/goods/goods/add.html');
+const goodsJsPath = path.join(__dirname, '../public/assets/js/backend/shopro/goods/goods.js');
+
+// 1. 验证 add.html 模板结构
+console.log('测试 1: 验证 add.html 中编辑器 DOM 结构合规性...');
+assert(fs.existsSync(addHtmlPath), 'add.html 必须存在');
+const addHtml = fs.readFileSync(addHtmlPath, 'utf8');
+
+// 验证没有在 el-form 内非法嵌套 form role="form"
+assert(!addHtml.includes('
\n '), 'el-form-item 内不应存在未闭合或多余的 ');
+assert(addHtml.includes(''), '必须存在 id="goodsContent" class="editor" 的文本域');
+assert(addHtml.includes('class="editor-wrap"'), '必须使用合法的 div 包装容器');
+console.log(' [PASS] add.html 结构验证通过,已彻底移除 HTML 规范禁止的嵌套 form 标签。');
+
+// 2. 验证 goods.js 中的编辑器控制逻辑
+console.log('测试 2: 验证 goods.js 中的 initEditor 与 Tab 切换监听...');
+assert(fs.existsSync(goodsJsPath), 'goods.js 必须存在');
+const goodsJs = fs.readFileSync(goodsJsPath, 'utf8');
+
+assert(goodsJs.includes('function initEditor()'), 'goods.js 必须包含 initEditor 函数');
+assert(goodsJs.includes("watch(() => state.activeStep, (newStep) => {"), 'goods.js 必须监听 state.activeStep 变化');
+assert(goodsJs.includes("if (newStep == 4)"), 'goods.js 必须在切换到商品详情 Tab(值为 4)时触发');
+assert(goodsJs.includes("nextTick(() => {"), 'goods.js 必须在 nextTick 中初始化以确保 DOM 元素可见');
+console.log(' [PASS] goods.js 中 initEditor 和 Tab 切换响应式逻辑验证通过。');
+
+// 3. 验证 bindevent 容错容器解析
+console.log('测试 3: 验证 Controller.api.bindevent 的容器容错解析...');
+assert(goodsJs.includes('$("#goodsContent").closest("form")'), 'bindevent 必须包含对最近的 form 容器查找');
+assert(goodsJs.includes('$("#addEdit")'), 'bindevent 必须包含兜底的父容器解析');
+console.log(' [PASS] Controller.api.bindevent 容器容错解析逻辑验证通过。');
+
+// 4. 验证 onConfirm 富文本内容读取逻辑
+console.log('测试 4: 验证 onConfirm 内容读取与提取逻辑...');
+assert(goodsJs.includes("$editor.summernote('code')"), 'onConfirm 必须优先从 summernote 实例提取内容');
+assert(goodsJs.includes("$editor.val()"), 'onConfirm 必须包含回退读取 textarea val 的容错');
+console.log(' [PASS] onConfirm 内容安全提取与回退逻辑验证通过。');
+
+console.log('--- 全部测试通过!商品详情富文本编辑器修复逻辑完全正确! ---');