feat(coupon): 支持立减优惠券并统一营销管理页面

This commit is contained in:
pikachu1995@126.com
2026-11-04 14:50:33 +08:00
parent cbaf89c835
commit d521587ad8
18 changed files with 1739 additions and 991 deletions

View File

@@ -162,7 +162,7 @@ lilishop-ui
## 8. 升级计划
> 更新日期:2026-11-03,依据各端近期提交及当前代码实现同步。
> 更新日期:2026-11-04,依据各端近期提交及当前代码实现同步。
### ✅ 已完成特性
| 功能模块 | 功能模块 |
@@ -173,7 +173,7 @@ lilishop-ui
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
| ✅ 卡密商品 | ✅ 二级分销 |
| ✅ 定时任务管理(数据库配置驱动) | ✅ 预约服务商品 |
| ✅ 主题色管理 | |
| ✅ 主题色管理 | ✅ 立减优惠券 |
### 🔜 近期规划中
- 🔜 微信小店对接

View File

@@ -39,7 +39,7 @@
<div>
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{ $filters.unitPrice(coupon.price) }}</span></span>
<span v-if="coupon.couponType === 'DISCOUNT'" class="fontsize_12 global_color"><span class="price">{{coupon.couponDiscount}}</span>折</span>
<span class="describe">满{{coupon.consumeThreshold}}元可用</span>
<span class="describe">{{ coupon.consumeThreshold > 0 ? '满' + coupon.consumeThreshold + '元可用' : '无门槛使用' }}</span>
</div>
<p>使用范围:{{useScope(coupon.scopeType, coupon.storeName)}}</p>
<p>有效期:{{coupon.endTime}}</p>

View File

@@ -100,7 +100,7 @@
@click="handleReceiveCoupon(item.id)"
>
<span v-if="item.couponType == 'PRICE'"
>满{{ item.consumeThreshold }}减{{ item.price }}</span>
>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '减' + item.price : '无门槛,立减' + item.price + '元' }}</span>
<span v-if="item.couponType == 'DISCOUNT'"
>满{{ item.consumeThreshold }}打{{
item.couponDiscount
@@ -117,7 +117,7 @@
@click="handleReceiveCoupon(item.id)"
>
<span v-if="item.couponType == 'PRICE'"
>满{{ item.consumeThreshold }}减{{ item.price }}</span>
>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '减' + item.price : '无门槛,立减' + item.price + '元' }}</span>
<span v-if="item.couponType == 'DISCOUNT'"
>满{{ item.consumeThreshold }}打{{
item.couponDiscount

View File

@@ -75,7 +75,7 @@
>¥{{ item.price }}</span>
<span v-if="item.couponType === 'DISCOUNT'"
>{{ item.couponDiscount }}折</span>
<span>满{{ item.consumeThreshold }}元可用</span>
<span>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
<el-button
class="coupon-btn"
size="small"

View File

@@ -11,7 +11,7 @@
<el-card shadow="never" v-for="(item, index) in autoCoupList" :key="index" style="margin: 10px 15px">
<span v-if="item.couponType === 'PRICE'" class="fontsize_12 global_color" style="font-size: 15px;">¥<span class="price">{{ $filters.unitPrice(item.price) }}</span></span>
<span v-if="item.couponType === 'DISCOUNT'" class="fontsize_12 global_color" style="font-size: 15px;"><span class="price">{{item.discount}}</span>折</span>
<span class="describe" style="font-size: 15px;margin-left: 5px;">满{{item.consumeThreshold}}元可用</span>
<span class="describe" style="font-size: 15px;margin-left: 5px;">{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
<p style="font-size: 14px;">使用范围:{{useScope(item.scopeType, item.storeName)}}</p>
<p style="font-size: 14px;">有效期:{{item.endTime}}</p>
</el-card>

View File

@@ -23,7 +23,7 @@
v-if="item.couponType === 'DISCOUNT'"
class="global_color coupon-amount"
><span class="price">{{ item.discount }}</span>折</span>
<span class="describe">满{{ item.consumeThreshold }}元可用</span>
<span class="describe">{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
</div>
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
<p class="coupon-expire">有效期:{{ formatValidity(item) }}</p>

View File

@@ -202,7 +202,7 @@
<template v-else>{{ item.discount }}折</template>
</div>
<div class="pay-coupon-info">
<p>满{{ item.consumeThreshold }}元可用</p>
<p>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</p>
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
<p>有效期:{{ item.endTime }}</p>
</div>

View File

@@ -2,29 +2,12 @@
<div class="search member-benefit">
<el-card shadow="never" class="table-card">
<el-card>
<template #header>
<div class="card-header-bar">
<div class="header-title">
<span>客户权益列表</span>
</div>
<div class="header-actions">
<el-button size="small" @click="getData">刷新</el-button>
<el-button type="primary" size="small" @click="openAdd">添加权益设置</el-button>
</div>
</div>
</template>
<div class="operation padding-row benefit-actions">
<el-button type="primary" @click="openAdd">添加权益设置</el-button>
<el-button @click="getData">刷新</el-button>
</div>
@@ -33,16 +16,14 @@
<el-table
v-loading="loading"
stripe
border
:data="data"
empty-text="暂无数据"
class="custom-benefit-table"
class="custom-benefit-table mt_10"
row-key="id"
style="width: 100%"
@@ -50,13 +31,13 @@
<!-- 权益ID -->
<el-table-column prop="id" label="权益ID" min-width="190" show-overflow-tooltip />
<el-table-column prop="id" label="权益ID" min-width="150" show-overflow-tooltip />
<!-- 权益LOGO -->
<el-table-column label="权益LOGO" width="86" align="center">
<el-table-column label="权益LOGO" width="100" align="center">
<template #default="{ row }">
@@ -96,7 +77,7 @@
<!-- 权益名称 -->
<el-table-column prop="benefitName" label="权益名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="benefitName" label="权益名称" min-width="150" show-overflow-tooltip />
@@ -108,7 +89,7 @@
<!-- 权益类型 -->
<el-table-column label="权益类型" width="140" align="center">
<el-table-column label="权益类型" width="120" align="center">
<template #default="{ row }">
@@ -122,13 +103,13 @@
<!-- 排序 -->
<el-table-column prop="benefitSort" label="排序" width="80" align="center" />
<el-table-column prop="benefitSort" label="排序" width="70" align="center" />
<!-- 状态 -->
<el-table-column label="状态" width="110" align="center">
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
@@ -158,7 +139,7 @@
<!-- 操作 -->
<el-table-column label="操作" width="140" align="center" fixed="right">
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
@@ -484,13 +465,15 @@
</el-form>
<div class="benefit-drawer-footer-btns">
<template #footer>
<div class="benefit-drawer-footer-btns">
<el-button @click="addFlag = false">取消</el-button>
<el-button type="primary" :loading="submitAddLoading" @click="submitAdd">提交</el-button>
</div>
</div>
</template>
</el-drawer>
@@ -724,13 +707,15 @@
</el-form>
<div class="benefit-drawer-footer-btns">
<template #footer>
<div class="benefit-drawer-footer-btns">
<el-button @click="editFlag = false">取消</el-button>
<el-button type="primary" :loading="submitEditLoading" @click="submitEdit">提交</el-button>
</div>
</div>
</template>
</el-drawer>
@@ -1770,47 +1755,15 @@ onMounted(() => {
<style lang="scss" scoped>
.card-header-bar {
.benefit-actions {
display: flex;
justify-content: space-between;
align-items: center;
.header-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 15px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
flex-wrap: wrap;
gap: 10px;
}
.benefit-actions .el-button + .el-button {
margin-left: 0;
}
.font-mono {
@@ -1913,21 +1866,13 @@ onMounted(() => {
.benefit-drawer-footer-btns {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #e8eaec;
text-align: right;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.benefit-drawer-footer-btns .el-button + .el-button {
margin-left: 8px;
margin-left: 0;
}
.link-text {
@@ -1953,6 +1898,21 @@ onMounted(() => {
<style lang="scss">
.benefit-form-drawer {
max-width: 100vw;
.el-drawer__header {
margin-bottom: 0;
padding: 20px 24px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.el-drawer__body { padding: 24px; }
.el-drawer__footer {
padding: 16px 24px;
border-top: 1px solid var(--el-border-color-lighter);
}
.el-form-item__content { min-width: 0; }
}
.benefit-logo-thumb {

View File

@@ -10,7 +10,7 @@
<span class="val">{{ cp.price }}</span>
</div>
<div class="right-info">
<span class="title">满{{ cp.consumeThreshold }}可用</span>
<span class="title">{{ cp.consumeThreshold > 0 ? '满' + cp.consumeThreshold + '可用' : '无门槛使用' }}</span>
<span class="scope" v-if="cp.title">{{ cp.title }}</span>
<span class="btn">立即领取</span>
</div>

View File

@@ -91,7 +91,7 @@
<el-table-column label="优惠券类型" width="130" align="center">
<template #default="{ row }">
<el-tag v-if="row" size="small" effect="light" :type="row.couponType === 'DISCOUNT' ? 'warning' : 'danger'">
{{ couponTypeText(row.couponType) }}
{{ couponTypeText(row) }}
</el-tag>
</template>
</el-table-column>
@@ -181,9 +181,9 @@ onMounted(() => {
getCouponActivity();
});
function couponTypeText(v) {
if (v === "DISCOUNT") return "打折券";
if (v === "PRICE") return "减免现金券";
function couponTypeText(row) {
if (row.couponType === "DISCOUNT") return "满折优惠券";
if (row.couponType === "PRICE") return Number(row.consumeThreshold) === 0 ? "立减优惠券" : "满减优惠券";
return "未知类型";
}

View File

@@ -25,13 +25,6 @@
/>
</el-form-item>
<el-form-item label="优惠券活动类型" prop="couponActivityType">
<el-radio-group v-model="form.couponActivityType">
<el-radio-button value="REGISTERED">新人发券</el-radio-button>
<el-radio-button value="SPECIFY">精确发券</el-radio-button>
<el-radio-button value="AUTO_COUPON">自动赠券</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item
label="领取频率"
v-if="form.couponActivityType === 'AUTO_COUPON'"
@@ -170,8 +163,8 @@
</template>
<script setup>
import { ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { ref, reactive, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import couponTemplate from "@/views/promotions/coupon/coupon";
import userList from "@/views/member/list/index";
import { saveActivityCoupon } from "@/api/promotion";
@@ -182,6 +175,7 @@ import { unixToDate } from "@/utils/filters";
defineOptions({ name: "add-coupon-activity" });
const router = useRouter();
const route = useRoute();
const appStore = useAppStore();
const options = reactive({
@@ -205,6 +199,15 @@ const form = reactive({
couponActivityItems: [],
couponFrequencyEnum: "",
});
watch(
() => route.query.couponActivityType,
(type) => {
if (["REGISTERED", "SPECIFY", "AUTO_COUPON"].includes(type)) {
form.couponActivityType = type;
}
},
{ immediate: true }
);
const submitLoading = ref(false);
const selectCouponList = ref([]);
const formRule = reactive({

View File

@@ -2,7 +2,9 @@
<div class="search">
<el-card>
<div class="operation padding-row">
<el-button type="primary" @click="add">添加活动</el-button>
<el-button type="primary" plain @click="addRegistered">注册赠券</el-button>
<el-button type="primary" plain @click="addSpecify">精准发券</el-button>
<el-button type="primary" plain @click="addAutoCoupon">自动赠券</el-button>
</div>
<el-table
@@ -105,7 +107,7 @@ function activityTypeText(type) {
REGISTERED: "注册赠券",
AUTO_COUPON: "自动发券",
};
return map[type] || "精确发券";
return map[type] || "精准发券";
}
function activityScopeText(scope) {
return scope === "DESIGNATED" ? "指定会员" : "全部会员";
@@ -145,8 +147,23 @@ function promotionStatusTagType(status) {
function init() {
getDataList();
}
function add() {
router.push({ name: "add-coupon-activity" });
function addRegistered() {
router.push({
name: "add-coupon-activity",
query: { couponActivityType: "REGISTERED" },
});
}
function addSpecify() {
router.push({
name: "add-coupon-activity",
query: { couponActivityType: "SPECIFY" },
});
}
function addAutoCoupon() {
router.push({
name: "add-coupon-activity",
query: { couponActivityType: "AUTO_COUPON" },
});
}
function info(v) {
router.push({ name: "coupon-activity-info", query: { id: v.id } });

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@
<el-table-column label="面额/折扣" width="100">
<template #default="{ row }">
<priceColorScheme
v-if="row && row.price"
v-if="row && row.couponType === 'PRICE'"
:value="row.price"
:color="$mainColor"
/>

View File

@@ -93,7 +93,7 @@
<el-table-column label="面额/折扣" width="150">
<template #default="{ row }">
<span v-if="row">
<span v-if="row.price" :style="{ color: $mainColor }">
<span v-if="row.couponType === 'PRICE'" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.price, "¥") }}</span>
<span v-else>{{ row.couponDiscount }}折</span>
</span>
@@ -118,7 +118,7 @@
<el-table-column label="优惠券类型" width="120">
<template #default="{ row }">
<el-tag v-if="row" :type="couponTypeTagType(row.couponType)">
{{ couponTypeText(row.couponType) }}
{{ couponTypeText(row) }}
</el-tag>
</template>
</el-table-column>
@@ -362,12 +362,10 @@ function getTypeTagType(type) {
};
return map[type] || "";
}
function couponTypeText(type) {
const map = {
DISCOUNT: "打折",
PRICE: "减免现金",
};
return map[type] || "未知";
function couponTypeText(row) {
if (row.couponType === "DISCOUNT") return "满折优惠券";
if (row.couponType === "PRICE") return Number(row.consumeThreshold) === 0 ? "立减优惠券" : "满减优惠券";
return "未知";
}
function couponTypeTagType(type) {
const map = {

File diff suppressed because it is too large Load Diff

View File

@@ -67,7 +67,7 @@
<el-table-column label="面额/折扣" width="100">
<template #default="{ row }">
<priceColorScheme
v-if="row && row.price"
v-if="row && row.couponType === 'PRICE'"
:value="row.price"
:color="$mainColor"
/>

View File

@@ -17,10 +17,10 @@
style="width: 240px"
/>
</el-form-item>
<el-form-item label="获取方式" prop="getType">
<el-select v-model="searchForm.getType" placeholder="请选择" clearable style="width: 240px">
<el-option label="免费获取" value="FREE" />
<el-option label="活动获取" value="ACTIVITY" />
<el-form-item label="优惠券类型" prop="couponType">
<el-select v-model="searchForm.couponType" placeholder="请选择" clearable style="width: 240px">
<el-option label="满减优惠券" value="PRICE" />
<el-option label="满折优惠券" value="DISCOUNT" />
</el-select>
</el-form-item>
<el-form-item label="活动状态" prop="promotionStatus">
@@ -75,7 +75,7 @@
<el-table-column label="面额/折扣" width="150">
<template #default="{ row }">
<span v-if="row">
<span v-if="row.price" :style="{ color: $mainColor }">
<span v-if="row.couponType === 'PRICE'" :style="{ color: $mainColor }">
{{ $filters.unitPrice(row.price, "¥") }}</span>
<span v-else>{{ row.couponDiscount }}折</span>
</span>
@@ -94,21 +94,17 @@
</el-table-column>
<el-table-column label="获取方式" width="120">
<template #default="{ row }">
<el-tag v-if="row" :type="getTypeTagType(row.getType)">{{ getTypeText(row.getType) }}</el-tag>
<span v-if="row">{{ getTypeText(row.getType) }}</span>
</template>
</el-table-column>
<el-table-column label="优惠券类型" width="120">
<template #default="{ row }">
<el-tag v-if="row" :type="couponTypeTagType(row.couponType)">
{{ couponTypeText(row.couponType) }}
</el-tag>
<span v-if="row">{{ couponTypeText(row) }}</span>
</template>
</el-table-column>
<el-table-column label="品类描述" width="120">
<template #default="{ row }">
<el-tag v-if="row" :type="scopeTypeTagType(row.scopeType)">
{{ scopeTypeText(row.scopeType) }}
</el-tag>
<span v-if="row">{{ scopeTypeText(row.scopeType) }}</span>
</template>
</el-table-column>
<el-table-column label="活动时间" width="200">
@@ -124,9 +120,7 @@
fixed="right"
>
<template #default="{ row }">
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
{{ promotionStatusText(row.promotionStatus) }}
</el-tag>
<span v-if="row">{{ promotionStatusText(row.promotionStatus) }}</span>
</template>
</el-table-column>
<el-table-column
@@ -269,15 +263,6 @@ function promotionStatusText(status) {
return map[status] || "未知";
}
function promotionStatusTagType(status) {
const map = {
NEW: "info",
START: "success",
END: "danger",
CLOSE: "danger",
};
return map[status] || "danger";
}
function scopeTypeText(type) {
const map = {
@@ -289,15 +274,6 @@ function scopeTypeText(type) {
return map[type] || "未知";
}
function scopeTypeTagType(type) {
const map = {
ALL: "info",
PORTION_GOODS_CATEGORY: "warning",
PORTION_SHOP_CATEGORY: "warning",
PORTION_GOODS: "primary",
};
return map[type] || "danger";
}
function getTypeText(type) {
const map = {
@@ -309,31 +285,13 @@ function getTypeText(type) {
return map[type] || "未知";
}
function getTypeTagType(type) {
const map = {
FREE: "danger",
ACTIVITY: "warning",
INSIDE: "success",
IGAME: "success",
};
return map[type] || "info";
function couponTypeText(row) {
if (row.couponType === "DISCOUNT") return "满折优惠券";
if (row.couponType === "PRICE") return Number(row.consumeThreshold) === 0 ? "立减优惠券" : "满减优惠券";
return "未知";
}
function couponTypeText(type) {
const map = {
DISCOUNT: "打折",
PRICE: "减免现金",
};
return map[type] || "未知";
}
function couponTypeTagType(type) {
const map = {
DISCOUNT: "primary",
PRICE: "info",
};
return map[type] || "info";
}
function isSameSelection(next, current) {
const a = next || [];