feat(buyer): 添加电子卡券(E_COUPON)支持

- 新增 E_COUPON 商品类型,支持电子卡券的展示与购买逻辑
- 优化商品详情页,调整促销、库存和购买按钮逻辑以适应电子卡券
- 增加卡密信息展示与管理功能,支持商家端卡池管理
- 更新相关页面以处理电子卡券的特殊逻辑,如免地址、隐藏优惠券等
This commit is contained in:
田香琪
2026-07-31 13:44:56 +08:00
parent 36878e6f7f
commit 1ae7ab3036
19 changed files with 1269 additions and 85 deletions

View File

@@ -53,6 +53,8 @@
>
<el-option label="实物商品" value="PHYSICAL_GOODS" />
<el-option label="虚拟商品" value="VIRTUAL_GOODS" />
<!-- E_COUPON卡密商品可跳转卡池管理 -->
<el-option label="电子卡券" value="E_COUPON" />
</el-select>
</el-form-item>
<el-form-item>
@@ -144,7 +146,7 @@
</template>
</el-table-column>
<el-table-column prop="storeName" label="店铺名称" width="200" show-overflow-tooltip />
<el-table-column label="操作" width="200" align="center" fixed="right">
<el-table-column label="操作" width="260" align="center" fixed="right">
<template #default="{ row }">
<template v-if="row.marketEnable === 'DOWN'">
<a class="link-text" @click="upper(row)">上架</a>
@@ -156,6 +158,11 @@
<span class="op-split">|</span>
<a class="link-text" @click="editGoods(row)">编辑</a>
</template>
<!-- E_COUPON 专属卡池管理入口原型 P-03 -->
<template v-if="row.goodsType === 'E_COUPON'">
<span class="op-split">|</span>
<a class="link-text" @click="goCardKeyPool(row)">卡池管理</a>
</template>
</template>
</el-table-column>
</el-table>
@@ -192,6 +199,7 @@
import {
getGoodsListData,
getGoodsNumerData,
getQueryGoodsIdGoodsList,
upGoods,
lowGoods,
} from "@/api/goods";
@@ -248,7 +256,34 @@ export default {
goodsTypeText(v) {
if (v === "PHYSICAL_GOODS") return "实物商品";
if (v === "VIRTUAL_GOODS") return "虚拟商品";
return "电子卡券";
if (v === "E_COUPON") return "电子卡券"; // 卡密商品
return v || "—";
},
/** 跳转卡池管理;单 SKU 商品默认取第一个 SKUcard-key-pool 需 skuId */
async goCardKeyPool(row) {
let skuId = row.skuId;
if (!skuId) {
try {
const res = await getQueryGoodsIdGoodsList(row.id);
if (res.success && res.result?.length) {
skuId = res.result[0].id;
} else {
this.$message.warning("该商品暂无 SKU请先完善商品规格");
return;
}
} catch {
this.$message.error("获取商品规格失败");
return;
}
}
this.$router.push({
path: "/card-key-pool",
query: {
skuId,
goodsId: row.id,
goodsName: row.goodsName,
},
});
},
marketEnableText(v) {
if (v === "DOWN") return "下架";

View File

@@ -84,6 +84,7 @@ export default {
data() {
return {
selectGoodsType: false, // 展示选择商品分类modal
/** 商品类型选项E_COUPON 为卡密商品(与 VIRTUAL_GOODS 核销型区分,见 FR-S-01 */
goodsTypeWay: [
{
title: "实物商品",
@@ -99,6 +100,13 @@ export default {
type: "VIRTUAL_GOODS",
check: false,
},
{
title: "电子卡券",
img: goodsType2Img,
desc: "卡密自动发卡,无需物流",
type: "E_COUPON", // goodsType库存由卡池同步非手动填写
check: false,
},
],
// 商品分类选择数组
category: [
@@ -230,4 +238,12 @@ export default {
gap: 12px;
width: 100%;
}
.content-goods-publish {
.goods-category li.activeClass {
background-color: #409eff;
border-color: #409eff;
color: #fff;
}
}
</style>

View File

@@ -44,7 +44,7 @@
</el-form-item>
<el-form-item class="form-item-view-el" label="销售模式" prop="salesModel">
<el-radio-group
v-if="baseInfoForm.goodsType != 'VIRTUAL_GOODS'"
v-if="!isVirtualLikeGoods"
v-model="baseInfoForm.salesModel"
@change="handleSalesModeChange"
>
@@ -52,7 +52,7 @@
<el-radio-button value="WHOLESALE">批发型</el-radio-button>
</el-radio-group>
<el-radio-group v-else v-model="baseInfoForm.salesModel">
<el-radio-button value="RETAIL">虚拟型</el-radio-button>
<el-radio-button value="RETAIL">{{ isECouponGoods ? "电子卡券" : "虚拟型" }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="baseInfoForm.salesModel == 'WHOLESALE'" class="form-item-view-el" label="销售规则"
@@ -336,7 +336,7 @@
@change="updateSkuTable(row, 'sn', $index)"
/>
<el-input
v-else-if="col.slot === 'weight' && baseInfoForm.goodsType !== 'VIRTUAL_GOODS'"
v-else-if="col.slot === 'weight' && needsLogistics && baseInfoForm.salesModel !== 'WHOLESALE'"
v-model="row.weight"
clearable
placeholder="请输入重量"
@@ -344,6 +344,14 @@
>
<template #append>kg</template>
</el-input>
<el-input
v-else-if="col.slot === 'quantity' && isECouponGoods"
:model-value="resolvePoolStockDisplay(row)"
disabled
placeholder="卡池可用数"
>
<template #append>{{ baseInfoForm.goodsUnit || "" }}</template>
</el-input>
<el-input
v-else-if="col.slot === 'quantity'"
v-model="row.quantity"
@@ -444,7 +452,7 @@
</div>
</el-form-item>
</div>
<div v-if="baseInfoForm.goodsType != 'VIRTUAL_GOODS'">
<div v-if="needsLogistics">
<h4>商品物流信息</h4>
<div class="form-item-view">
<el-form-item class="form-item-view-el" label="物流模板" prop="templateId">
@@ -513,7 +521,7 @@
@selected="(list) => { selectedImage = list }"
/>
<template #footer>
<el-button @click="picModelFlag = false">取消</el-button>
<el-button @click="picModelFlag = false; selectedImage = []">取消</el-button>
<el-button type="primary" @click="confirmUrls">确定</el-button>
</template>
</el-dialog>
@@ -552,6 +560,22 @@ export default {
type: Object,
},
},
computed: {
/** 卡密商品:库存只读展示 poolStock提交时 quantity 传 0FR-S-01 / P-02 */
isECouponGoods() {
return this.baseInfoForm.goodsType === "E_COUPON";
},
isVirtualGoods() {
return this.baseInfoForm.goodsType === "VIRTUAL_GOODS";
},
isVirtualLikeGoods() {
return this.isVirtualGoods || this.isECouponGoods;
},
/** 仅实物需要运费模板E_COUPON 强制 templateId=0§6.1 */
needsLogistics() {
return this.baseInfoForm.goodsType === "PHYSICAL_GOODS";
},
},
data() {
// 表单验证项,商品价格
const checkPrice = (rule, value, callback) => {
@@ -759,11 +783,32 @@ export default {
}
},
methods: {
/** E_COUPON 库存列只读展示,无卡密时显示 0 */
resolvePoolStockDisplay(row) {
if (row.poolStock != null && row.poolStock !== "") {
return row.poolStock;
}
if (row.quantity != null && row.quantity !== "") {
return row.quantity;
}
return 0;
},
/** E_COUPON 提交固定传 0真实库存由卡池同步 */
resolveSubmitQuantity(sku) {
if (this.isECouponGoods) {
return 0;
}
return sku.quantity;
},
defaultSkuQuantity() {
return this.isECouponGoods ? 0 : "";
},
draggableItemKey(item) {
return item;
},
// 选择图片modal
handleCLickImg(val, index) {
this.selectedImage = [];
this.picModelFlag = true;
this.selectedFormBtnName = val;
this.$nextTick(() => {
@@ -793,22 +838,60 @@ export default {
desc: "视频大小不能超过10MB",
});
},
parseOssSelectionUrl(item) {
if (!item) return "";
if (typeof item === "string") {
const index = item.indexOf(",");
return index >= 0 ? item.slice(index + 1) : item;
}
return item.url || "";
},
// 图片选择后回调
callbackSelected(val) {
this.picModelFlag = false;
if (val && this.selectedFormBtnName == 'selectedSkuImages') {
this.selectedSku.images.push(val);
if (!val?.url) {
return;
}
if (this.selectedFormBtnName === "selectedSkuImages") {
if (!this.selectedSku.images) {
this.selectedSku.images = [];
}
this.selectedSku.images.push(val.url);
} else {
this.baseInfoForm[this.selectedFormBtnName].push(val.url);
}
},
confirmUrls() {
if (this.selectedImage && this.selectedFormBtnName == 'selectedSkuImages') {
this.selectedSku.images = [...this.selectedSku.images, ...this.selectedImage];
} else {
this.baseInfoForm[this.selectedFormBtnName] = [...this.baseInfoForm[this.selectedFormBtnName], ...this.selectedImage];
const urls = (this.selectedImage || [])
.map((item) => this.parseOssSelectionUrl(item))
.filter(Boolean);
if (!urls.length) {
this.$Message.warning("请选择图片");
return;
}
if (this.selectedFormBtnName === "selectedSkuImages") {
if (!this.selectedSku.images) {
this.selectedSku.images = [];
}
urls.forEach((url) => {
if (this.selectedSku.images.length < 5) {
this.selectedSku.images.push(url);
}
});
} else if (this.selectedFormBtnName === "goodsGalleryFiles") {
urls.forEach((url) => {
if (this.baseInfoForm.goodsGalleryFiles.length < 5) {
this.baseInfoForm.goodsGalleryFiles.push(url);
}
});
} else if (this.selectedFormBtnName) {
const target = this.baseInfoForm[this.selectedFormBtnName];
if (Array.isArray(target)) {
urls.forEach((url) => target.push(url));
}
}
this.selectedImage = [];
this.picModelFlag = false;
},
// 局部刷新
refresh(v) {
@@ -1255,6 +1338,7 @@ export default {
price: e.price,
// cost: e.cost,
quantity: e.quantity,
poolStock: e.poolStock,
// alertQuantity: e.alertQuantity,
weight: e.weight,
};
@@ -1724,14 +1808,20 @@ export default {
...combination,
id: existingCombination.id || "",
sn: existingCombination.sn || "",
quantity: existingCombination.quantity || "",
quantity: this.isECouponGoods
? this.resolvePoolStockDisplay(existingCombination)
: (existingCombination.quantity || ""),
poolStock: existingCombination.poolStock,
cost: existingCombination.cost || "",
price: existingCombination.price || "",
weight: existingCombination.weight || ""
};
} else {
// 新组合使用默认值
return combination;
return {
...combination,
quantity: this.defaultSkuQuantity(),
};
}
});
this.baseInfoForm.regeneratorSkuFlag = true;
@@ -1779,14 +1869,20 @@ export default {
...combination,
id: existingCombination.id || "",
sn: existingCombination.sn || "",
quantity: existingCombination.quantity || "",
quantity: this.isECouponGoods
? this.resolvePoolStockDisplay(existingCombination)
: (existingCombination.quantity || ""),
poolStock: existingCombination.poolStock,
cost: existingCombination.cost || "",
price: existingCombination.price || "",
weight: existingCombination.weight || ""
};
} else {
// 新组合使用默认值
return combination;
return {
...combination,
quantity: this.defaultSkuQuantity(),
};
}
});
@@ -1833,7 +1929,7 @@ export default {
// 有重量的情况
if (
this.baseInfoForm.goodsType !== "VIRTUAL_GOODS" &&
this.needsLogistics &&
this.baseInfoForm.salesModel !== "WHOLESALE"
) {
pushData.push({
@@ -1843,7 +1939,7 @@ export default {
}
pushData.push(
{
title: "库存",
title: this.isECouponGoods ? "卡池可用数" : "库存",
slot: "quantity",
},
// {
@@ -1878,7 +1974,10 @@ export default {
...combination,
id: existingCombination.id || "",
sn: existingCombination.sn || "",
quantity: existingCombination.quantity || "",
quantity: this.isECouponGoods
? this.resolvePoolStockDisplay(existingCombination)
: (existingCombination.quantity || ""),
poolStock: existingCombination.poolStock,
cost: existingCombination.cost || "",
price: existingCombination.price || (this.baseInfoForm.salesModel === 'WHOLESALE' && this.wholesaleData.length > 0 ? this.wholesaleData[0].price : ""),
weight: existingCombination.weight || ""
@@ -1889,7 +1988,7 @@ export default {
...combination,
id: "",
sn: "",
quantity: "",
quantity: this.defaultSkuQuantity(),
cost: "",
price: this.baseInfoForm.salesModel === 'WHOLESALE' && this.wholesaleData.length > 0 ? this.wholesaleData[0].price : "",
weight: ""
@@ -2153,6 +2252,10 @@ export default {
return;
}
if (submit.templateId === "") submit.templateId = 0;
if (this.isECouponGoods) {
submit.templateId = 0;
submit.salesModel = "RETAIL";
}
let flag = false;
let paramValue = "";
@@ -2175,7 +2278,7 @@ export default {
let skuCopy = {
cost: 1,
price: sku.price,
quantity: sku.quantity,
quantity: this.resolveSubmitQuantity(sku),
// alertQuantity: sku.alertQuantity,
sn: sku.sn,
images: [],