Files
gxwebsoft 07add26fe6 refactor(cmsArticle): 统一文章编辑器为单一 WangEditor 富文本模式
- 删除原有 TinyMCE 富文本和 MdEditor Markdown 双模式切换及相关逻辑
- 替换文章编辑弹窗内容详情为 RichTextEditor 组件
- 新增图片库和视频库选择按钮,支持插入图片和视频 HTML 标签
- 实现一键排版和首行缩进按钮,直接操作内容 HTML 字符串
- 回显时 Markdown 内容转换为 HTML,保存时固定 editor 类型为 1
- 移除无用的配置和样式,简化代码逻辑,提升编辑体验
- 留下文章编辑器类型设置字段待后续清理,展示端保持一致 HTML 渲染方式
2026-08-11 21:58:09 +08:00

13 KiB
Raw Permalink Blame History

2026-08-11 工作记录

运费模板配送地区改为「省+市两级多选穿梭弹窗」

需求背景

客户要求运费模板的配送地区选择改成仿截图的双栏穿梭弹窗(左侧候选 + 右侧已选), 选择粒度从原「省+市单选(级联)」升级为「多条规则覆盖多城市」,并保留「不选=全国」规则。

改动清单(纯前端,后端字段由用户确认可改)

  1. 新建组件 src/components/RegionMultiSelect/index.vue
    • 树形可展开:省级可展开看市级;省级"选择"=选该省全部市,市级可单独选。
    • 左栏搜索(按省/市名过滤)、右栏已选列表(单项是"省/市"+取消)。
    • 底部:全部选择 / 取消 / 确认。
    • v-model:value 绑定已选城市ID数组;额外 emit update:provinceIds(由城市推导的省份ID数组)。
    • 复用 src/components/RegionsSelect/load-data.ts 的 getRegionsData()public/json/regions-data.json,取省+市两级,丢弃区级)。
  2. model src/api/shop/shopExpressTemplateDetail/model/index.ts
    • 新增 provinceIds?: string(逗号分隔)、cityIds?: string(逗号分隔);保留旧 provinceId/cityId 兼容历史数据。
  3. 编辑弹窗 src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue
    • 移除 RegionsSelect type="provinceCity",改用 RegionMultiSelect
    • 打开时从 record.cityIds(兼容旧 cityId)回填;保存时数组→逗号串提交 cityIds/provinceIds
  4. 列表页 src/views/shop/shopExpressTemplateDetail/index.vue
    • "配送地区"列 customRender 改为遍历 cityIds 多值反查 regionMap;超22字截断+tooltip;旧单值数据兼容。
    • 新增 h 函数 import。

待办 / 风险

  • antd 为 v4var(--ant-color-border) 在暗色下可能回退浅灰(边框略浅,不影响功能);如要完美暗色适配可后续改 less 变量。
  • 构建验证(vite build)已通过。

⚠️ 重大修订(同日):前端字段名与后端不匹配,导致"没存进库"

根因

  • 后端 ShopExpressTemplateDetail 实体已有配送区域多选机制:regionMode(0=全国 / 1=指定配送白名单 / 2=不发货黑名单)+ regionIds(逗号分隔的 6 位城市编码串)。
  • 旧代码 provinceId/cityId 是单值历史字段,已被迁移脚本 sql/shop_express_template_detail_region.sql 迁成 regionMode=1, regionIds=城市编码
  • 我第一版前端提交的是 provinceIds/cityIds,后端实体无此字段 → MyBatis-Plus 自动忽略未知字段 → 看起来"没存没变化"。
  • ORM 是 MyBatis-Plusadd/update 用 save()/updateById() 自动生成 SQL,接收的是实体类本身(非 Param);新增字段只需在实体加属性 + 表加列,无需改 Mapper XML。

修复(对接后端已有的 regionMode/regionIds,无需后端改动)

  1. modelprovinceIds/cityIds → 改为 regionMode?regionIds?
  2. RegionMultiSelect 组件:去掉 update:provinceIds emit,只 emit update:value(城市编码数组)。
  3. 编辑弹窗:城市选择变化 → form.regionIds = 逗号串form.regionMode = 1(有选)或 0(全国);回显从 form.regionIds 拆出。
  4. 列表页:customRender 读 record.regionIds 反查城市名。
  • 结论:本需求纯前端即可闭环,后端无需改。城市编码(regions-data.json 的 city value6位)与 regionIds 完全一致,下单校验 DeliveryRegionCheckerregionIds 直接生效。

修复:多规格SKU表格缺少库存列

  • 文件:src/views/shop/shopGoods/components/shopGoodsEdit.vue
  • 问题:columns 数组缺少 stock(库存) 和 salePrice(划线价) 列定义,导致多规格SKU明细表不显示库存输入框(批量设置栏有,但表格列没有)
  • 修复:在 columns 数组中补齐 salePricestock 两列(bodyCell 模板已有渲染逻辑,只需加列定义)

修复:商品保存「快递配送时运费模板必选」前端校验缺失

  • 文件:src/views/shop/shopGoods/components/shopGoodsEdit.vue
  • 问题:后端有校验规则——canExpress=1 时要求 expressTemplateId 必填,但前端无对应校验,用户开启快递配送后直接提交被后端拦住,报错"运费模板必选"。
  • 修复:在 save() 的 validate 通过后、请求前增加前端拦截:canExpress=1 && !expressTemplateIdmessage.error('请选择运费模板')
  • UI 优化:运费模板 <a-form-item>:required="form.canExpress === 1" 动态必填标记 + placeholder 提示文字。

修复:多规格SKU会员价(buyingPrice)保存失败

  • 根因:后端 ShopGoodsSku 实体类(guilixu-java + mp-java)和数据库表 shop_goods_sku 均无 buying_price 字段。前端提交的 buyingPrice 被 MyBatis-Plus 忽略 → 保存后丢失。
  • 改动(三端同步)
    1. guilixu-java entity/ShopGoodsSku.java:加 private BigDecimal buyingPrice;
    2. mp-java entity/ShopGoodsSku.java:同上(shop 模块两端同步)
    3. 前端 TS 接口 api/shop/shopGoodsSku/model/index.ts:加 buyingPrice?: string;
  • 需手动执行 SQLALTER TABLE shop_goods_sku ADD COLUMN buying_price DECIMAL(10,2) DEFAULT NULL COMMENT '会员价' AFTER cost;

修复:多规格SKU图片删不掉

  • 文件:src/views/shop/shopGoods/components/shopGoodsEdit.vue
  • 问题:onDeleteSkuItem(index) 误删 images.value(主商品图片数组),而非 skuList.value[index].images(对应SKU行图片)→ 点击 × 无反应
  • 修复:改为清空 skuList.value[index].images = [] + image = ''

商品列表「价格」列支持多规格价格区间(方案A)

  • 需求:多规格商品各 SKU 价格不同,原列表价格列只显示主表 price/salePrice/dealerPrice(占位值),与 SKU 实际价格对不上。确认采用方案A:多规格显示 ¥最低价 ~ ¥最高价,单规格保持单值
  • 后端(guilixu-java,生效后端):
    1. entity/ShopGoods.java:加 3 个非持久化字段 priceMax / salePriceMax / dealerPriceMaxBigDecimal@TableField(exist = false))。
    2. service/impl/ShopGoodsServiceImpl.javapageRel/listRel 查完后调 fillSkuPriceRange(list)——收集 specs==1 的商品,shopGoodsSkuService.lambdaQuery().in(goodsId, ids) 一次批量查 SKU,按 price/salePrice/buyingPrice 算 min/max 回填(主表 price=minpriceMax=max;市场价/会员价同理)。只填内存对象、不落库。
  • 前端(guilixu-admin):
    1. api/shop/shopGoods/model/index.tsShopGoods 加 priceMax? / salePriceMax? / dealerPriceMax?
    2. views/shop/shopGoods/index.vuepriceGroup 列渲染改为「区间不相等时 ¥min ~ ¥max,否则单值」;导出 Excel 新增 fmtPrice() 同步处理。
  • 验证:guilixu-java mvn -o compile 通过(本机无全局 mvn,临时用 /tmp/apache-maven-3.9.6 离线编译);guilixu-admin vite build 通过。
  • 待确认:mp-java 是否需要同步——本部署 admin 商城走 guilixu-javamp-java 未参与;若 mp-java 侧也有管理端/列表调用 ShopGoods.listRel,则需同步 fillSkuPriceRange + 实体字段。

修复:商品「是否可以快递配送 / 运费模板」保存不成功(同根因)

  • 根因ShopGoods 实体(guilixu-java)与 shop_goods都没有 can_express / express_template_id 字段。前端提交这两个值被 MyBatis-Plus 静默丢弃(与 8-11 多规格 buyingPrice 同一类问题)。
  • 改动
    1. guilixu-java entity/ShopGoods.java:加 private Integer canExpress;(是否可以快递配送 0/1+ private Integer expressTemplateId;(运费模板ID,关联 shop_express_template.id)。map-underscore-to-camel-case=true 已开,自动映射 can_express/express_template_id
    2. 迁移 SQL(新建)guilixu-java/sql/add_shop_goods_express_fields.sqlALTER TABLE shop_goods ADD can_express TINYINT NOT NULL DEFAULT 1, ADD express_template_id INT NULL;
    3. 前端 shopGoodsEdit.vue:把"运费模板"表单项从「基本信息」页签移到「营销设置」页签内、"是否可以快递配送"开关正下方(用户要求)。
  • ⚠️ 上线前置动作(否则不生效):必须在 db_guilixu 执行该 SQL 加列,且 guilixu-java 需重新编译部署(实体加了字段)。
  • 此修复同时解决"运费模板"也存不进库的问题(同一根因,用户只报了 canExpress)。
  • 前端 运费模板 必填校验(save 内 canExpress===1 && !expressTemplateId)与动态 label 已早先存在,逻辑无需改。

商品管理编辑弹窗:Markdown 编辑器换成 WangEditor 富文本

需求

用户反馈商品「编辑弹窗」的 Markdown 编辑器不好用,要求换成 website-admin 的 app/pages/articles.vue 那套编辑器。

方案(经评估后采用)

  • articles.vue 用的是 app/components/RichTextEditor.vueWangEditor @wangeditor/editor-for-vue,输出 HTML),并非 Markdown。
  • 采用「编辑时按需转换」策略:老商品 content 是 Markdown,编辑回显时用 markdown-it 转成 HTML 再进富文本编辑器;已是 HTML 则原样返回。零 DB 风险,存储格式新旧混存。

改动清单

  1. 新增依赖guilixu-adminpnpm):@wangeditor/editor@^5@wangeditor/editor-for-vue@^5markdown-it@^14@types/markdown-it@^14(已写 package.json + node_modules)。
  2. 新建组件 src/components/RichTextEditor.vue:从 website-admin 的同名组件复制,删除 Nuxt 专属 <ClientOnly> 包装guilixu-admin 是 Vite SPA 无此组件);图片上传用 @/api/system/fileuploadFile(返回 res.url || res.pathadmin 已有)。
  3. 改造 src/views/shop/shopGoods/components/shopGoodsEdit.vue
    • 移除 MdEditormd-editor-v3+ 相关 import / markdownToolbars / onMarkdownUploadImg / editorVisible watch / disabled ref / editorRef / 文件库 SelectData 弹窗(无打开入口,死代码)/ :deep(.md-editor-*) 样式。
    • 商品详情 tab 内改用 <RichTextEditor v-model="content" :height="520" v-if="active==='content'"/>(仅在 tab 激活时挂载,规避隐藏容器初始化宽度 0 问题)。
    • 新增 md = markdownit()toEditorContent(raw),编辑回显 content.value = toEditorContent(props.data.content)
    • 补回被误删的 FileRecord importchooseImage 等函数仍用)。
  4. 保存逻辑不变save() 已读 content.value,直接提交 HTML。

验证

  • pnpm buildvite build)通过,exit 0dist 正常产出。

风险 / 待确认

  • 消费端(小程序 guilixu-taro/src/pages/shop/product-detail.tsx)已兼容两种格式parseContent![alt](url)<img>,其余 HTML 直接 RichText nodes 渲染 → 新旧 content 都能展示,不崩。
  • 潜在坑:若 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 切换逻辑、markdownToolbarsonMarkdownUploadImgopenMarkdownImageSelector/openMarkdownVideoSelectorinsertMarkdownText、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 域名白名单同商品模块,需真机确认。