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

@@ -65,7 +65,7 @@ export function addCartGoods (params) {
/** /**
* 创建交易 * 创建交易
* @param client 客户端H5/移动端 PC/PC端,WECHAT_MP/小程序端,APP/移动应用端 * @param client 客户端H5/移动端 PC/PC端,WECHAT_MP/小程序端,APP/移动应用端
* @param way 购物车购买CART/立即购买BUY_NOW / 积分购买POINT * @param way 购物车CART / 立即购买BUY_NOW / 积分POINTS / 拼团PINTUAN / 砍价KANJIA
* @param remark 备注 非必填 * @param remark 备注 非必填
*/ */
export function createTrade (data) { export function createTrade (data) {
@@ -81,7 +81,7 @@ export function createTrade (data) {
/** /**
* 选择优惠券 * 选择优惠券
* @param memberCouponId 优惠券id * @param memberCouponId 优惠券id
* @param way 购物车购买CART/立即购买BUY_NOW/ 积分购买POINT * @param way 购物车CART / 立即购买BUY_NOW / 积分POINTS
* @param used 使用true 弃用 false * @param used 使用true 弃用 false
*/ */
export function selectCoupon (params) { export function selectCoupon (params) {
@@ -95,7 +95,7 @@ export function selectCoupon (params) {
/** /**
* 选择礼品卡 * 选择礼品卡
* @param way 购物车购买CART/立即购买BUY_NOW/拼团购买PINTUAN / 积分购买POINT * @param way 购物车CART / 立即购买BUY_NOW / 拼团PINTUAN / 积分POINTS / 砍价KANJIA
* @param credentialId 礼品卡凭证 idused=false 且不传则清空全部 * @param credentialId 礼品卡凭证 idused=false 且不传则清空全部
* @param used 使用 true / 弃用 false * @param used 使用 true / 弃用 false
*/ */

View File

@@ -71,7 +71,7 @@
</div> </div>
<!-- 限时秒杀 --> <!-- 限时秒杀 -->
<Promotion <Promotion
v-if="!isECouponGoods && promotionMap['SECKILL']" v-if="promotionMap['SECKILL']"
:time="promotionMap['SECKILL'].endTime" :time="promotionMap['SECKILL'].endTime"
></Promotion> ></Promotion>
<!-- 商品详细 价格优惠券促销 --> <!-- 商品详细 价格优惠券促销 -->
@@ -80,7 +80,7 @@
<!-- 秒杀价格 --> <!-- 秒杀价格 -->
<div <div
class="item-price-row" class="item-price-row"
v-if="skuDetail.promotionPrice && !isECouponGoods && promotionMap['SECKILL']" v-if="skuDetail.promotionPrice && promotionMap['SECKILL']"
> >
<p> <p>
<span class="item-price-title" v-if="promotionMap['SECKILL']" <span class="item-price-title" v-if="promotionMap['SECKILL']"
@@ -126,7 +126,7 @@
</div> </div>
</div> </div>
<!-- 优惠券展示 --> <!-- 优惠券展示 -->
<div class="item-price-coupon-row" v-if="!isECouponGoods && promotionMap['COUPON'].length"> <div class="item-price-coupon-row" v-if="promotionMap['COUPON'].length">
<div class="Ellipsis"> <div class="Ellipsis">
<span class="item-price-title"> </span> <span class="item-price-title"> </span>
<span> <span>
@@ -165,8 +165,38 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 限时折扣 -->
<div class="item-price-row" v-if="promotionMap['DISCOUNT']">
<p>
<span class="item-price-title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="item-promotion">限时折扣</span>
<span class="item-desc-pintuan" v-if="promotionMap['DISCOUNT'].discount">
{{ promotionMap["DISCOUNT"].discount }}
</span>
</p>
</div>
<!-- 第N件优惠 -->
<div class="item-price-row" v-if="promotionMap['NTH']">
<p>
<span class="item-price-title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="item-promotion">第N件</span>
<span class="item-desc-pintuan">
{{ promotionMap["NTH"].nthNum || promotionMap["NTH"].num }} 件优惠
</span>
</p>
</div>
<!-- 拼团 -->
<div class="item-price-row" v-if="promotionMap['PINTUAN']">
<p>
<span class="item-price-title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="item-promotion">拼团价</span>
<span class="item-desc-pintuan" v-if="promotionMap['PINTUAN'].price">
{{ $filters.unitPrice(promotionMap["PINTUAN"].price, "") }}
</span>
</p>
</div>
<!-- 满减展示 --> <!-- 满减展示 -->
<div class="item-price-row" v-if="!isECouponGoods && promotionMap['FULL_DISCOUNT']"> <div class="item-price-row" v-if="promotionMap['FULL_DISCOUNT']">
<p> <p>
<span class="item-price-title" <span class="item-price-title"
>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span> >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
@@ -294,6 +324,14 @@
@click="addShoppingCartBtn" @click="addShoppingCartBtn"
>加入购物车</el-button >加入购物车</el-button
> >
<el-button
class="goods-action-btn"
v-if="promotionMap['PINTUAN']"
:loading="loadingPintuan"
:disabled="isOutOfStock"
@click="pintuanBuy"
>拼团购买</el-button
>
<el-button <el-button
class="goods-action-btn" class="goods-action-btn"
:loading="loading1" :loading="loading1"
@@ -334,6 +372,11 @@ import {
isECoupon, isECoupon,
getECouponStock, getECouponStock,
getECouponMaxBuyNum, getECouponMaxBuyNum,
createEmptyPromotionMap,
resolveECouponCartType,
E_COUPON_CART_BUY_NOW,
E_COUPON_CART_PINTUAN,
E_COUPON_CART_POINTS,
} from "@/constants/goodsType"; } from "@/constants/goodsType";
export default { export default {
@@ -377,15 +420,11 @@ export default {
specList: [], specList: [],
}, // sku详情 }, // sku详情
goodsSpecList: this.detail.specs, // 商品spec goodsSpecList: this.detail.specs, // 商品spec
promotionMap: { promotionMap: createEmptyPromotionMap(),
// 活动状态
SECKILL: null,
FULL_DISCOUNT: null,
COUPON: [],
}, // 促销活动
formatList: [], // 选择商品品类的数组 formatList: [], // 选择商品品类的数组
loading: false, // 立即购买loading loading: false, // 立即购买loading
loading1: false, // 加入购物车loading loading1: false, // 加入购物车loading
loadingPintuan: false,
isCollected: false, // 是否收藏 isCollected: false, // 是否收藏
}; };
}, },
@@ -412,7 +451,7 @@ export default {
}) })
: []; : [];
}, },
/** 卡密商品:禁加购/禁促销,仅立即购买库存用 poolStockFR-B-01 / P-06 */ /** 卡密商品:禁加购,仅立即购买库存读 quantityM-02 起支持促销 */
isECouponGoods() { isECouponGoods() {
return isECoupon(this.skuDetail.goodsType); return isECoupon(this.skuDetail.goodsType);
}, },
@@ -539,21 +578,32 @@ export default {
}); });
}, },
buyNow() { buyNow() {
// 立即购买 this.startCheckout(E_COUPON_CART_BUY_NOW, "loading1");
},
pintuanBuy() {
this.startCheckout(E_COUPON_CART_PINTUAN, "loadingPintuan");
},
pointPay() {
this.startCheckout(E_COUPON_CART_POINTS, "loading");
},
startCheckout(action, loadingKey) {
const params = { const params = {
num: this.count, num: this.count,
skuId: this.skuDetail.id, skuId: this.skuDetail.id,
cartType: "BUY_NOW", cartType: "BUY_NOW",
}; };
// 虚拟商品cartType=VIRTUAL核销码流程
if (this.skuDetail.goodsType === "VIRTUAL_GOODS") { if (this.skuDetail.goodsType === "VIRTUAL_GOODS") {
params.cartType = "VIRTUAL"; params.cartType = "VIRTUAL";
} else if (this.isECouponGoods) {
params.cartType = resolveECouponCartType({
routeWay: this.$route.query.way,
action,
});
} }
// E_COUPON 卡密商品cartType 须保持 BUY_NOW勿用 VIRTUALFR-B-01 / EC-24 this[loadingKey] = true;
this.loading1 = true;
addCartGoods(params) addCartGoods(params)
.then((res) => { .then((res) => {
this.loading1 = false; this[loadingKey] = false;
if (res.success) { if (res.success) {
this.$router.push({ this.$router.push({
path: "/pay", path: "/pay",
@@ -564,7 +614,7 @@ export default {
} }
}) })
.catch(() => { .catch(() => {
this.loading1 = false; this[loadingKey] = false;
}); });
}, },
async collect() { async collect() {
@@ -642,16 +692,20 @@ export default {
}); });
}, },
promotion() { promotion() {
this.promotionMap = {SECKILL: null, FULL_DISCOUNT: null, COUPON: []}; this.promotionMap = createEmptyPromotionMap();
// 格式化促销活动,返回当前促销的对象 if (!this.detail || !this.detail.promotionMap) return false;
if (!this.detail.promotionMap) return false; const keysArr = Object.keys(this.detail.promotionMap);
let keysArr = Object.keys(this.detail.promotionMap);
if (keysArr.length === 0) return false; if (keysArr.length === 0) return false;
for (let i = 0; i < keysArr.length; i++) { for (let i = 0; i < keysArr.length; i++) {
let key = keysArr[i].split("-")[0]; const key = keysArr[i].split("-")[0];
if (key === "COUPON") { if (key === "COUPON") {
this.promotionMap[key].push(this.detail.promotionMap[keysArr[i]]); if (!Array.isArray(this.promotionMap.COUPON)) {
this.promotionMap.COUPON = [];
}
this.promotionMap.COUPON.push(this.detail.promotionMap[keysArr[i]]);
} else if (Object.prototype.hasOwnProperty.call(this.promotionMap, key)) {
this.promotionMap[key] = this.detail.promotionMap[keysArr[i]];
} else { } else {
this.promotionMap[key] = this.detail.promotionMap[keysArr[i]]; this.promotionMap[key] = this.detail.promotionMap[keysArr[i]];
} }

View File

@@ -1,9 +1,9 @@
/** /**
* 卡密商品E_COUPON / 电子卡券)— 买家端公共常量与工具 * 卡密商品E_COUPON / 电子卡券)— 买家端公共常量与工具
* *
* 与 VIRTUAL_GOODS核销型虚拟区分E_COUPON 仅立即购买(BUY_NOW * 与 VIRTUAL_GOODS核销型虚拟区分E_COUPON 常规促销走 BUY_NOW
* 支付后自动发卡,订单终态 COMPLETED可售库存优先读 poolStock * 特殊活动走 PINTUAN / POINTS / KANJIA禁止 CART / VIRTUAL
* 需求lilishop/docs/requirements/card-key-goods-v4.md * 需求lilishop/docs/requirements/card-key-goods-v4.md · M-02
* *
* @author Mike * @author Mike
* @date 2026-07-31 * @date 2026-07-31
@@ -11,9 +11,15 @@
export const E_COUPON_GOODS_TYPE = "E_COUPON"; export const E_COUPON_GOODS_TYPE = "E_COUPON";
export const E_COUPON_ORDER_TYPE = "E_COUPON"; export const E_COUPON_ORDER_TYPE = "E_COUPON";
/** 单次购买数量上限O-09 待拍板默认min(poolStock, 999) */ /** 单次购买数量上限O-09 默认min(quantity, 999) */
export const E_COUPON_MAX_BUY_NUM = 999; export const E_COUPON_MAX_BUY_NUM = 999;
/** 常规立即购买(秒杀/满减/券/限时/第N件等 */
export const E_COUPON_CART_BUY_NOW = "BUY_NOW";
export const E_COUPON_CART_PINTUAN = "PINTUAN";
export const E_COUPON_CART_POINTS = "POINTS";
export const E_COUPON_CART_KANJIA = "KANJIA";
export function isECoupon(goodsType) { export function isECoupon(goodsType) {
return goodsType === E_COUPON_GOODS_TYPE; return goodsType === E_COUPON_GOODS_TYPE;
} }
@@ -22,13 +28,9 @@ export function isECouponOrder(orderType) {
return orderType === E_COUPON_ORDER_TYPE; return orderType === E_COUPON_ORDER_TYPE;
} }
/** 可售库存:优先 poolStock */ /** 可售库存:E_COUPON 读 SKU quantity由卡池 syncSkuStock 同步) */
export function getECouponStock(sku) { export function getECouponStock(sku) {
if (!sku) return 0; if (!sku) return 0;
const pool = sku.poolStock;
if (pool !== undefined && pool !== null) {
return Number(pool) || 0;
}
return Number(sku.quantity) || 0; return Number(sku.quantity) || 0;
} }
@@ -37,3 +39,167 @@ export function getECouponMaxBuyNum(sku) {
if (stock <= 0) return 1; if (stock <= 0) return 1;
return Math.min(stock, E_COUPON_MAX_BUY_NUM); return Math.min(stock, E_COUPON_MAX_BUY_NUM);
} }
/**
* 解析 E_COUPON 结算 cartType / way
* @param {object} opts
* @param {string} [opts.routeWay] 路由 query.way
* @param {'BUY_NOW'|'PINTUAN'|'POINTS'|'KANJIA'} [opts.action] 用户点击的购买入口
*/
export function resolveECouponCartType({ routeWay, action } = {}) {
if (action === E_COUPON_CART_PINTUAN) return E_COUPON_CART_PINTUAN;
if (action === E_COUPON_CART_POINTS) return E_COUPON_CART_POINTS;
if (action === E_COUPON_CART_KANJIA) return E_COUPON_CART_KANJIA;
if (action === E_COUPON_CART_BUY_NOW) return E_COUPON_CART_BUY_NOW;
const way = routeWay ? String(routeWay).toUpperCase() : "";
if (way === "KANJIA") return E_COUPON_CART_KANJIA;
if (way === "PINTUAN") return E_COUPON_CART_PINTUAN;
if (way === "POINT" || way === "POINTS") return E_COUPON_CART_POINTS;
return E_COUPON_CART_BUY_NOW;
}
/** 详情页 promotionMap 初始结构(切换 SKU 时完整重置) */
export function createEmptyPromotionMap() {
return {
SECKILL: null,
FULL_DISCOUNT: null,
COUPON: [],
DISCOUNT: null,
NTH: null,
PINTUAN: null,
KANJIA: null,
};
}
/** 卡密履约状态(与后端 CardKeyFulfillStatusEnum 一致) */
export const CARD_KEY_FULFILL_STATUS = {
PENDING: "PENDING",
DELIVERED: "DELIVERED",
FAILED: "FAILED",
NOT_APPLICABLE: "NOT_APPLICABLE",
};
export const CARD_KEY_FULFILL_STATUS_TEXT = {
PENDING: "待发放",
DELIVERED: "已发放",
FAILED: "发放失败",
NOT_APPLICABLE: "不适用",
};
export function cardKeyFulfillAlertType(status) {
const map = {
PENDING: "info",
DELIVERED: "success",
FAILED: "warning",
};
return map[status] || "info";
}
export function formatCardKeyFulfillStatus(status) {
return CARD_KEY_FULFILL_STATUS_TEXT[status] || status || "—";
}
export function resolveCardKeyFulfillMessage(line) {
if (line && line.message) return line.message;
return formatCardKeyFulfillStatus(line?.status);
}
export function isCardKeyFulfillApplicable(item) {
if (!item) return false;
const status = item.cardKeyFulfillStatus;
if (status) return status !== CARD_KEY_FULFILL_STATUS.NOT_APPLICABLE;
if (item.cardKeyDelivered != null || (item.cardKeys && item.cardKeys.length)) return true;
return item.goodsType === E_COUPON_GOODS_TYPE;
}
function normalizeCardKeyFulfillLine(source) {
const status =
source.cardKeyFulfillStatus ||
(source.cardKeyDelivered
? CARD_KEY_FULFILL_STATUS.DELIVERED
: CARD_KEY_FULFILL_STATUS.PENDING);
const cardKeys =
status === CARD_KEY_FULFILL_STATUS.DELIVERED && source.cardKeyDelivered
? source.cardKeys || []
: [];
return {
key: source.sn || source.orderSn || source.id || source.skuId,
goodsName: source.goodsName,
status,
message: source.cardKeyFulfillMessage,
cardKeys,
isGift: !!source.isGift,
};
}
/** 汇总 E_COUPON 主单行 + 满赠子单摘要的履约行 */
export function collectCardKeyFulfillLines(orderItems = [], giftSummaries = []) {
const lines = [];
(orderItems || []).forEach((item) => {
if (!isCardKeyFulfillApplicable(item)) return;
lines.push(normalizeCardKeyFulfillLine(item));
});
(giftSummaries || []).forEach((gift) => {
lines.push(normalizeCardKeyFulfillLine({ ...gift, isGift: true }));
});
return lines;
}
export function orderHasCardKeySection({ orderItems, giftSummaries, isECouponOrder } = {}) {
if (isECouponOrder) return true;
if (Array.isArray(giftSummaries) && giftSummaries.length > 0) return true;
if (collectCardKeyFulfillLines(orderItems, []).length > 0) return true;
return (orderItems || []).some(
(item) =>
item.goodsType === E_COUPON_GOODS_TYPE ||
(Array.isArray(item.cardKeys) && item.cardKeys.length > 0)
);
}
export function flattenCardKeyFulfillLines(lines, withGoodsName = false) {
const rows = [];
(lines || []).forEach((line) => {
if (line.status !== CARD_KEY_FULFILL_STATUS.DELIVERED) return;
(line.cardKeys || []).forEach((ck) => {
rows.push(withGoodsName ? { ...ck, goodsName: line.goodsName } : { ...ck });
});
});
return rows;
}
/** 订单是否含已交付或可展示的卡密行(含满赠 E_COUPON 子单行) */
export function orderHasCardKeyContent(orderItems, giftSummaries) {
const lines = collectCardKeyFulfillLines(orderItems, giftSummaries);
return flattenCardKeyFulfillLines(lines).length > 0;
}
/** 扁平化订单项卡密(仅 cardKeyDelivered 且含 cardKeys兼容旧逻辑 */
export function flattenOrderCardKeys(orderItems, withGoodsName = false, giftSummaries) {
const lines = collectCardKeyFulfillLines(orderItems, giftSummaries);
if (lines.some((line) => line.status)) {
return flattenCardKeyFulfillLines(lines, withGoodsName);
}
const rows = [];
(orderItems || []).forEach((item) => {
if (!item.cardKeyDelivered || !item.cardKeys || !item.cardKeys.length) return;
item.cardKeys.forEach((ck) => {
rows.push(withGoodsName ? { ...ck, goodsName: item.goodsName } : { ...ck });
});
});
return rows;
}
/** createTrade 后是否无需进入收银台(积分单 / 0 元 / 服务端已标记) */
export function shouldSkipPaymentPage(tradeResult, way) {
if (!tradeResult) return false;
if (way === E_COUPON_CART_POINTS) return true;
if (tradeResult.needPay === false || tradeResult.paid === true) return true;
const price =
tradeResult.price != null
? Number(tradeResult.price)
: tradeResult.flowPrice != null
? Number(tradeResult.flowPrice)
: null;
if (price != null && !Number.isNaN(price) && price <= 0) return true;
return false;
}

View File

@@ -248,6 +248,7 @@ import Promotion from "@/components/goodsDetail/Promotion";
import Search from "@/components/Search"; import Search from "@/components/Search";
import * as APICart from "@/api/cart"; import * as APICart from "@/api/cart";
import * as APIMember from "@/api/member"; import * as APIMember from "@/api/member";
import { isECoupon } from "@/constants/goodsType";
export default { export default {
name: "Cart", name: "Cart",
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
@@ -407,11 +408,20 @@ export default {
}, },
// 跳转支付页面 // 跳转支付页面
pay() { pay() {
if (this.checkedNum) { if (!this.checkedNum) {
this.$router.push({ path: "/pay", query: { way: "CART" } });
} else {
Message.warning("请至少选择一件商品"); Message.warning("请至少选择一件商品");
return;
} }
const hasECoupon = this.cartList.some((shop) =>
(shop.skuList || []).some(
(goods) => goods.checked && isECoupon(goods.goodsSku && goods.goodsSku.goodsType)
)
);
if (hasECoupon) {
Message.warning("电子卡券请使用立即购买,不可通过购物车结算");
return;
}
this.$router.push({ path: "/pay", query: { way: "CART" } });
}, },
// 展示优惠券 // 展示优惠券
showCoupon(storeId, index) { showCoupon(storeId, index) {

View File

@@ -132,23 +132,39 @@
</template> </template>
<div v-else style="color: #999; margin-left: 5px">未开发票</div> <div v-else style="color: #999; margin-left: 5px">未开发票</div>
</div> </div>
<!-- 电子卡券卡密信息PC 内嵌 --> <!-- 卡密信息E_COUPON 主单或满赠子单,以 cardKeyFulfillStatus 为准 -->
<div class="order-card ecoupon-card-keys" v-if="isECouponOrder"> <div class="order-card ecoupon-card-keys" v-if="hasCardKeySection">
<h3>卡密信息</h3> <h3>卡密信息</h3>
<div
v-for="line in cardKeyFulfillLines"
:key="line.key"
class="ecoupon-fulfill-block"
>
<div v-if="cardKeyFulfillLines.length > 1" class="ecoupon-fulfill-title">
{{ line.goodsName || (line.isGift ? "满赠电子卡券" : "电子卡券") }}
</div>
<el-alert <el-alert
v-if="!ecouponCardKeyDelivered" v-if="line.status !== 'DELIVERED'"
type="info" :type="cardKeyFulfillAlertType(line.status)"
show-icon show-icon
:closable="false" :closable="false"
title="卡密尚未发放,请稍后刷新或联系商家" :title="resolveCardKeyFulfillMessage(line)"
class="mb_10"
/> />
<template v-else-if="ecouponCardKeyRows.length"> <template v-else-if="line.cardKeys.length">
<div class="ecoupon-card-keys-mobile"> <div class="ecoupon-card-keys-mobile">
<el-button type="primary" @click="cardKeyDialogVisible = true">查看卡密</el-button> <el-button type="primary" @click="cardKeyDialogVisible = true">查看卡密</el-button>
</div> </div>
<div class="ecoupon-card-keys-pc"> <div class="ecoupon-card-keys-pc">
<el-table border :data="ecouponCardKeyRows" style="width: 100%"> <el-table border :data="lineCardKeyRows(line)" style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column
v-if="cardKeyFulfillLines.length > 1"
prop="goodsName"
label="商品"
min-width="120"
show-overflow-tooltip
/>
<el-table-column prop="cardNo" 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="cardSecret" label="卡密" min-width="120" show-overflow-tooltip />
<el-table-column prop="allocatedTime" label="发卡时间" width="170" /> <el-table-column prop="allocatedTime" label="发卡时间" width="170" />
@@ -158,10 +174,18 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-button class="mt_10" size="small" @click="copyAllCardKeys">复制全部卡密</el-button>
</div> </div>
</template> </template>
</div> </div>
<el-button
v-if="ecouponCardKeyRows.length"
class="mt_10 ecoupon-card-keys-pc"
size="small"
@click="copyAllCardKeys"
>
复制全部卡密
</el-button>
</div>
<!-- 订单商品 --> <!-- 订单商品 -->
<div class="goods"> <div class="goods">
@@ -378,7 +402,14 @@ import {
getPackage getPackage
} from "@/api/order.js"; } from "@/api/order.js";
import { afterSaleReason, receiptDetail } from "@/api/member"; import { afterSaleReason, receiptDetail } from "@/api/member";
import { isECouponOrder as checkECouponOrder } from "@/constants/goodsType"; import {
isECouponOrder as checkECouponOrder,
collectCardKeyFulfillLines,
orderHasCardKeySection,
flattenOrderCardKeys,
cardKeyFulfillAlertType,
resolveCardKeyFulfillMessage,
} from "@/constants/goodsType";
/** /**
* 订单详情E_COUPON 展示 orderItems[].cardKeys屏蔽售后/物流FR-B-03 / FR-B-04 * 订单详情E_COUPON 展示 orderItems[].cardKeys屏蔽售后/物流FR-B-03 / FR-B-04
@@ -415,20 +446,37 @@ export default {
isNonPhysicalOrder() { isNonPhysicalOrder() {
return this.isVirtualOrder || this.isECouponOrder; return this.isVirtualOrder || this.isECouponOrder;
}, },
ecouponCardKeyDelivered() { /** 是否展示卡密区块(含满赠 E_COUPON 子单 giftECouponOrders */
return (this.order.orderItems || []).some((item) => item.cardKeyDelivered); hasCardKeySection() {
return orderHasCardKeySection({
orderItems: this.order.orderItems,
giftSummaries: this.order.giftECouponOrders,
isECouponOrder: this.isECouponOrder,
});
},
cardKeyFulfillLines() {
return collectCardKeyFulfillLines(
this.order.orderItems,
this.order.giftECouponOrders
);
}, },
ecouponCardKeyRows() { ecouponCardKeyRows() {
const rows = []; return flattenOrderCardKeys(
(this.order.orderItems || []).forEach((item) => { this.order.orderItems,
(item.cardKeys || []).forEach((ck) => { false,
rows.push({ ...ck }); this.order.giftECouponOrders
}); );
});
return rows;
}, },
}, },
methods: { methods: {
cardKeyFulfillAlertType,
resolveCardKeyFulfillMessage,
lineCardKeyRows(line) {
return (line.cardKeys || []).map((ck) => ({
...ck,
goodsName: line.goodsName,
}));
},
copyCardKey(row) { copyCardKey(row) {
const text = `卡号:${row.cardNo || ""}\n卡密${row.cardSecret || ""}`; const text = `卡号:${row.cardNo || ""}\n卡密${row.cardSecret || ""}`;
this.copyText(text); this.copyText(text);
@@ -767,6 +815,18 @@ table {
display: none; display: none;
} }
.ecoupon-fulfill-block + .ecoupon-fulfill-block {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #eee;
}
.ecoupon-fulfill-title {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.ecoupon-card-keys-pc { .ecoupon-card-keys-pc {
display: block; display: block;
} }

View File

@@ -151,8 +151,8 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 发票信息 --> <!-- 发票信息E_COUPON 虚拟型结算不开发票 -->
<div class="invoice"> <div class="invoice" v-if="!isECouponCheckout">
<div class="card-head mt_20 mb_20"> <div class="card-head mt_20 mb_20">
<span class="relative">发票信息<span class="inv-tips"> <span class="relative">发票信息<span class="inv-tips">
<el-icon><Warning /></el-icon>开企业抬头发票须填写纳税人识别号以免影响报销 <el-icon><Warning /></el-icon>开企业抬头发票须填写纳税人识别号以免影响报销
@@ -167,8 +167,8 @@
<span @click="editInvoice">编辑</span> <span @click="editInvoice">编辑</span>
</div> </div>
</div> </div>
<!-- 优惠券E_COUPON 不参与促销 M-01 --> <!-- 优惠券 -->
<div class="invoice" v-if="!isECouponCheckout"> <div class="invoice">
<div class="card-head mt_20 mb_20"> <div class="card-head mt_20 mb_20">
<span class="relative">优惠券</span> <span class="relative">优惠券</span>
</div> </div>
@@ -204,7 +204,7 @@
</ul> </ul>
</div> </div>
<!-- 礼品卡 --> <!-- 礼品卡 -->
<div class="invoice pay-gcc-module" v-if="!isECouponCheckout"> <div class="invoice pay-gcc-module">
<div class="pay-gcc-head"> <div class="pay-gcc-head">
<div class="pay-gcc-title"> <div class="pay-gcc-title">
使用礼品卡 使用礼品卡
@@ -284,13 +284,13 @@
<!-- 底部支付栏 --> <!-- 底部支付栏 -->
<div class="order-footer"> <div class="order-footer">
<div class="pay ml_20" @click="pay">提交订单</div> <div class="pay ml_20" @click="pay">提交订单</div>
<div class="pay-address" v-if="addressList.length && selectedDeliverMethod === 'LOGISTICS'"> <div class="pay-address" v-if="!isVirtualLikeCheckout && addressList.length && selectedDeliverMethod === 'LOGISTICS'">
配送至{{ $filters.unitAddress(selectedAddress.consigneeAddressPath) }} 配送至{{ $filters.unitAddress(selectedAddress.consigneeAddressPath) }}
{{ selectedAddress.detail }}&nbsp;&nbsp;收货人{{ {{ selectedAddress.detail }}&nbsp;&nbsp;收货人{{
selectedAddress.name selectedAddress.name
}}&nbsp;&nbsp;{{ selectedAddress.mobile }} }}&nbsp;&nbsp;{{ selectedAddress.mobile }}
</div> </div>
<div class="pay-address" v-if="addressList.length && selectedDeliverMethod === 'SELF_PICK_UP'"> <div class="pay-address" v-if="!isVirtualLikeCheckout && addressList.length && selectedDeliverMethod === 'SELF_PICK_UP'">
自提地点{{selectedStoreAddress.address}} &nbsp;&nbsp;联系方式{{ selectedStoreAddress.mobile }} 自提地点{{selectedStoreAddress.address}} &nbsp;&nbsp;联系方式{{ selectedStoreAddress.mobile }}
</div> </div>
</div> </div>
@@ -329,7 +329,7 @@ import {
} from "@/api/cart"; } from "@/api/cart";
import { getStoreAddress } from "@/api/shopentry.js" import { getStoreAddress } from "@/api/shopentry.js"
import { canUseCouponList } from "@/api/member.js"; import { canUseCouponList } from "@/api/member.js";
import { isECoupon } from "@/constants/goodsType"; import { isECoupon, shouldSkipPaymentPage } from "@/constants/goodsType";
export default { export default {
name: "Pay", name: "Pay",
@@ -379,7 +379,7 @@ export default {
); );
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
}, },
/** 电子卡券结算:免地址、隐藏优惠券/礼品卡/发票M-01 / O-11 */ /** 电子卡券结算:免地址、隐藏发票;优惠券/礼品卡/促销明细可用M-02 */
isECouponCheckout() { isECouponCheckout() {
return isECoupon(this.goodsType); return isECoupon(this.goodsType);
}, },
@@ -855,15 +855,15 @@ export default {
.then((res) => { .then((res) => {
Spin.hide(); Spin.hide();
if (res.success) { if (res.success) {
if (params.way === "POINTS") { const way = params.way;
// 积分支付不需要跳转支付页面 if (shouldSkipPaymentPage(res.result, way)) {
this.$router.push("/payDone"); this.$router.push("/payDone");
} else { return;
}
this.$router.push({ this.$router.push({
path: "/payment", path: "/payment",
query: { orderType: "TRADE", sn: res.result.sn }, query: { orderType: "TRADE", sn: res.result.sn },
}); });
}
} else if (res.message) { } else if (res.message) {
Message.warning(res.message); Message.warning(res.message);
} }

View File

@@ -78,6 +78,10 @@ export default {
this.isStart = true this.isStart = true
this.support = this.payDetail.support this.support = this.payDetail.support
this.walletValue = this.payDetail.walletValue this.walletValue = this.payDetail.walletValue
const price = Number(this.payDetail.price);
if (!Number.isNaN(price) && price <= 0) {
this.$router.replace('/payDone');
}
} }
}).catch((err) => { }).catch((err) => {
this.$router.push({name: 'MyOrder'}); this.$router.push({name: 'MyOrder'});

View File

@@ -2,18 +2,14 @@
<div style="background:#fff;"> <div style="background:#fff;">
<BaseHeader></BaseHeader> <BaseHeader></BaseHeader>
<Search style="border-bottom:2px solid red;"></Search> <Search style="border-bottom:2px solid red;"></Search>
<!-- <drawer></drawer> -->
<div class="base-width cate-container"> <div class="base-width cate-container">
<el-breadcrumb> <el-breadcrumb>
<el-breadcrumb-item to="/">首页</el-breadcrumb-item> <el-breadcrumb-item to="/">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ goodsMsg.pointsGoodsCategoryName }}</el-breadcrumb-item> <el-breadcrumb-item>{{ goodsMsg.pointsGoodsCategoryName }}</el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
</div> </div>
<!-- 商品信息展示 -->
<div class="item-detail-show"> <div class="item-detail-show">
<!-- 详情左侧展示数据图片收藏举报 -->
<div class="item-detail-left"> <div class="item-detail-left">
<!-- 大图放大镜 -->
<div class="item-detail-big-img"> <div class="item-detail-big-img">
<el-image <el-image
v-if="goodsSku.thumbnail" v-if="goodsSku.thumbnail"
@@ -29,18 +25,18 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 右侧商品信息活动信息操作展示 -->
<div class="item-detail-right"> <div class="item-detail-right">
<div class="item-detail-title"> <div class="item-detail-title">
<div class="item-detail-title-row">
<p>{{ goodsSku.goodsName }}</p> <p>{{ goodsSku.goodsName }}</p>
<el-tag v-if="isECouponSku" class="goods-type-tag" size="small">电子卡券</el-tag>
</div>
</div> </div>
<div class="sell-point"> <div class="sell-point">
{{ goodsSku.sellingPoint }} {{ goodsSku.sellingPoint }}
</div> </div>
<!-- 商品详细 -->
<div class="item-detail-price-row"> <div class="item-detail-price-row">
<div class="item-price-left"> <div class="item-price-left">
<!-- 商品原价 -->
<div class="item-price-row"> <div class="item-price-row">
<p> <p>
<span class="item-price-title"> &nbsp;&nbsp;&nbsp;&nbsp;</span> <span class="item-price-title"> &nbsp;&nbsp;&nbsp;&nbsp;</span>
@@ -55,11 +51,19 @@
<p>数量</p> <p>数量</p>
</div> </div>
<div class="item-select-row"> <div class="item-select-row">
<el-input-number :min="1" :disabled="goodsSku.quantity === 0" v-model="count"></el-input-number> <el-input-number
<span class="inventory"> 库存{{goodsSku.quantity}}</span> :min="1"
:max="quantityMax"
:disabled="isOutOfStock"
v-model="count"
/>
<span class="inventory"> 库存{{ displayStock }}</span>
</div> </div>
</div> </div>
<div class="item-select" v-if="goodsSku.goodsType !== 'VIRTUAL_GOODS' && goodsSku.weight !== 0"> <div
class="item-select"
v-if="!isECouponSku && goodsSku.goodsType !== 'VIRTUAL_GOODS' && goodsSku.weight !== 0"
>
<div class="item-select-title"> <div class="item-select-title">
<p>重量</p> <p>重量</p>
</div> </div>
@@ -68,12 +72,16 @@
</div> </div>
</div> </div>
<div class="add-buy-car"> <div class="add-buy-car">
<el-button type="danger" :loading="loading" :disabled="goodsSku.quantity === 0" @click="pointBuy">积分购买</el-button> <el-button
type="danger"
:loading="loading"
:disabled="isOutOfStock"
@click="pointBuy"
>积分购买</el-button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 商品详情 -->
<div class="base-width item-intro" ref="itemIntroGoods"> <div class="base-width item-intro" ref="itemIntroGoods">
<div>商品介绍</div> <div>商品介绍</div>
<div v-html="goodsSku.intro" class="mt_10 ml_10" v-if="goodsSku.intro"></div> <div v-html="goodsSku.intro" class="mt_10 ml_10" v-if="goodsSku.intro"></div>
@@ -86,11 +94,19 @@
<script> <script>
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import Search from '@/components/Search'; import Search from "@/components/Search";
import { addCartGoods } from '@/api/cart.js'; import { addCartGoods } from "@/api/cart.js";
import { pointGoodsDetail } from '@/api/promotion'; import { pointGoodsDetail } from "@/api/promotion";
import {
isECoupon,
getECouponStock,
getECouponMaxBuyNum,
E_COUPON_CART_POINTS,
} from "@/constants/goodsType";
export default { export default {
name: 'PointGoodsDetail', name: "PointGoodsDetail",
components: { Search },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
window.scrollTo(0, 0); window.scrollTo(0, 0);
next(); next();
@@ -99,74 +115,106 @@ export default {
this.getGoodsDetail(); this.getGoodsDetail();
}, },
mounted() { mounted() {
window.addEventListener('scroll', this.handleScroll) window.addEventListener("scroll", this.handleScroll);
}, },
data() { data() {
return { return {
goodsMsg: {}, // 商品信息 goodsMsg: {},
goodsSku: {}, // 商品sku goodsSku: {},
isLoading: false, // 加载状态 isLoading: false,
categoryBar: [], // 分类 onceFlag: true,
onceFlag: true, // 只调用一次 count: 1,
count: 1, // 购买商品数量 loading: false,
loading: false // 提交加载状态
}; };
}, },
computed: {
isECouponSku() {
return isECoupon(this.goodsSku.goodsType);
},
displayStock() {
if (this.isECouponSku) {
return getECouponStock(this.goodsSku);
}
return Number(this.goodsSku.quantity) || 0;
},
quantityMax() {
if (this.isECouponSku) {
return getECouponMaxBuyNum(this.goodsSku);
}
const qty = Number(this.goodsSku.quantity) || 0;
return qty > 0 ? qty : 1;
},
isOutOfStock() {
return this.displayStock <= 0;
},
},
methods: { methods: {
// 获取积分商品详情
getGoodsDetail() { getGoodsDetail() {
this.isLoading = true; this.isLoading = true;
pointGoodsDetail(this.$route.query.id).then((res) => { pointGoodsDetail(this.$route.query.id)
.then((res) => {
this.isLoading = false; this.isLoading = false;
if (res.success) { if (res.success) {
this.goodsMsg = res.result; this.goodsMsg = res.result;
this.goodsSku = res.result.goodsSku this.goodsSku = res.result.goodsSku || {};
this.syncCountWithQuantity();
} else { } else {
Message.error(res.message) Message.error(res.message);
this.$router.push('/') this.$router.push("/");
} }
}).catch(() => { })
this.$router.push('/') .catch(() => {
this.$router.push("/");
}); });
}, },
syncCountWithQuantity() {
const max = this.quantityMax;
if (max <= 0) {
this.count = 1;
return;
}
if (this.count > max) this.count = max;
if (this.count < 1) this.count = 1;
},
pointBuy() { pointBuy() {
const params = { const params = {
num: this.count, num: this.count,
skuId: this.goodsMsg.skuId, skuId: this.goodsMsg.skuId,
cartType: 'POINTS' cartType: E_COUPON_CART_POINTS,
}; };
this.loading = true; this.loading = true;
addCartGoods(params).then(res => { addCartGoods(params)
.then((res) => {
this.loading = false; this.loading = false;
if (res.success) { if (res.success) {
this.$router.push({path: '/pay', query: {way: params.cartType}}); this.$router.push({ path: "/pay", query: { way: params.cartType } });
} else { } else {
Message.warning(res.message); Message.warning(res.message);
} }
}).catch(() => { })
.catch(() => {
this.loading = false; this.loading = false;
}); });
}, },
handleScroll () { // 监听页面滚动 handleScroll() {
if (this.onceFlag) { if (this.onceFlag) {
this.$nextTick(() => { this.$nextTick(() => {
this.changeHeight() this.changeHeight();
}); });
this.onceFlag = false this.onceFlag = false;
} }
}, },
changeHeight () { // 设置商品详情高度 changeHeight() {
let goodsDetailCon = document.querySelector('.item-intro') const goodsDetailCon = document.querySelector(".item-intro");
if (!goodsDetailCon || !this.$refs.itemIntroGoods) return;
let heightCss = window.getComputedStyle(goodsDetailCon).height; let heightCss = window.getComputedStyle(goodsDetailCon).height;
heightCss = parseInt(heightCss.substr(0, heightCss.length - 2)) + 89; heightCss = parseInt(heightCss.substr(0, heightCss.length - 2), 10) + 89;
this.$refs.itemIntroGoods.style.height = heightCss + 'px'; this.$refs.itemIntroGoods.style.height = heightCss + "px";
} },
}, },
components: {
Search,
}
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.base-width { .base-width {
width: 1200px; width: 1200px;
@@ -180,7 +228,6 @@ export default {
padding-left: 10px; padding-left: 10px;
margin-top: 10px; margin-top: 10px;
} }
// 商品图片,价格等
.item-detail-show { .item-detail-show {
width: 1200px; width: 1200px;
margin: 0 auto; margin: 0 auto;
@@ -192,7 +239,6 @@ export default {
width: 350px; width: 350px;
margin-right: 30px; margin-right: 30px;
} }
.item-detail-big-img { .item-detail-big-img {
width: 350px; width: 350px;
height: 350px; height: 350px;
@@ -202,12 +248,10 @@ export default {
width: 100%; width: 100%;
} }
} }
.item-detail-img-row { .item-detail-img-row {
margin-top: 15px; margin-top: 15px;
display: flex; display: flex;
} }
.item-detail-img-small { .item-detail-img-small {
width: 68px; width: 68px;
height: 68px; height: 68px;
@@ -219,20 +263,27 @@ export default {
width: 100%; width: 100%;
} }
} }
/*商品选购详情*/
.item-detail-right { .item-detail-right {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.item-detail-title-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.item-detail-title p { .item-detail-title p {
@include content_color($light_content_color); @include content_color($light_content_color);
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 20px;
padding: 8px 0; padding: 8px 0;
margin: 0;
}
.goods-type-tag {
flex-shrink: 0;
} }
.item-detail-express { .item-detail-express {
font-size: 14px; font-size: 14px;
padding: 2px 3px; padding: 2px 3px;
@@ -240,15 +291,6 @@ export default {
background-color: $theme_color; background-color: $theme_color;
color: #fff; color: #fff;
} }
/*商品标签*/
.item-detail-tag {
padding: 8px 0;
font-size: 12px;
color: $theme_color;
}
/*价格详情等*/
.item-detail-price-row { .item-detail-price-row {
padding: 10px; padding: 10px;
display: flex; display: flex;
@@ -256,39 +298,28 @@ export default {
justify-content: space-between; justify-content: space-between;
background: url("../../assets/images/goodsDetail/price-bg.png"); background: url("../../assets/images/goodsDetail/price-bg.png");
} }
.item-price-left { .item-price-left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.item-price-title { .item-price-title {
color: #999999; color: #999999;
font-size: 14px; font-size: 14px;
margin-right: 15px; margin-right: 15px;
} }
.item-price-row { .item-price-row {
margin: 5px 0px; margin: 5px 0px;
} }
.item-price { .item-price {
color: $theme_color; color: $theme_color;
font-size: 20px; font-size: 20px;
cursor: pointer; cursor: pointer;
} }
.item-price-old {
color: gray;
text-decoration: line-through;
font-size: 14px;
margin-left: 5px;
}
.add-buy-car-box { .add-buy-car-box {
width: 100%; width: 100%;
margin-top: 15px; margin-top: 15px;
border-top: 1px dotted $border_color; border-top: 1px dotted $border_color;
} }
.add-buy-car { .add-buy-car {
margin-top: 15px; margin-top: 15px;
> * { > * {
@@ -300,54 +331,20 @@ export default {
flex-direction: row; flex-direction: row;
margin-top: 15px; margin-top: 15px;
} }
.item-select-title { .item-select-title {
@include content_color($light_content_color); @include content_color($light_content_color);
font-size: 14px; font-size: 14px;
margin-right: 15px; margin-right: 15px;
width: 60px; width: 60px;
} }
.item-select-column {
display: flex;
flex-wrap: wrap;
flex: 1;
}
.item-select-row { .item-select-row {
margin-bottom: 8px; margin-bottom: 8px;
} }
.item-select-box {
display: flex;
flex-direction: row;
align-items: center;
padding: 5px;
margin-right: 8px;
@include background_color($light_background_color);
border: 0.5px solid $border_color;
cursor: pointer;
@include content_color($light_content_color);
}
.item-select-box:hover {
border: 0.5px solid $theme_color;
}
.item-select-box-active {
border: 0.5px solid $theme_color;
}
.item-select-intro p {
margin: 0px;
padding: 5px;
}
.sell-point { .sell-point {
font-size: 12px; font-size: 12px;
color: red; color: red;
margin-bottom: 5px; margin-bottom: 5px;
} }
// 商品详情
.item-intro { .item-intro {
margin-top: 10px; margin-top: 10px;
> div:nth-child(1) { > div:nth-child(1) {

View File

@@ -45,7 +45,15 @@
<div class="wap-sku"> <div class="wap-sku">
{{ item.goodsUnit }} {{ item.goodsUnit }}
<el-tag <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'" :type="item.salesModel === 'RETAIL' ? 'info' : 'primary'"
> >
{{ item.salesModel === "RETAIL" ? "零售型" : "批发型" }} {{ item.salesModel === "RETAIL" ? "零售型" : "批发型" }}
@@ -53,6 +61,9 @@
</div> </div>
<div class="wap-content-desc-bottom"> <div class="wap-content-desc-bottom">
<div>{{ $filters.unitPrice(item.price) }}</div> <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> </div>
</div> </div>
@@ -75,6 +86,7 @@
</template> </template>
<script> <script>
import * as API_Goods from "@/api/goods"; import * as API_Goods from "@/api/goods";
import { goodsTypeLabel, goodsTypeTagType } from "@/constants/goodsType";
export default { export default {
props: { props: {
selectedWay: { selectedWay: {
@@ -129,6 +141,8 @@ export default {
this.init(); this.init();
}, },
methods: { methods: {
goodsTypeLabel,
goodsTypeTagType,
onSearchGoods() { onSearchGoods() {
this.goodsData = []; this.goodsData = [];
this.goodsParams.pageNumber = 1; this.goodsParams.pageNumber = 1;
@@ -270,7 +284,9 @@ export default {
} }
</style> </style>
<style lang="scss"> <style lang="scss">
.goods-dialog-cascader-popper { .pool-stock-hint {
z-index: 10001 !important; font-size: 12px;
color: #999;
margin-top: 4px;
} }
</style> </style>

View File

@@ -10,6 +10,7 @@
export const CARD_KEY_STATUS_TEXT = { export const CARD_KEY_STATUS_TEXT = {
UNUSED: "未使用", UNUSED: "未使用",
ALLOCATED: "已分配", ALLOCATED: "已分配",
RESERVED: "已预占",
VOIDED: "已作废", VOIDED: "已作废",
}; };

View File

@@ -0,0 +1,35 @@
/**
* 卡密商品E_COUPON— 平台端公共常量
*
* @author Mike
* @date 2026-08-02
*/
export const E_COUPON_GOODS_TYPE = "E_COUPON";
export function isECoupon(goodsType) {
return goodsType === E_COUPON_GOODS_TYPE;
}
export function goodsTypeLabel(goodsType) {
const map = {
E_COUPON: "电子卡券",
VIRTUAL_GOODS: "虚拟商品",
PHYSICAL_GOODS: "实物商品",
};
return map[goodsType] || goodsType || "—";
}
export function goodsTypeTagType(goodsType) {
if (goodsType === E_COUPON_GOODS_TYPE) return "warning";
if (goodsType === "VIRTUAL_GOODS") return "info";
return "";
}
export function eCouponStockHint(goodsType) {
if (!isECoupon(goodsType)) return "";
return "库存来自卡池同步quantity";
}
export function promotionPriceMin(goodsType) {
return isECoupon(goodsType) ? 0 : 0.01;
}

View File

@@ -105,6 +105,7 @@
import { saveKanJiaActivityGoods } from "@/api/promotion"; import { saveKanJiaActivityGoods } from "@/api/promotion";
import { regular } from "@/utils"; import { regular } from "@/utils";
import skuSelect from "@/components/lili-dialog"; import skuSelect from "@/components/lili-dialog";
import { isECoupon } from "@/constants/goodsType";
export default { export default {
name: "add-kanjia-activity-goods", name: "add-kanjia-activity-goods",
@@ -175,6 +176,7 @@ export default {
let checkResult = true; let checkResult = true;
this.form.promotionGoodsList.forEach((res) => { this.form.promotionGoodsList.forEach((res) => {
const eCoupon = isECoupon(res.goodsType);
if (res.stock <= 0 || res.stock > res.quantity) { if (res.stock <= 0 || res.stock > res.quantity) {
checkResult = false; checkResult = false;
this.$Message.error("活动库存不能为0且不能超过商品库存"); this.$Message.error("活动库存不能为0且不能超过商品库存");
@@ -186,9 +188,16 @@ export default {
this.$Message.error("结算价格金额格式不正确"); this.$Message.error("结算价格金额格式不正确");
return; return;
} }
if (res.settlementPrice < 0 || res.settlementPrice > res.price) { if (
(eCoupon ? res.settlementPrice < 0 : res.settlementPrice <= 0) ||
res.settlementPrice > res.price
) {
checkResult = false; checkResult = false;
this.$Message.error("结算价格金额不能为0且不能超过商品价格"); this.$Message.error(
eCoupon
? "结算价格不能小于0且不能超过商品价格"
: "结算价格金额不能为0且不能超过商品价格"
);
return; return;
} }
if (!regular.money.test(res.highestPrice)) { if (!regular.money.test(res.highestPrice)) {
@@ -206,9 +215,9 @@ export default {
this.$Message.error("最低可砍金额格式错误"); this.$Message.error("最低可砍金额格式错误");
return; return;
} }
if (res.lowestPrice <= 0 || res.lowestPrice > res.price) { if (res.lowestPrice < 0 || (!eCoupon && res.lowestPrice <= 0) || res.lowestPrice > res.price) {
checkResult = false; checkResult = false;
this.$Message.error("最低可砍金额不能为0"); this.$Message.error(eCoupon ? "最低可砍金额不能小于0" : "最低可砍金额不能为0");
return; return;
} }
if (parseInt(res.lowestPrice) > parseInt(res.highestPrice)) { if (parseInt(res.lowestPrice) > parseInt(res.highestPrice)) {
@@ -277,6 +286,7 @@ export default {
storeId: e.storeId, storeId: e.storeId,
storeName: e.storeName, storeName: e.storeName,
skuId: e.id, skuId: e.id,
goodsType: e.goodsType,
}); });
}); });
this.form.promotionGoodsList = list; this.form.promotionGoodsList = list;

View File

@@ -90,6 +90,7 @@
<script> <script>
import { getKanJiaActivityGoodsById, editKanJiaActivityGoods } from "@/api/promotion"; import { getKanJiaActivityGoodsById, editKanJiaActivityGoods } from "@/api/promotion";
import { regular } from "@/utils"; import { regular } from "@/utils";
import { isECoupon } from "@/constants/goodsType";
export default { export default {
name: "editKanjiaActivityGoods", name: "editKanjiaActivityGoods",
@@ -200,6 +201,8 @@ export default {
params.endTime = this.$filters.unixToDate(endDate.getTime() / 1000); params.endTime = this.$filters.unixToDate(endDate.getTime() / 1000);
delete params.rangeTime; delete params.rangeTime;
const eCoupon = isECoupon(params.goodsType || params.goodsSku?.goodsType);
if (params.stock <= 0 || params.stock > params.goodsSku.quantity) { if (params.stock <= 0 || params.stock > params.goodsSku.quantity) {
this.$Message.error("活动库存不能为0且不能超过商品库存"); this.$Message.error("活动库存不能为0且不能超过商品库存");
return; return;
@@ -208,8 +211,15 @@ export default {
this.$Message.error("结算价格金额格式不正确"); this.$Message.error("结算价格金额格式不正确");
return; return;
} }
if (params.settlementPrice < 0 || params.settlementPrice > params.price) { if (
this.$Message.error("结算价格金额不能为0且不能超过商品价格"); (eCoupon ? params.settlementPrice < 0 : params.settlementPrice <= 0) ||
params.settlementPrice > params.price
) {
this.$Message.error(
eCoupon
? "结算价格不能小于0且不能超过商品价格"
: "结算价格金额不能为0且不能超过商品价格"
);
return; return;
} }
if (!regular.money.test(params.highestPrice)) { if (!regular.money.test(params.highestPrice)) {
@@ -224,8 +234,12 @@ export default {
this.$Message.error("最低可砍金额格式错误"); this.$Message.error("最低可砍金额格式错误");
return; return;
} }
if (params.lowestPrice <= 0 || params.lowestPrice > params.price) { if (
this.$Message.error("最低可砍金额不能为0"); params.lowestPrice < 0 ||
(!eCoupon && params.lowestPrice <= 0) ||
params.lowestPrice > params.price
) {
this.$Message.error(eCoupon ? "最低可砍金额不能小于0" : "最低可砍金额不能为0");
return; return;
} }
if (params.lowestPrice > params.highestPrice) { if (params.lowestPrice > params.highestPrice) {

View File

@@ -23,6 +23,11 @@
> >
<el-table-column type="selection" width="60" align="center" /> <el-table-column type="selection" width="60" align="center" />
<el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip />
<el-table-column label="类型" width="100">
<template #default="{ row }">
<span v-if="row">{{ goodsTypeLabel(row.goodsType) }}</span>
</template>
</el-table-column>
<el-table-column label="SKU编码" min-width="120"> <el-table-column label="SKU编码" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row">{{ row.skuId }}</span> <span v-if="row">{{ row.skuId }}</span>
@@ -117,6 +122,7 @@
<script> <script>
import { addPointsGoods, getPointsGoodsCategoryList } from "@/api/promotion"; import { addPointsGoods, getPointsGoodsCategoryList } from "@/api/promotion";
import skuSelect from "@/components/lili-dialog"; import skuSelect from "@/components/lili-dialog";
import { isECoupon, goodsTypeLabel } from "@/constants/goodsType";
export default { export default {
name: "addPoinsGoods", name: "addPoinsGoods",
@@ -160,6 +166,7 @@ export default {
await this.getCategory(); await this.getCategory();
}, },
methods: { methods: {
goodsTypeLabel,
async getCategory() { async getCategory() {
let res = await getPointsGoodsCategoryList(); let res = await getPointsGoodsCategoryList();
this.categoryList = res.result.records; this.categoryList = res.result.records;
@@ -172,6 +179,13 @@ export default {
this.$Modal.warning({ title: "提示", content: "请选择指定商品" }); this.$Modal.warning({ title: "提示", content: "请选择指定商品" });
return; return;
} }
for (const row of params) {
const stock = Number(row.quantity) || 0;
if (isECoupon(row.goodsType) && Number(row.activeStock) > stock) {
this.$Message.error(`${row.goodsName}】活动库存不能超过卡池库存(${stock}`);
return;
}
}
this.form.startTime = this.form.rangeTime[0]; this.form.startTime = this.form.rangeTime[0];
this.form.endTime = this.form.rangeTime[1]; this.form.endTime = this.form.rangeTime[1];
const start = this.$filters.unixToDate(this.form.startTime / 1000); const start = this.$filters.unixToDate(this.form.startTime / 1000);
@@ -251,6 +265,7 @@ export default {
quantity: e.quantity || "", quantity: e.quantity || "",
storeName: e.storeName || "", storeName: e.storeName || "",
price: e.price || "", price: e.price || "",
goodsType: e.goodsType,
}; };
list.push(obj); list.push(obj);
}); });

View File

@@ -11,18 +11,21 @@
export const CARD_KEY_STATUS = { export const CARD_KEY_STATUS = {
UNUSED: "UNUSED", UNUSED: "UNUSED",
ALLOCATED: "ALLOCATED", ALLOCATED: "ALLOCATED",
RESERVED: "RESERVED",
VOIDED: "VOIDED", VOIDED: "VOIDED",
}; };
export const CARD_KEY_STATUS_TEXT = { export const CARD_KEY_STATUS_TEXT = {
UNUSED: "未使用", UNUSED: "未使用",
ALLOCATED: "已分配", ALLOCATED: "已分配",
RESERVED: "已预占",
VOIDED: "已作废", VOIDED: "已作废",
}; };
export const CARD_KEY_STATUS_TAG = { export const CARD_KEY_STATUS_TAG = {
UNUSED: "success", UNUSED: "success",
ALLOCATED: "info", ALLOCATED: "info",
RESERVED: "warning",
VOIDED: "danger", VOIDED: "danger",
}; };
@@ -30,6 +33,7 @@ export const CARD_KEY_STATUS_TAG = {
export const CARD_KEY_STATUS_TABS = [ export const CARD_KEY_STATUS_TABS = [
{ key: "", label: "全部" }, { key: "", label: "全部" },
{ key: CARD_KEY_STATUS.UNUSED, label: "未使用" }, { key: CARD_KEY_STATUS.UNUSED, label: "未使用" },
{ key: CARD_KEY_STATUS.RESERVED, label: "已预占" },
{ key: CARD_KEY_STATUS.ALLOCATED, label: "已分配" }, { key: CARD_KEY_STATUS.ALLOCATED, label: "已分配" },
{ key: CARD_KEY_STATUS.VOIDED, label: "已作废" }, { key: CARD_KEY_STATUS.VOIDED, label: "已作废" },
]; ];

View File

@@ -0,0 +1,152 @@
/**
* 卡密商品E_COUPON— 商家端公共常量
*
* @author Mike
* @date 2026-08-02
*/
export const E_COUPON_GOODS_TYPE = "E_COUPON";
export function isECoupon(goodsType) {
return goodsType === E_COUPON_GOODS_TYPE;
}
export function goodsTypeLabel(goodsType) {
const map = {
E_COUPON: "电子卡券",
VIRTUAL_GOODS: "虚拟商品",
PHYSICAL_GOODS: "实物商品",
};
return map[goodsType] || goodsType || "—";
}
export function goodsTypeTagType(goodsType) {
if (goodsType === E_COUPON_GOODS_TYPE) return "warning";
if (goodsType === "VIRTUAL_GOODS") return "info";
return "";
}
/** E_COUPON 活动库存提示 */
export function eCouponStockHint(goodsType) {
if (!isECoupon(goodsType)) return "";
return "库存来自卡池同步quantity";
}
/** 活动价下限E_COUPON 允许 0 元 */
export function promotionPriceMin(goodsType) {
return isECoupon(goodsType) ? 0 : 0.01;
}
/** 活动价下限E_COUPON 允许 0 元 */
export function promotionPriceMin(goodsType) {
return isECoupon(goodsType) ? 0 : 0.01;
}
export const CARD_KEY_FULFILL_STATUS = {
PENDING: "PENDING",
DELIVERED: "DELIVERED",
FAILED: "FAILED",
NOT_APPLICABLE: "NOT_APPLICABLE",
};
export const CARD_KEY_FULFILL_STATUS_TEXT = {
PENDING: "待发放",
DELIVERED: "已发放",
FAILED: "发放失败",
NOT_APPLICABLE: "不适用",
};
export function cardKeyFulfillAlertType(status) {
const map = { PENDING: "info", DELIVERED: "success", FAILED: "warning" };
return map[status] || "info";
}
export function formatCardKeyFulfillStatus(status) {
return CARD_KEY_FULFILL_STATUS_TEXT[status] || status || "—";
}
export function resolveCardKeyFulfillMessage(line) {
if (line && line.message) return line.message;
return formatCardKeyFulfillStatus(line?.status);
}
export function isCardKeyFulfillApplicable(item) {
if (!item) return false;
const status = item.cardKeyFulfillStatus;
if (status) return status !== CARD_KEY_FULFILL_STATUS.NOT_APPLICABLE;
if (item.cardKeyDelivered != null || (item.cardKeys && item.cardKeys.length)) return true;
return item.goodsType === E_COUPON_GOODS_TYPE;
}
function normalizeCardKeyFulfillLine(source) {
const status =
source.cardKeyFulfillStatus ||
(source.cardKeyDelivered
? CARD_KEY_FULFILL_STATUS.DELIVERED
: CARD_KEY_FULFILL_STATUS.PENDING);
const cardKeys =
status === CARD_KEY_FULFILL_STATUS.DELIVERED && source.cardKeyDelivered
? source.cardKeys || []
: [];
return {
key: source.sn || source.orderSn || source.id || source.skuId,
goodsName: source.goodsName,
status,
message: source.cardKeyFulfillMessage,
cardKeys,
isGift: !!source.isGift,
};
}
export function collectCardKeyFulfillLines(orderItems = [], giftSummaries = []) {
const lines = [];
(orderItems || []).forEach((item) => {
if (!isCardKeyFulfillApplicable(item)) return;
lines.push(normalizeCardKeyFulfillLine(item));
});
(giftSummaries || []).forEach((gift) => {
lines.push(normalizeCardKeyFulfillLine({ ...gift, isGift: true }));
});
return lines;
}
export function orderHasCardKeySection({ orderItems, giftSummaries, isECouponOrder } = {}) {
if (isECouponOrder) return true;
if (Array.isArray(giftSummaries) && giftSummaries.length > 0) return true;
if (collectCardKeyFulfillLines(orderItems, []).length > 0) return true;
return (orderItems || []).some(
(item) =>
item.goodsType === E_COUPON_GOODS_TYPE ||
(Array.isArray(item.cardKeys) && item.cardKeys.length > 0)
);
}
export function flattenCardKeyFulfillLines(lines, withGoodsName = false) {
const rows = [];
(lines || []).forEach((line) => {
if (line.status !== CARD_KEY_FULFILL_STATUS.DELIVERED) return;
(line.cardKeys || []).forEach((ck) => {
rows.push(withGoodsName ? { ...ck, goodsName: line.goodsName } : { ...ck });
});
});
return rows;
}
/** 订单是否含已交付卡密(含满赠子单) */
export function orderHasCardKeyContent(orderItems, giftSummaries) {
return flattenCardKeyFulfillLines(collectCardKeyFulfillLines(orderItems, giftSummaries)).length > 0;
}
export function flattenOrderCardKeys(orderItems, withGoodsName = false, giftSummaries) {
const lines = collectCardKeyFulfillLines(orderItems, giftSummaries);
if (lines.some((line) => line.status)) {
return flattenCardKeyFulfillLines(lines, withGoodsName);
}
const rows = [];
(orderItems || []).forEach((item) => {
if (!item.cardKeyDelivered || !item.cardKeys || !item.cardKeys.length) return;
item.cardKeys.forEach((ck) => {
rows.push(withGoodsName ? { ...ck, goodsName: item.goodsName } : { ...ck });
});
});
return rows;
}

View File

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

View File

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

View File

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

View File

@@ -257,25 +257,40 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- E_COUPON卡密来自订单详情 orderItems[].cardKeysS-09无独立 API-S-06 --> <!-- 卡密信息E_COUPON 主单或满赠子单 cardKeyFulfillStatus 为准 -->
<div v-if="isECouponOrder" class="ecoupon-card-keys mt_10"> <div v-if="hasCardKeySection" class="ecoupon-card-keys mt_10">
<h4>卡密信息</h4> <h4>卡密信息</h4>
<div
v-for="line in cardKeyFulfillLines"
:key="line.key"
class="ecoupon-fulfill-block"
>
<div v-if="cardKeyFulfillLines.length > 1" class="ecoupon-fulfill-title mb_10">
{{ line.goodsName || (line.isGift ? "满赠电子卡券" : "电子卡券") }}
</div>
<el-alert <el-alert
v-if="!ecouponCardKeyDelivered" v-if="line.status !== 'DELIVERED'"
type="info" :type="cardKeyFulfillAlertType(line.status)"
show-icon show-icon
:closable="false" :closable="false"
title="卡密尚未发放" :title="resolveCardKeyFulfillMessage(line)"
class="mb_10" class="mb_10"
/> />
<el-table <el-table
v-else-if="ecouponCardKeyRows.length" v-else-if="line.cardKeys.length"
border border
:data="ecouponCardKeyRows" :data="lineCardKeyRows(line)"
style="width: 100%" style="width: 100%"
class="mb_10"
> >
<el-table-column type="index" label="序号" width="60" align="center" /> <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
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="cardNo" label="卡号" min-width="140" show-overflow-tooltip />
<el-table-column prop="cardSecret" label="卡密" min-width="120" 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 prop="allocatedTime" label="发卡时间" width="170" />
@@ -291,6 +306,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
</div>
<div class="goods-total"> <div class="goods-total">
<ul> <ul>
@@ -731,6 +747,13 @@ import multipleMap from "@/views/my-components/map/multiple-map";
import vueQr from "vue-qr"; import vueQr from "vue-qr";
import { printElement } from "@/utils/print"; import { printElement } from "@/utils/print";
import { formatCardKeyStatus } from "@/constants/cardKey"; import { formatCardKeyStatus } from "@/constants/cardKey";
import {
collectCardKeyFulfillLines,
orderHasCardKeySection,
flattenOrderCardKeys,
cardKeyFulfillAlertType,
resolveCardKeyFulfillMessage,
} from "@/constants/goodsType";
export default { export default {
name: "orderDetail", name: "orderDetail",
components: { components: {
@@ -863,20 +886,18 @@ export default {
isNonPhysicalOrder() { isNonPhysicalOrder() {
return this.isVirtualOrder || this.isECouponOrder; return this.isVirtualOrder || this.isECouponOrder;
}, },
ecouponCardKeyDelivered() { hasCardKeySection() {
return (this.data || []).some((item) => item.cardKeyDelivered); return orderHasCardKeySection({
orderItems: this.data,
giftSummaries: this.orderInfo.giftECouponOrders,
isECouponOrder: this.isECouponOrder,
});
},
cardKeyFulfillLines() {
return collectCardKeyFulfillLines(this.data, this.orderInfo.giftECouponOrders);
}, },
ecouponCardKeyRows() { ecouponCardKeyRows() {
const rows = []; return flattenOrderCardKeys(this.data, true, this.orderInfo.giftECouponOrders);
(this.data || []).forEach((item) => {
(item.cardKeys || []).forEach((ck) => {
rows.push({
...ck,
goodsName: item.goodsName,
});
});
});
return rows;
}, },
canPartDelivery() { canPartDelivery() {
if (!this.allowOperation.ship) return false; if (!this.allowOperation.ship) return false;
@@ -910,6 +931,14 @@ export default {
}, },
methods: { methods: {
formatCardKeyStatus, formatCardKeyStatus,
cardKeyFulfillAlertType,
resolveCardKeyFulfillMessage,
lineCardKeyRows(line) {
return (line.cardKeys || []).map((ck) => ({
...ck,
goodsName: line.goodsName,
}));
},
copyCardKey(row) { copyCardKey(row) {
const text = `卡号:${row.cardNo || ""}\n卡密${row.cardSecret || ""}`; const text = `卡号:${row.cardNo || ""}\n卡密${row.cardSecret || ""}`;
if (navigator.clipboard && navigator.clipboard.writeText) { 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 prop="originalPrice" label="原价" width="90" align="center" />
<el-table-column label="活动价" width="150" align="center"> <el-table-column label="活动价" width="150" align="center">
<template #default="{ row }"> <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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -50,6 +55,7 @@
<script> <script>
import { getFlashDiscountDetail, saveFlashDiscount, editFlashDiscount } from "@/api/promotion.js"; import { getFlashDiscountDetail, saveFlashDiscount, editFlashDiscount } from "@/api/promotion.js";
import { promotionPriceMin } from "@/constants/goodsType";
export default { export default {
data() { data() {
@@ -75,6 +81,7 @@ export default {
} }
}, },
methods: { methods: {
promotionPriceMin,
openGoods() { openGoods() {
this.$refs.liliDialog.open("goods"); this.$refs.liliDialog.open("goods");
}, },
@@ -88,6 +95,7 @@ export default {
originalPrice: g.price, originalPrice: g.price,
price: g.price, price: g.price,
quantity: g.quantity, quantity: g.quantity,
goodsType: g.goodsType,
}); });
} }
}); });

View File

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

View File

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

View File

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