feat(shopGoods): 优化商品列表加载及封面图性能
- image.ts 中 getCompressedImageUrl 增加缓存,避免列表重渲染时重复计算压缩 URL - shopGoods 页面用原生 img 标签替换 antdv a-image,支持浏览器懒加载减少首屏网络压力 - shopGoods 页面数据加载流程优化,取消表格自动首屏加载,改为手动触发,合并请求避免重复调用 - 首屏分类数据延迟加载,不阻塞首屏渲染,提升页面响应速度 - search.vue 中用 onMounted 替代 watch immediate,防止首次入场重复触发请求 - 新增商品封面图样式,保证图片大小固定且样式统一
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
dist.zip
|
||||
.verify-build
|
||||
.DS_Store
|
||||
*.local
|
||||
@@ -0,0 +1,90 @@
|
||||
# 2026-08-05 商品管理页(shopGoods)加载优化分析
|
||||
|
||||
## 关键结论(修正 2026-08-04 误判)
|
||||
- 后端 `mp-java` 的 `ShopGoodsMapper.xml` 已支持 `sceneType` 参数:
|
||||
- `on_sale` → `AND a.status = 0`
|
||||
- `pending` → `AND a.status != 0`
|
||||
- `sold_out` → `AND a.stock = 0`
|
||||
- 前端 `search.vue` 传 `where.sceneType` 给 `pageShopGoods` 是**有效的**(JS 运行时携带,TS 类型未声明但不影响运行)。
|
||||
- 结论:「出售中/待上架/已售罄」状态筛选功能**正常**,**不是卡顿根因**。上轮判定"sceneType 后端不识别"为误判。
|
||||
|
||||
## getCount 合并进 pageShopGoods 的影响面
|
||||
- 商品模块 `getCount`(`/shop/shop-goods/data`)仅被 `src/views/shop/shopGoods/components/search.vue` **一处**调用。
|
||||
- `cms/cmsArticle` 的 `getCount` 是独立接口(`/cms/cms-article/data`),互不影响。
|
||||
- `guilixu-taro`(用户端小程序)未调用 `/shop/shop-goods/data`。
|
||||
- 合并到 page 响应、保留 `/data` 接口 → 向后兼容,零影响其他端。
|
||||
- 收益有限:当前 `getCount` 与 `page` 在 `reload()` 里已是并发触发,合并仅省一次 HTTP 往返,非量级差异。
|
||||
|
||||
## 首屏请求并发情况
|
||||
- 首屏同一帧发 3 个请求:pageShopGoods(datasource)、getCount(search.vue reload)、listShopGoodsCategory(index.vue 顶层 query())。
|
||||
- `search.vue` 的 `watch(() => props.merchantId, ..., { immediate: true })` 与 `index.vue` 顶层 `query()` 存在时序竞争,但单次进入只触发一次,未构成死循环。
|
||||
|
||||
## 卡顿真因(待 profiling 确认,未执行改动)
|
||||
- 头号嫌疑:商品封面 `<a-image>`(Ant Design Vue)不支持原生 `loading="lazy"`,首屏多行图片一次性并发请求;另 `getCompressedImageUrl` 每次 v-for 渲染重算。
|
||||
- 次因:首屏 3 并发 + watch immediate 竞争。
|
||||
- 建议:让用户开 DevTools → Network 看 `pageShopGoods` 耗时与是否有重复/慢请求,再定改动优先级。
|
||||
|
||||
## 用户决策(本轮)
|
||||
- 已售罄=库存为0(与后端一致,确认)。
|
||||
- getCount 合并影响面小但收益有限,待用户定。
|
||||
- 导出(listShopGoods 全量)本轮先不做。
|
||||
- 澄清:要替换的是商品封面 `<a-image>`,**不是 ele-pro-table 整体**;仅当前模块。
|
||||
|
||||
## 后端 /data(getCount)实现细节(影响合并决策)
|
||||
- `ShopGoodsController.data()` 实际执行:
|
||||
1. `count(status=0 AND stock>0)` → totalNum(出售中)
|
||||
2. `count(status>0)` → totalNum2(待上架)
|
||||
3. `count(stock=0)` → totalNum3(已售罄)
|
||||
4. **`update shop_goods SET status=1 WHERE stock=0`** ← 写副作用!每次调 /data 都会把已售罄商品自动下架。
|
||||
- `/page` 用 MyBatis-Plus `selectPageRel`,内部已含 1 次 count(total) + 1 次 select,共 2 SQL。
|
||||
- 现状进入/筛选:page(2 SQL) + data(3 count + 1 update = 4 SQL) 并发。
|
||||
- 翻页/排序:只调 page(2 SQL),**不触发 data 的 update**。
|
||||
|
||||
## getCount 合并进 /page 的后端消耗结论(负优化,不建议)
|
||||
- 合并后每次 page 调用(含翻页/排序)都多跑 3 次 count + 1 次 update → 翻页/排序 SQL 从 2 飙到 6。
|
||||
- 列表返回变慢:原来 page 的 2 SQL 快速回,data 的 4 SQL 异步在后;合并后列表要等额外 3 count + 1 update 算完才返回。
|
||||
- 写副作用被高频触发:翻页/排序也跑 `UPDATE ... WHERE stock=0`,幂等但带来行锁竞争,且写操作混在读链路是反模式。
|
||||
- 总 DB 读次数在「进入/筛选」场景不变,但「翻页/排序」场景陡增;净收益为负(仅省一次 HTTP 往返)。
|
||||
- **结论:单纯合并是负优化,不执行。** 真要优化后端统计应:① 3 次 count 合成 1 次 GROUP BY 条件聚合;② 把 update 副作用从读接口剥离(移到定时任务/下单扣库存时);③ 再谈是否挂到 page 响应。前端侧保持并发(Promise.all)即可,零后端改动。
|
||||
|
||||
## 已执行的前端优化(2026-08-05)
|
||||
用户确认执行,已改 3 个文件:
|
||||
|
||||
### 1. 商品封面图懒加载(index.vue + image.ts)★ 最可能见效
|
||||
- `src/views/shop/shopGoods/index.vue` 第 39 行附近:`<a-image :src :preview=false :width :height>` 替换为原生
|
||||
`<img class="goods-thumb" loading="lazy" width="50" height="50" alt="">`。
|
||||
- 原因:antdv `a-image` 不支持转发原生 `loading="lazy"`,且即使 `:preview=false` 仍包一层容器/事件;换成原生 `<img>` 后由浏览器 IntersectionObserver 懒加载,首屏仅可视区~10 张图请求。
|
||||
- 新增 `.goods-thumb` 样式(object-fit:cover、圆角 4px、暗色占位背景)。
|
||||
- `src/utils/image.ts`:`getCompressedImageUrl` 加 `Map<string,string>` 缓存,避免列表重渲染(勾选/hover/滚动)时重复拼接 OSS URL 字符串。全局受益,纯函数无副作用。
|
||||
|
||||
### 2. 首屏请求收敛(index.vue + search.vue)
|
||||
- `index.vue`:`<ele-pro-table>` 加 `:init-load="false"`(关闭表格自身自动首屏加载)。
|
||||
- `search.vue`:去掉 `watch(merchantId, {...immediate:true})`,改为 `onMounted` 主动触发一次 `reload()`(设置 where.merchantId + 发 getCount + emit search 触发表格首次加载)。
|
||||
- 原因:原 `initLoad`(默认 true) + `watch immediate` 导致首屏重复发一次 `pageShopGoods`(自动加载一次 + watch 又 reload 一次);改后首屏只有 1 次 page + 1 次 getCount(并发),无重复。
|
||||
- `index.vue`:`query()`(加载分类)从顶层同步调用改为 `onMounted(query)`,分类数据延迟到列表首屏渲染后异步加载,不阻塞首屏;去掉 `query()` 内无用的 `loading.value = true` 写入。
|
||||
- 注:`loading` ref 仍保留(handleExport 用它做防重入,非死变量,上轮"loading 死变量"判断有误)。
|
||||
|
||||
### 验证
|
||||
- 已跑 `vite build` 后台验证可编译(待结果)。
|
||||
|
||||
## 后端优化已执行(2026-08-05,用户确认"现在改")
|
||||
改 mp-java 共 6 处:
|
||||
1. `ShopGoodsMapper.xml`:新增 `selectCountSummary` —— `CASE WHEN` 聚合一条 SQL 返回 totalNum(出售中=status=0且stock>0)/totalNum2(待上架=status>0)/totalNum3(已售罄=stock=0),替代原 3 次 count;含 `param.merchantId` 可选条件,多租户插件自动加 tenant_id。
|
||||
2. `ShopGoodsMapper.java`:加 `selectCountSummary` + `downSoldOutGoods`(`@InterceptorIgnore(tenantLine=true)` 跨租户 `UPDATE shop_goods SET status=1 WHERE stock=0 AND status!=1`,加 `status!=1` 避免重复写)。
|
||||
3. `ShopGoodsService.java`/`Impl`:`getCountSummary(param)`(聚合统计,Map→int 安全转换)+ `downSoldOutGoods()`。
|
||||
4. `ShopGoodsController.data()`:改为调 `getCountSummary(param)`,删除 3 次 count 与 update 写操作(data 接口不再写库)。
|
||||
5. 新建 `shop/task/GoodsSoldOutTask.java`:`@Component` + `@Scheduled(cron="${shop.goods.sold-out.cron:0 */5 * * * ?}")`,每 5 分钟全局下架已售罄商品(仿 CouponExpireTask 写法,@EnableScheduling 已在 WebSoftApplication 启用)。
|
||||
- 编译验证:IntelliJ 内置 Maven `compile` 通过(target/classes 已生成 GoodsSoldOutTask/ServiceImpl/Mapper.class)。
|
||||
- **语义变化(需用户知晓)**:
|
||||
- 下架时机:原"每次打开商品列表才下架当前租户" → 现"定时任务每 5 分钟全局下架所有租户"。5 分钟窗口内新售罄商品会短暂留在"出售中"列表(on_sale 只查 status=0),直至定时任务执行。
|
||||
- 范围:原依赖请求线程租户上下文(只下当前租户)→ 现 `@InterceptorIgnore` 跨租户(所有商户已售罄都下架),更彻底一致。
|
||||
- 频率可调:`shop.goods.sold-out.cron` 配置项。
|
||||
- 若要求"出售中"列表立即可不显示 stock=0,可给 on_sale SQL 加 `AND a.stock > 0`(未做,超出剥离范围,待用户决定)。
|
||||
- 注:Controller 改造后 `LambdaQueryWrapper`/`LambdaUpdateWrapper` import 可能 unused,但 Java 编译允许 unused import 且其他接口可能仍用,未清理(编译已通过)。
|
||||
|
||||
## ⚠️ 重要修正:guilixu-java 才是本次后端优化的目标端(2026-08-05 用户指出"改错路径")
|
||||
- 用户明确:后端改动**应该**落在 `/Users/gxwebsoft/JAVA/guilixu-java`,且 **mp-java 也保留**(两端都要有)。
|
||||
- 原因澄清:shopGoods 的 `ShopGoodsController.data()` / Service / Mapper / task 在 **mp-java 与 guilixu-java 都存在且结构一致(同 `com.gxwebsoft.shop` 包)**,所以"共享 shop 业务模块"的改动要**两端同步**。
|
||||
- 已对 guilixu-java 等价移植上述 6 处改动(selectCountSummary SQL、selectCountSummary/downSoldOutGoods 方法、getCountSummary/downSoldOutGoods 方法体、data() 剥离写、新建 GoodsSoldOutTask),与 mp-java 完全一致。
|
||||
- 验证:IntelliJ 内置 Maven `compile` 通过(target/classes 已生成 GoodsSoldOutTask/ServiceImpl/Mapper/Controller.class)。
|
||||
- 教训:之前 MEMORY.md 说"guilixu-java 的 cms 是死代码、前端不调 guilixu-java"是**针对 cms 模块**的;**不能推广到所有 shop 模块**——shopGoods 等共享业务在两端都有活代码,改动需两端同步。
|
||||
@@ -0,0 +1,10 @@
|
||||
# 2026-08-06 工作日志
|
||||
|
||||
## 运费模板「不发货地区」需求分析
|
||||
- 客户需求:运费模板要支持"排除地区/不发货地区"(黑名单模式),而非仅"指定配送"(白名单)。
|
||||
- 现状问题:当前 `ShopExpressTemplateDetail` 仅 `provinceId`+`cityId` 单值,级联选择器只能选 1 个省市,无排除逻辑。截图里的双栏 Transfer modal 前端代码库里不存在,需从零开发。
|
||||
- 产出接口方案文档:`docs/运费模板-不发货地区-接口方案.md`,含表字段变更(region_mode/region_ids)、下单校验逻辑、6 个待后端确认问题。
|
||||
- 后端在 mp-java 的 shop 模块(前端走 MODULES_API_URL)。用户下一步:先与后端对齐接口方案,再做前端改造。
|
||||
- 地区 ID 为行政区划编码字符串(省110000/市110100/区110101),下单时可前缀匹配。
|
||||
- 存储方案确认:region_ids 采用**逗号分隔单字段**(VARCHAR(2000)),不新建关联表。理由:查询方向固定为"模板→明细"、排除地区少、改动最小可快速上线。已写入方案文档。
|
||||
- 用户确认"分两步"策略:第1步接口方案已产出(docs/运费模板-不发货地区-接口方案.md),待后端回复 6 个确认问题后做第2步前端改造。
|
||||
@@ -28,10 +28,15 @@
|
||||
- **mp-java**(`/Users/gxwebsoft/JAVA/mp-java`)= 独立的「modules 服务」,用 `modules` 库;cms(235类)、shop 等业务模块都在它这里,持续高频开发。
|
||||
- **guilixu-java**(`/Users/gxwebsoft/JAVA/guilixu-java`)= 桂礼序主后端,用 `db_guilixu` 库;是 mp-java 的「子集分叉」。
|
||||
- **guilixu-admin 前端已预设双基地址**:`SERVER_API_URL`(主后端) + `MODULES_API_URL`(localStorage ApiUrl / VITE_API_URL)。shop 商户/优惠券、cms 全部接口**都已走 MODULES_API_URL**,即前端本就假定 cms/shop 由 mp-java(modules 服务)提供。
|
||||
- **结论:guilixu-java 里的 cms 包(152类)是 mp-java cms 的旧分叉子集、且前端并不调它(前端调 modules 服务),属重复/死代码,应删除而非复制或扩展。**
|
||||
- **结论(仅限 cms 模块):guilixu-java 里的 cms 包(152类)是 mp-java cms 的旧分叉子集、且前端并不调它(前端调 modules 服务),属重复/死代码,应删除而非复制或扩展。**
|
||||
- 复制「整个 cms 到 guilixu-java」不可行:mp-java 完整 cms 的 CmsApp/CmsWebsiteService 依赖 `project` 模块(guilixu-java 没有),会编译失败。
|
||||
- 注意:mp-java 与 guilixu-java 是独立 git 仓库(`git.websoft.top/gxwebsoft/mp-java.git` 与 `guilixu-java.git`),各自独立部署、独立库。
|
||||
|
||||
### ⚠️ 双后端改动同步规则(2026-08-05 修正)
|
||||
- **cms 模块**改动:仅 mp-java(guilixu-java 的 cms 是死代码)。
|
||||
- **shop 等共享业务模块**(如 `ShopGoodsController` / Service / Mapper / task):**mp-java 与 guilixu-java 都有活代码且结构一致(同 `com.gxwebsoft.shop` 包)**,用户确认改动要**两端同步**(例:2026-08-05 shopGoods `/data` 统计优化,两端各改 6 处)。
|
||||
- 改动前先 grep 两端确认文件是否存在,避免只改一处导致另一端行为不一致。
|
||||
|
||||
### shopGoods status 字段(上架/下架)
|
||||
- `0` = 已上架 / 上架
|
||||
- `1` = 待上架 / 下架
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# 运费模板「不发货地区(排除地区)」接口方案
|
||||
|
||||
> 目标:支持商家配置"除部分偏远地区外,全国均可配送"的场景,避免逐个添加可配送省份。
|
||||
> 前端代码库:guilixu-admin(shop 模块接口走 MODULES_API_URL,即 mp-java 的 shop 模块)
|
||||
|
||||
---
|
||||
|
||||
## 一、现状(改造前)
|
||||
|
||||
- 一条 `shop_express_template_detail` = 一种配送区域 + 一套运费规则。
|
||||
- 当前**只能选 1 个省市组合**,字段为 `province_id` + `city_id`(单值)。
|
||||
- 不选 = 默认全国。
|
||||
- **无任何排除/不发货逻辑**。
|
||||
|
||||
## 二、需求(改造后)
|
||||
|
||||
配送区域支持两种模式:
|
||||
|
||||
| 模式 | 含义 | 典型场景 |
|
||||
|------|------|----------|
|
||||
| 白名单(指定配送) | 只发列表中的地区 | 仅发江浙沪 |
|
||||
| 黑名单(指定不发货) | 发列表外的所有地区 | 除新疆/西藏/港澳台外全国发 |
|
||||
|
||||
新增「不发货地区」配置:商家框选不发货的省/市,保存后这些地区下单时报"暂不配送"。
|
||||
|
||||
## 三、数据库变更
|
||||
|
||||
`shop_express_template_detail` 表新增两个字段(存储方案选型见下方说明):
|
||||
|
||||
> **存储方案选型:推荐逗号分隔单字段,不新建关联表。**
|
||||
> 理由:① 本业务查询模式是「模板→明细」,非「地区→模板」,无需按地区反查;② 排除地区通常仅几个省,VARCHAR(2000) 足够;③ 改动最小、前端直接透传编码字符串、可快速上线满足客户急迫配置需求。若未来需「按地区统计运费模板」等报表,再迁关联表。
|
||||
|
||||
```sql
|
||||
ALTER TABLE shop_express_template_detail
|
||||
ADD COLUMN region_mode TINYINT NOT NULL DEFAULT 0 COMMENT '配送区域模式: 0=全国, 1=指定配送(白名单), 2=指定不发货(黑名单)',
|
||||
ADD COLUMN region_ids VARCHAR(2000) DEFAULT NULL COMMENT '地区编码列表(与regions-data.json一致),逗号分隔,如 110000,120000。region_mode=1或2时有效';
|
||||
```
|
||||
|
||||
- `region_mode=0`(全国):`region_ids` 为空,兼容旧"不选=全国"行为。
|
||||
- `region_mode=1/2`:`region_ids` 存选中的地区编码(省/市/区 6 位编码,与前端 `regions-data.json` 的 `value` 一致)。
|
||||
|
||||
### 旧数据兼容
|
||||
|
||||
历史数据 `province_id` / `city_id` 保留不删。迁移建议:
|
||||
- 旧数据 `region_mode` 默认置 `1`,`region_ids` = `province_id[,city_id]`(有 city 才拼)。
|
||||
- 前端编辑时若检测到旧字段(无 `region_mode`),按 `province_id/city_id` 回显到白名单模式。
|
||||
|
||||
## 四、region_ids 存储与匹配规则
|
||||
|
||||
- 存**最小选中粒度**编码:用户选到省只存省码,选到市存市码(省码可推导,不必冗余存)。
|
||||
- 编码前缀关系:区码 `110101` ⊃ 市码 `110100` ⊃ 省码 `110000`。
|
||||
- 匹配算法(下单时使用):
|
||||
- 收货地址解析出 省码 / 市码 / 区码。
|
||||
- 命中判断:`region_ids` 中包含收货地的 省码 **或** 市码 **或** 区码 即视为命中该条明细。
|
||||
- 推荐:前端提交时统一存**省+市两级**(不存区),简化逻辑;如需区县级排除再加。
|
||||
|
||||
## 五、下单配送校验逻辑(后端 + 小程序端双重校验)
|
||||
|
||||
```
|
||||
收货地址 → 解析 {provinceCode, cityCode, districtCode}
|
||||
|
||||
1. 查该商品运费模板下所有 detail 明细
|
||||
2. 遍历所有 region_mode=2(黑名单) 明细:
|
||||
若 收货地省/市/区码 任一 ∈ 该明细.region_ids
|
||||
→ 直接判定【不配送】,返回 "该地区暂不配送"
|
||||
3. 若无黑名单命中,按现有逻辑选运费明细:
|
||||
- region_mode=0(全国) 明细 → 匹配
|
||||
- region_mode=1(白名单) 明细 → 收货地码 ∈ region_ids 才匹配
|
||||
4. 无任何明细匹配 → 不配送(或走默认模板,按业务定)
|
||||
```
|
||||
|
||||
> 优先级:**黑名单排除 > 白名单/全国匹配**。即一旦落入不发货列表,直接不可下单。
|
||||
|
||||
## 六、接口变更
|
||||
|
||||
涉及接口(mp-java `ShopExpressTemplateDetailController`):
|
||||
|
||||
| 接口 | METHOD & PATH | 变更 |
|
||||
|------|---------------|------|
|
||||
| 新增明细 | POST `/shop/shop-express-template-detail` | 入参加 `regionMode`、`regionIds` |
|
||||
| 修改明细 | PUT `/shop/shop-express-template-detail` | 入参加 `regionMode`、`regionIds` |
|
||||
| 明细详情 | GET `/shop/shop-express-template-detail/{id}` | 出参加 `regionMode`、`regionIds` |
|
||||
| 明细列表 | GET `/shop/shop-express-template-detail/page` | 出参加 `regionMode`、`regionIds` |
|
||||
|
||||
入参示例(新增):
|
||||
```json
|
||||
{
|
||||
"templateId": 12,
|
||||
"type": true,
|
||||
"regionMode": 2,
|
||||
"regionIds": "650000,540000,810000,820000,710000",
|
||||
"firstNum": "1",
|
||||
"firstAmount": "0",
|
||||
"extraNum": "1",
|
||||
"extraAmount": "0",
|
||||
"status": 0,
|
||||
"sortNumber": 100
|
||||
}
|
||||
```
|
||||
|
||||
## 七、需后端确认的问题 —— 已确认(2026-08-06 后端已实现)
|
||||
|
||||
> 落地仓库:**guilixu-java**(不是 mp-java;两边 shop 模块原本字节一致,本次只改 guilixu-java)。
|
||||
|
||||
| # | 问题 | 结论 |
|
||||
|---|------|------|
|
||||
| 1 | 编码 vs 名称 | **存编码**。6 位行政区划码,与 `regions-data.json` 的 `value` 一致。 |
|
||||
| 2 | 粒度 | **省/市/区三级都支持**。前端建议只提交省+市两级;后端匹配时省/市/区任一命中即算命中。 |
|
||||
| 3 | 黑白名单优先级 | **确认:黑名单命中即不可下单**,优先级高于白名单/全国。允许同一模板下多种模式共存。 |
|
||||
| 4 | 旧数据迁移 | **由后端 SQL 一次性迁移**,见 `sql/shop_express_template_detail_region.sql`。经核查现网 `province_id/city_id` 存的就是 6 位区划码(如 150000/150200),可直接拼接,无需映射表。 |
|
||||
| 5 | 下单校验落点 | **后端统一做**,已接入 `OrderBusinessService.createOrder()` 第 3.2 步,防绕过。小程序端可另做前置提示。 |
|
||||
| 6 | 多模板 | **按租户维度跨模板取并集**。因 `shop_goods` 目前未与运费模板建立关联字段,按"本店除偏远地区外全国发货"的语义处理;后续商品绑定模板后可加 `templateId` 过滤。 |
|
||||
|
||||
### 后端实现要点(前端需知)
|
||||
|
||||
- **新增字段**:`regionMode`(Integer) + `regionIds`(String),已贯通 page / list / get / save / update 全部接口,直接传即可。
|
||||
- **`regionIds` 格式**:逗号分隔的 6 位编码字符串,如 `"650000,540000,810000,820000,710000"`。
|
||||
- **`regionMode` 取值**:`0`=全国、`1`=指定配送(白名单)、`2`=指定不发货(黑名单)。**默认 0**。
|
||||
- **列表筛选**:`regionMode` 支持精确筛选;`regionIds` 支持模糊包含筛选。
|
||||
- **旧数据回显**:迁移 SQL 已把历史行刷成 `regionMode=1` + `regionIds`,前端**无需**再兼容 `province_id/city_id` 回显逻辑。
|
||||
- **地址名称→编码**:后端 `RegionCodeResolver` 负责把 `shop_user_address` 里的省/市/区**名称**翻译成编码(该表只存名称不存编码)。已处理 `新疆维吾尔自治区`↔`新疆`、`北京市`↔`北京` 等后缀归一化。
|
||||
- **拦截文案**:命中时后端抛 `BusinessException`,接口返回形如 `"新疆维吾尔自治区暂不配送,请更换收货地址"`,前端直接展示 message 即可。
|
||||
- **放行兜底**:地址解析不出行政区划编码时(境外地址等)**放行不拦截**,避免误伤。
|
||||
|
||||
> ⚠️ 上线顺序:必须**先执行** `sql/shop_express_template_detail_region.sql`,再发布后端,否则查询会报未知列。
|
||||
|
||||
---
|
||||
|
||||
*前端侧对应改造:新建 `RegionsTransfer` 双栏选择组件 + 改造 `shopExpressTemplateDetailEdit.vue` 增加模式切换 + 列表回显。*
|
||||
+21
-3
@@ -76,18 +76,34 @@ function removeOssProcess(url: string): string {
|
||||
* @param options 压缩选项(width/quality 不传则使用默认值)
|
||||
* @returns 处理后的图片 URL
|
||||
*/
|
||||
// 缓存已计算的压缩 URL,避免列表重渲染时(勾选、hover、滚动)重复拼接字符串
|
||||
const compressedUrlCache = new Map<string, string>();
|
||||
|
||||
export function getCompressedImageUrl(
|
||||
url: string,
|
||||
options?: ImageCompressOptions
|
||||
): string {
|
||||
if (!url) return '';
|
||||
|
||||
// 命中缓存直接返回,省去重复计算
|
||||
const cacheKey = options ? `${url}|${JSON.stringify(options)}` : url;
|
||||
const cached = compressedUrlCache.get(cacheKey);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
// 先补全为完整 URL
|
||||
const fullUrl = ensureFullUrl(url);
|
||||
if (!fullUrl) return '';
|
||||
if (!fullUrl) {
|
||||
compressedUrlCache.set(cacheKey, '');
|
||||
return '';
|
||||
}
|
||||
|
||||
const { width, quality, enabled } = { ...DEFAULT_OPTIONS, ...options };
|
||||
|
||||
// 未启用压缩,原样返回
|
||||
if (!enabled) return fullUrl;
|
||||
if (!enabled) {
|
||||
compressedUrlCache.set(cacheKey, fullUrl);
|
||||
return fullUrl;
|
||||
}
|
||||
|
||||
// 优先使用调用方指定的 width/quality:
|
||||
// 先剥离 URL 自带的 x-oss-process,再按本函数参数重新拼接,确保以调用方为准。
|
||||
@@ -96,5 +112,7 @@ export function getCompressedImageUrl(
|
||||
// 已有其他 query 参数用 & 拼接,否则用 ?
|
||||
const separator = baseUrl.includes('?') ? '&' : '?';
|
||||
|
||||
return `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
|
||||
const result = `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
|
||||
compressedUrlCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
ArrowDownOutlined,
|
||||
DownOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { getCount } from '@/api/shop/shopGoods';
|
||||
import type { GoodsCount, ShopGoodsParam } from '@/api/shop/shopGoods/model';
|
||||
import useSearch from '@/utils/use-search';
|
||||
@@ -244,18 +244,20 @@
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
// 首次进入:设置商户条件并触发统计 + 列表加载。
|
||||
// 用 onMounted 替代 watch 的 immediate,避免与 ele-pro-table 的自动加载(initLoad)重复发请求。
|
||||
onMounted(() => {
|
||||
const id = props.merchantId;
|
||||
where.merchantId = Number(id) > 0 ? id : undefined;
|
||||
reload();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.merchantId,
|
||||
(id) => {
|
||||
if (Number(id) > 0) {
|
||||
where.merchantId = id;
|
||||
reload();
|
||||
} else {
|
||||
where.merchantId = undefined;
|
||||
where.merchantId = Number(id) > 0 ? id : undefined;
|
||||
reload();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
:init-load="false"
|
||||
:scroll="{ x: 1800 }"
|
||||
size="small"
|
||||
v-model:selection="selection"
|
||||
@@ -36,12 +37,14 @@
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<a-space class="flex items-center cursor-pointer">
|
||||
<a-image
|
||||
:src="getCompressedImageUrl(record.image)"
|
||||
<img
|
||||
v-if="record.image"
|
||||
:preview="false"
|
||||
:width="50"
|
||||
:height="50"
|
||||
:src="getCompressedImageUrl(record.image)"
|
||||
class="goods-thumb"
|
||||
loading="lazy"
|
||||
alt=""
|
||||
width="50"
|
||||
height="50"
|
||||
/>
|
||||
<div class="flex flex-col justify-center leading-tight">
|
||||
<span class="text-gray-700 font-bold">{{ record.name }}</span>
|
||||
@@ -154,7 +157,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { createVNode, ref, onMounted } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
@@ -511,9 +514,8 @@
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
/* 查询:分类数据延迟到列表首屏渲染完成后再加载,不阻塞首屏 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
// 加载分类数据
|
||||
if (!navigationList.value) {
|
||||
listShopGoodsCategory({}).then((res) => {
|
||||
@@ -623,7 +625,7 @@
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
onMounted(query);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -632,4 +634,14 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
.goods-thumb {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user