feat(shopGoods): 支持多规格价格区间及修复快递配送设置保存问题
- 商品列表价格列新增多规格价格区间显示,单规格依旧显示单值 - 后端新增非持久化最大价字段 priceMax/salePriceMax/dealerPriceMax,批量读取SKU价格区间 - 前端 API 和列表价格列渲染及导出支持价格区间显示 - 添加快递配送相关字段 canExpress 和 expressTemplateId 至后端实体并新增对应数据库字段 - 调整前端运费模板表单项位置及必填校验逻辑,修复保存不生效问题 - 修改 SKU 图片删除方法,避免误删主商品图片数组
This commit is contained in:
@@ -64,3 +64,24 @@
|
|||||||
- 文件:`src/views/shop/shopGoods/components/shopGoodsEdit.vue`
|
- 文件:`src/views/shop/shopGoods/components/shopGoodsEdit.vue`
|
||||||
- 问题:`onDeleteSkuItem(index)` 误删 `images.value`(主商品图片数组),而非 `skuList.value[index].images`(对应SKU行图片)→ 点击 × 无反应
|
- 问题:`onDeleteSkuItem(index)` 误删 `images.value`(主商品图片数组),而非 `skuList.value[index].images`(对应SKU行图片)→ 点击 × 无反应
|
||||||
- 修复:改为清空 `skuList.value[index].images = []` + `image = ''`
|
- 修复:改为清空 `skuList.value[index].images = []` + `image = ''`
|
||||||
|
|
||||||
|
## 商品列表「价格」列支持多规格价格区间(方案A)
|
||||||
|
- 需求:多规格商品各 SKU 价格不同,原列表价格列只显示主表 `price/salePrice/dealerPrice`(占位值),与 SKU 实际价格对不上。确认采用方案A:**多规格显示 ¥最低价 ~ ¥最高价,单规格保持单值**。
|
||||||
|
- 后端(**guilixu-java**,生效后端):
|
||||||
|
1. `entity/ShopGoods.java`:加 3 个**非持久化**字段 `priceMax / salePriceMax / dealerPriceMax`(BigDecimal,`@TableField(exist = false)`)。
|
||||||
|
2. `service/impl/ShopGoodsServiceImpl.java`:`pageRel`/`listRel` 查完后调 `fillSkuPriceRange(list)`——收集 `specs==1` 的商品,`shopGoodsSkuService.lambdaQuery().in(goodsId, ids)` 一次批量查 SKU,按 `price/salePrice/buyingPrice` 算 min/max 回填(主表 `price=min`、`priceMax=max`;市场价/会员价同理)。只填内存对象、不落库。
|
||||||
|
- 前端(**guilixu-admin**):
|
||||||
|
1. `api/shop/shopGoods/model/index.ts`:ShopGoods 加 `priceMax? / salePriceMax? / dealerPriceMax?`。
|
||||||
|
2. `views/shop/shopGoods/index.vue`:priceGroup 列渲染改为「区间不相等时 ¥min ~ ¥max,否则单值」;导出 Excel 新增 `fmtPrice()` 同步处理。
|
||||||
|
- 验证:`guilixu-java` `mvn -o compile` 通过(本机无全局 mvn,临时用 /tmp/apache-maven-3.9.6 离线编译);`guilixu-admin` `vite build` 通过。
|
||||||
|
- 待确认:**mp-java 是否需要同步**——本部署 admin 商城走 guilixu-java,mp-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.sql`:`ALTER 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 已早先存在,逻辑无需改。
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ export interface ShopGoods {
|
|||||||
memberMarketPrice?: string;
|
memberMarketPrice?: string;
|
||||||
// 经销商价格
|
// 经销商价格
|
||||||
dealerPrice?: string;
|
dealerPrice?: string;
|
||||||
|
// 价格区间最大值(多规格列表展示用,由后端按 SKU 计算;单规格/区间相等时为 undefined)
|
||||||
|
priceMax?: string;
|
||||||
|
salePriceMax?: string;
|
||||||
|
dealerPriceMax?: string;
|
||||||
// 有赠品
|
// 有赠品
|
||||||
buyingGift?: boolean;
|
buyingGift?: boolean;
|
||||||
// 有赠品
|
// 有赠品
|
||||||
|
|||||||
@@ -106,20 +106,6 @@
|
|||||||
v-model:value="form.buyingPrice"
|
v-model:value="form.buyingPrice"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<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 ? '请选择运费模板(必选)' : '请选择运费模板'"
|
|
||||||
>
|
|
||||||
<a-select-option
|
|
||||||
v-for="(item, index) in expressTemplateList"
|
|
||||||
:key="index"
|
|
||||||
:value="item.id"
|
|
||||||
>{{ item.title }}
|
|
||||||
</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="商品图片(400x400)" name="image">
|
<a-form-item label="商品图片(400x400)" name="image">
|
||||||
<!-- <SelectFile-->
|
<!-- <SelectFile-->
|
||||||
<!-- :placeholder="`请选择视频文件`"-->
|
<!-- :placeholder="`请选择视频文件`"-->
|
||||||
@@ -356,7 +342,7 @@
|
|||||||
:data="record.images || []"
|
:data="record.images || []"
|
||||||
:index="index"
|
:index="index"
|
||||||
@done="chooseSkuImage"
|
@done="chooseSkuImage"
|
||||||
@del="onDeleteSkuItem"
|
@del="() => onDeleteSkuItem(index)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'price'">
|
<template v-if="column.key === 'price'">
|
||||||
@@ -458,6 +444,20 @@
|
|||||||
:un-checked-value="0"
|
:un-checked-value="0"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<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 ? '请选择运费模板(必选)' : '请选择运费模板'"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="(item, index) in expressTemplateList"
|
||||||
|
:key="index"
|
||||||
|
:value="item.id"
|
||||||
|
>{{ item.title }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label="是否新品">
|
<a-form-item label="是否新品">
|
||||||
<a-switch
|
<a-switch
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -58,16 +58,36 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'priceGroup'">
|
<template v-if="column.key === 'priceGroup'">
|
||||||
<div class="flex flex-col leading-tight">
|
<div class="flex flex-col leading-tight">
|
||||||
<span class="text-gray-800 font-bold">¥{{ record.price }}</span>
|
<span class="text-gray-800 font-bold"
|
||||||
<span class="text-gray-400 text-xs"
|
>¥{{ record.price
|
||||||
>市场价:{{
|
}}<span
|
||||||
record.salePrice ? `¥${record.salePrice}` : '-'
|
v-if="record.priceMax != null && record.priceMax !== record.price"
|
||||||
}}</span
|
> ~ ¥{{ record.priceMax }}</span
|
||||||
|
></span
|
||||||
>
|
>
|
||||||
<span class="text-gray-400 text-xs"
|
<span class="text-gray-400 text-xs"
|
||||||
>会员价:{{
|
>市场价:<template v-if="record.salePrice"
|
||||||
record.dealerPrice ? `¥${record.dealerPrice}` : '-'
|
>¥{{ record.salePrice
|
||||||
}}</span
|
}}<span
|
||||||
|
v-if="
|
||||||
|
record.salePriceMax != null &&
|
||||||
|
record.salePriceMax !== record.salePrice
|
||||||
|
"
|
||||||
|
> ~ ¥{{ record.salePriceMax }}</span
|
||||||
|
></template
|
||||||
|
><template v-else>-</template></span
|
||||||
|
>
|
||||||
|
<span class="text-gray-400 text-xs"
|
||||||
|
>会员价:<template v-if="record.dealerPrice"
|
||||||
|
>¥{{ record.dealerPrice
|
||||||
|
}}<span
|
||||||
|
v-if="
|
||||||
|
record.dealerPriceMax != null &&
|
||||||
|
record.dealerPriceMax !== record.dealerPrice
|
||||||
|
"
|
||||||
|
> ~ ¥{{ record.dealerPriceMax }}</span
|
||||||
|
></template
|
||||||
|
><template v-else>-</template></span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -389,6 +409,17 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* 价格区间文本:多规格且区间不相等时显示 ¥min ~ ¥max,否则显示单值 */
|
||||||
|
const fmtPrice = (min?: string | number, max?: string | number) => {
|
||||||
|
if (min == null || min === '') {
|
||||||
|
return max != null && max !== '' ? `¥${max}` : '';
|
||||||
|
}
|
||||||
|
const base = `¥${min}`;
|
||||||
|
return max != null && max !== '' && `${max}` !== `${min}`
|
||||||
|
? `${base} ~ ¥${max}`
|
||||||
|
: base;
|
||||||
|
};
|
||||||
|
|
||||||
/* 导出商品列表 */
|
/* 导出商品列表 */
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
@@ -424,9 +455,9 @@
|
|||||||
array.push([
|
array.push([
|
||||||
`${goods.goodsId || ''}`,
|
`${goods.goodsId || ''}`,
|
||||||
`${goods.name || ''}`,
|
`${goods.name || ''}`,
|
||||||
`¥${goods.price || 0}`,
|
fmtPrice(goods.price, goods.priceMax),
|
||||||
`${goods.salePrice ? `¥${goods.salePrice}` : ''}`,
|
fmtPrice(goods.salePrice, goods.salePriceMax),
|
||||||
`${goods.dealerPrice ? `¥${goods.dealerPrice}` : ''}`,
|
fmtPrice(goods.dealerPrice, goods.dealerPriceMax),
|
||||||
`${goods.sales || 0}`,
|
`${goods.sales || 0}`,
|
||||||
`${goods.stock || 0}`,
|
`${goods.stock || 0}`,
|
||||||
statusMap[goods.status || 0] || '',
|
statusMap[goods.status || 0] || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user