feat(buyer): 更新电子卡券(E_COUPON)相关逻辑与展示

- 优化购物车与支付页面,确保电子卡券只能通过立即购买结算
- 增强商品详情页,添加限时折扣与第N件优惠展示
- 更新促销逻辑,支持拼团与积分购买选项
- 规范化商品类型与库存提示,提升用户体验
This commit is contained in:
田香琪
2026-08-02 13:55:19 +08:00
parent 1ae7ab3036
commit 1accdf5826
24 changed files with 920 additions and 311 deletions

View File

@@ -116,6 +116,7 @@
>
作废
</el-button>
<span v-else-if="row && row.status === 'RESERVED'" class="text-muted">预占中</span>
<span v-else></span>
</template>
</el-table-column>
@@ -257,10 +258,14 @@ export default {
const withCount = (label, count) =>
count != null && this.stats ? `${label}(${count})` : label;
const total =
(s.unusedCount || 0) + (s.allocatedCount || 0) + (s.voidedCount || 0);
(s.unusedCount || 0) +
(s.reservedCount || 0) +
(s.allocatedCount || 0) +
(s.voidedCount || 0);
return [
{ title: withCount("全部", total), value: "ALL" },
{ title: withCount("未使用", s.unusedCount || 0), value: "UNUSED" },
{ title: withCount("已预占", s.reservedCount || 0), value: "RESERVED" },
{ title: withCount("已分配", s.allocatedCount || 0), value: "ALLOCATED" },
{ title: withCount("已作废", s.voidedCount || 0), value: "VOIDED" },
];
@@ -482,4 +487,9 @@ export default {
color: #909399;
line-height: 1.5;
}
.text-muted {
color: #909399;
font-size: 12px;
}
</style>

View File

@@ -346,9 +346,9 @@
</el-input>
<el-input
v-else-if="col.slot === 'quantity' && isECouponGoods"
:model-value="resolvePoolStockDisplay(row)"
:model-value="row.quantity ?? 0"
disabled
placeholder="卡池可用数"
placeholder="库存"
>
<template #append>{{ baseInfoForm.goodsUnit || "" }}</template>
</el-input>
@@ -561,7 +561,7 @@ export default {
},
},
computed: {
/** 卡密商品:库存只读展示 poolStock,提交时 quantity 传 0FR-S-01 / P-02 */
/** 卡密商品:库存只读展示 quantity,提交时 quantity 传 0FR-S-01 / P-02 */
isECouponGoods() {
return this.baseInfoForm.goodsType === "E_COUPON";
},
@@ -783,17 +783,7 @@ 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真实库存由卡池同步 */
/** E_COUPON 提交固定传 0真实库存由卡池 syncSkuStock 同步至 quantity */
resolveSubmitQuantity(sku) {
if (this.isECouponGoods) {
return 0;
@@ -1338,7 +1328,6 @@ export default {
price: e.price,
// cost: e.cost,
quantity: e.quantity,
poolStock: e.poolStock,
// alertQuantity: e.alertQuantity,
weight: e.weight,
};
@@ -1809,9 +1798,8 @@ export default {
id: existingCombination.id || "",
sn: existingCombination.sn || "",
quantity: this.isECouponGoods
? this.resolvePoolStockDisplay(existingCombination)
? (existingCombination.quantity ?? 0)
: (existingCombination.quantity || ""),
poolStock: existingCombination.poolStock,
cost: existingCombination.cost || "",
price: existingCombination.price || "",
weight: existingCombination.weight || ""
@@ -1870,9 +1858,8 @@ export default {
id: existingCombination.id || "",
sn: existingCombination.sn || "",
quantity: this.isECouponGoods
? this.resolvePoolStockDisplay(existingCombination)
? (existingCombination.quantity ?? 0)
: (existingCombination.quantity || ""),
poolStock: existingCombination.poolStock,
cost: existingCombination.cost || "",
price: existingCombination.price || "",
weight: existingCombination.weight || ""
@@ -1939,7 +1926,7 @@ export default {
}
pushData.push(
{
title: this.isECouponGoods ? "卡池可用数" : "库存",
title: "库存",
slot: "quantity",
},
// {
@@ -1975,9 +1962,8 @@ export default {
id: existingCombination.id || "",
sn: existingCombination.sn || "",
quantity: this.isECouponGoods
? this.resolvePoolStockDisplay(existingCombination)
? (existingCombination.quantity ?? 0)
: (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 || ""

View File

@@ -41,10 +41,19 @@
</div>
<div class="wap-content-desc">
<div class="wap-content-desc-title">{{ item.goodsName }}</div>
<div class="wap-sku">
{{ item.goodsUnit }}
<el-tag
style="margin-left: 10px"
v-if="item.goodsType"
style="margin-left: 6px"
size="small"
:type="goodsTypeTagType(item.goodsType)"
>
{{ goodsTypeLabel(item.goodsType) }}
</el-tag>
<el-tag
style="margin-left: 6px"
:type="item.salesModel === 'RETAIL' ? 'info' : 'primary'"
>
{{ item.salesModel === "RETAIL" ? "零售型" : "批发型" }}
@@ -52,6 +61,9 @@
</div>
<div class="wap-content-desc-bottom">
<div>{{ $filters.unitPrice(item.price) }}</div>
<div v-if="item.goodsType === 'E_COUPON'" class="pool-stock-hint">
库存 {{ item.quantity != null ? item.quantity : "" }}卡池
</div>
</div>
</div>
</div>
@@ -73,6 +85,7 @@
</template>
<script>
import * as API_Goods from "@/api/goods";
import { goodsTypeLabel, goodsTypeTagType } from "@/constants/goodsType";
export default {
props: {
selectedWay: {
@@ -127,6 +140,8 @@ export default {
this.init();
},
methods: {
goodsTypeLabel,
goodsTypeTagType,
onSearchGoods() {
this.goodsData = [];
this.goodsParams.pageNumber = 1;
@@ -265,6 +280,11 @@ export default {
margin-top: 12px;
justify-content: flex-end;
}
.pool-stock-hint {
font-size: 12px;
color: #999;
margin-top: 4px;
}
</style>
<style lang="scss">
.goods-dialog-cascader-popper {

View File

@@ -257,39 +257,55 @@
</el-table-column>
</el-table>
<!-- E_COUPON卡密来自订单详情 orderItems[].cardKeysS-09无独立 API-S-06 -->
<div v-if="isECouponOrder" class="ecoupon-card-keys mt_10">
<!-- 卡密信息E_COUPON 主单或满赠子单 cardKeyFulfillStatus 为准 -->
<div v-if="hasCardKeySection" class="ecoupon-card-keys mt_10">
<h4>卡密信息</h4>
<el-alert
v-if="!ecouponCardKeyDelivered"
type="info"
show-icon
:closable="false"
title="卡密尚未发放"
class="mb_10"
/>
<el-table
v-else-if="ecouponCardKeyRows.length"
border
:data="ecouponCardKeyRows"
style="width: 100%"
<div
v-for="line in cardKeyFulfillLines"
:key="line.key"
class="ecoupon-fulfill-block"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="goodsName" label="商品" min-width="140" show-overflow-tooltip />
<el-table-column prop="cardNo" label="卡号" min-width="140" show-overflow-tooltip />
<el-table-column prop="cardSecret" label="卡密" min-width="120" show-overflow-tooltip />
<el-table-column prop="allocatedTime" label="发卡时间" width="170" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<span v-if="row">{{ formatCardKeyStatus(row.status || 'ALLOCATED') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button v-if="row" link type="primary" @click="copyCardKey(row)">复制</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="cardKeyFulfillLines.length > 1" class="ecoupon-fulfill-title mb_10">
{{ line.goodsName || (line.isGift ? "满赠电子卡券" : "电子卡券") }}
</div>
<el-alert
v-if="line.status !== 'DELIVERED'"
:type="cardKeyFulfillAlertType(line.status)"
show-icon
:closable="false"
:title="resolveCardKeyFulfillMessage(line)"
class="mb_10"
/>
<el-table
v-else-if="line.cardKeys.length"
border
:data="lineCardKeyRows(line)"
style="width: 100%"
class="mb_10"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column
v-if="cardKeyFulfillLines.length > 1"
prop="goodsName"
label="商品"
min-width="140"
show-overflow-tooltip
/>
<el-table-column prop="cardNo" label="卡号" min-width="140" show-overflow-tooltip />
<el-table-column prop="cardSecret" label="卡密" min-width="120" show-overflow-tooltip />
<el-table-column prop="allocatedTime" label="发卡时间" width="170" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<span v-if="row">{{ formatCardKeyStatus(row.status || 'ALLOCATED') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button v-if="row" link type="primary" @click="copyCardKey(row)">复制</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="goods-total">
@@ -731,6 +747,13 @@ import multipleMap from "@/views/my-components/map/multiple-map";
import vueQr from "vue-qr";
import { printElement } from "@/utils/print";
import { formatCardKeyStatus } from "@/constants/cardKey";
import {
collectCardKeyFulfillLines,
orderHasCardKeySection,
flattenOrderCardKeys,
cardKeyFulfillAlertType,
resolveCardKeyFulfillMessage,
} from "@/constants/goodsType";
export default {
name: "orderDetail",
components: {
@@ -863,20 +886,18 @@ export default {
isNonPhysicalOrder() {
return this.isVirtualOrder || this.isECouponOrder;
},
ecouponCardKeyDelivered() {
return (this.data || []).some((item) => item.cardKeyDelivered);
hasCardKeySection() {
return orderHasCardKeySection({
orderItems: this.data,
giftSummaries: this.orderInfo.giftECouponOrders,
isECouponOrder: this.isECouponOrder,
});
},
cardKeyFulfillLines() {
return collectCardKeyFulfillLines(this.data, this.orderInfo.giftECouponOrders);
},
ecouponCardKeyRows() {
const rows = [];
(this.data || []).forEach((item) => {
(item.cardKeys || []).forEach((ck) => {
rows.push({
...ck,
goodsName: item.goodsName,
});
});
});
return rows;
return flattenOrderCardKeys(this.data, true, this.orderInfo.giftECouponOrders);
},
canPartDelivery() {
if (!this.allowOperation.ship) return false;
@@ -910,6 +931,14 @@ export default {
},
methods: {
formatCardKeyStatus,
cardKeyFulfillAlertType,
resolveCardKeyFulfillMessage,
lineCardKeyRows(line) {
return (line.cardKeys || []).map((ck) => ({
...ck,
goodsName: line.goodsName,
}));
},
copyCardKey(row) {
const text = `卡号:${row.cardNo || ""}\n卡密${row.cardSecret || ""}`;
if (navigator.clipboard && navigator.clipboard.writeText) {

View File

@@ -33,7 +33,12 @@
<el-table-column prop="originalPrice" label="原价" width="90" align="center" />
<el-table-column label="活动价" width="150" align="center">
<template #default="{ row }">
<el-input-number v-model="row.price" :min="0.01" :precision="2" size="small" />
<el-input-number
v-model="row.price"
:min="promotionPriceMin(row.goodsType)"
:precision="2"
size="small"
/>
</template>
</el-table-column>
</el-table>
@@ -50,6 +55,7 @@
<script>
import { getFlashDiscountDetail, saveFlashDiscount, editFlashDiscount } from "@/api/promotion.js";
import { promotionPriceMin } from "@/constants/goodsType";
export default {
data() {
@@ -75,6 +81,7 @@ export default {
}
},
methods: {
promotionPriceMin,
openGoods() {
this.$refs.liliDialog.open("goods");
},
@@ -88,6 +95,7 @@ export default {
originalPrice: g.price,
price: g.price,
quantity: g.quantity,
goodsType: g.goodsType,
});
}
});

View File

@@ -113,6 +113,7 @@
</el-select>
</el-form-item>
<el-form-item v-if="form.giftFlag" label="赠品" prop="giftId">
<p class="describe">电子卡券赠品支付后自动发卡卡池不足时仅取消赠品子单不影响主订单</p>
<el-select
v-model="form.giftId"
filterable
@@ -127,7 +128,7 @@
v-for="item in giftList"
:value="item.id"
:key="item.id"
:label="item.goodsName"
:label="giftOptionLabel(item)"
/>
</el-select>
</el-form-item>
@@ -241,6 +242,7 @@ import {
import { getGoodsSkuListDataSeller } from "@/api/goods";
import { regular } from "@/utils";
import vueQr from "vue-qr";
import { goodsTypeLabel } from "@/constants/goodsType";
export default {
name: "add-full-discount",
@@ -313,6 +315,16 @@ export default {
this.getGiftList();
},
methods: {
giftOptionLabel(item) {
if (!item) return "";
const typeLabel =
item.goodsType === "E_COUPON" ? ` [${goodsTypeLabel(item.goodsType)}]` : "";
const stock =
item.goodsType === "E_COUPON" && item.quantity != null
? ` · 库存${item.quantity}`
: "";
return `${item.goodsName || ""}${typeLabel}${stock}`;
},
closeCurrentPage() {
this.$router.back();
},
@@ -442,6 +454,7 @@ export default {
thumbnail: e.thumbnail,
skuId: e.id,
goodsId: e.goodsId,
goodsType: e.goodsType,
});
});
this.form.promotionGoodsList = list;

View File

@@ -44,7 +44,7 @@
<el-input-number
v-if="row"
v-model="row.price"
:min="0.01"
:min="promotionPriceMin(row.goodsType)"
:precision="2"
controls-position="right"
size="small"
@@ -133,6 +133,7 @@
import { getPintuanGoodsList, getPintuanDetail, editPintuan } from "@/api/promotion.js";
import liliDialog from "@/views/lili-dialog";
import vueQr from "vue-qr";
import { promotionPriceMin } from "@/constants/goodsType";
export default {
components: { liliDialog, vueQr },
@@ -158,6 +159,7 @@ export default {
},
},
methods: {
promotionPriceMin,
promotionStatusText(status) {
const map = {
NEW: "未开始",

View File

@@ -68,7 +68,7 @@
<el-input-number
v-if="row"
v-model="row.price"
:min="0.01"
:min="promotionPriceMin(row.goodsType)"
:precision="2"
:disabled="row.promotionApplyStatus === 'PASS'"
controls-position="right"
@@ -185,6 +185,7 @@ import {
delSeckillGoods,
} from "@/api/promotion.js";
import liliDialog from "@/views/lili-dialog";
import { promotionPriceMin } from "@/constants/goodsType";
export default {
components: {
@@ -215,6 +216,7 @@ export default {
},
},
methods: {
promotionPriceMin,
seckillStatusText(status) {
const map = {
NEW: "未开始",
@@ -349,6 +351,7 @@ export default {
storeName: item.storeName,
skuId: item.id,
timeLine,
goodsType: item.goodsType,
};
});
this.goodsList[this.tabIndex].list = list;