mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-13 12:28:54 +08:00
feat(promotion): 新增满折满赠活动及阶梯优惠规则支持
- 满额活动列表页支持满减、满折、满赠三种类型筛选与标签展示 - 新增阶梯优惠规则列,按活动类型格式化展示各级优惠详情 - 活动详情页支持1至3级阶梯优惠配置及满赠赠品选择 - 页面装修组件增加满折、满赠活动类型标签展示 - 隐藏"添加活动优惠券"按钮入口 - 优化活动类型、状态、时间等表格列宽与样式
This commit is contained in:
@@ -45,3 +45,16 @@ test('搜索清空与空权限账号正确处理', () => {
|
||||
assert.deepEqual(searchNavigation(flattenNavigation([]), ''), []);
|
||||
assert.equal(findNavigationItem(items, 'unknown-detail'), null);
|
||||
});
|
||||
test('满减/满折/满赠详情页按 activityType 定位对应左侧菜单', () => {
|
||||
const promoMenus = [
|
||||
{ name: 'promotion', title: '营销', children: [{ title: '店铺活动', children: [
|
||||
{ name: 'full-discount', path: 'full-discount', title: '满减活动', component },
|
||||
{ name: 'store-full-rate', path: 'full-rate', title: '满折活动', component },
|
||||
{ name: 'store-full-gift', path: 'full-gift', title: '满赠活动', component },
|
||||
] }] },
|
||||
];
|
||||
const promoItems = flattenNavigation(promoMenus);
|
||||
assert.equal(findNavigationItem(promoItems, 'full-discount-detail', { activityType: 'FULL_GIFT' })?.name, 'store-full-gift');
|
||||
assert.equal(findNavigationItem(promoItems, 'full-discount-detail', { activityType: 'FULL_RATE' })?.name, 'store-full-rate');
|
||||
assert.equal(findNavigationItem(promoItems, 'full-discount-detail', { activityType: 'FULL_MINUS' })?.name, 'full-discount');
|
||||
});
|
||||
|
||||
@@ -39,10 +39,31 @@ export function flattenNavigation(menus) {
|
||||
));
|
||||
}
|
||||
|
||||
const FULL_DISCOUNT_DETAIL_MENUS = {
|
||||
FULL_GIFT: ["store-full-gift", "full-gift", "full-discount", "full-cut"],
|
||||
FULL_RATE: ["store-full-rate", "full-rate", "full-discount", "full-cut"],
|
||||
FULL_MINUS: ["full-discount", "full-cut"],
|
||||
};
|
||||
|
||||
function resolveDetailMenuCandidates(routeName, routeQuery = {}) {
|
||||
if (routeName === "full-discount-detail") {
|
||||
const type = routeQuery?.activityType;
|
||||
if (type && FULL_DISCOUNT_DETAIL_MENUS[type]) {
|
||||
return FULL_DISCOUNT_DETAIL_MENUS[type];
|
||||
}
|
||||
}
|
||||
return DETAIL_MENUS[routeName] || [];
|
||||
}
|
||||
|
||||
/** 优先精确匹配菜单,再匹配详情页业务入口;无授权入口时返回空,不沿用旧账号栏目。 */
|
||||
export function findNavigationItem(items, routeName) {
|
||||
return items.find((item) => item.name === routeName) ||
|
||||
(DETAIL_MENUS[routeName] || []).map((name) => items.find((item) => item.name === name)).find(Boolean) || null;
|
||||
export function findNavigationItem(items, routeName, routeQuery = {}) {
|
||||
return (
|
||||
items.find((item) => item.name === routeName) ||
|
||||
resolveDetailMenuCandidates(routeName, routeQuery)
|
||||
.map((name) => items.find((item) => item.name === name || item.path === name))
|
||||
.find(Boolean) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** 按菜单、分组和栏目检索,空格分隔的关键词须全部匹配。 */
|
||||
|
||||
@@ -503,6 +503,15 @@ util.initRouterNode = function (routers, data) {
|
||||
}
|
||||
const menu = Object.assign({}, item);
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
if (
|
||||
!hasChildren &&
|
||||
(menu.frontRoute === "promotion/full-discount/full-discount" ||
|
||||
menu.name === "full-discount" ||
|
||||
menu.name === "full-cut") &&
|
||||
menu.title === "满额活动"
|
||||
) {
|
||||
menu.title = "满减活动";
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
menu.children = [];
|
||||
|
||||
@@ -128,10 +128,19 @@ const load = ref(false);
|
||||
const loading = computed(() => appStore.loading);
|
||||
const unreadMessageCount = computed(() => appStore.unreadMessageCount);
|
||||
const pageTagsList = computed(() => appStore.storeOpenedList);
|
||||
const currentPageTitle = computed(() => route.meta.title || pageTagsList.value.find((item) => item.name === route.name)?.title || '工作台');
|
||||
const currentPageTitle = computed(() => {
|
||||
if (route.name === "full-discount-detail") {
|
||||
const type = route.query?.activityType;
|
||||
const isDetail = Boolean(route.query?.id);
|
||||
if (type === "FULL_GIFT") return isDetail ? "满赠活动详情" : "添加满赠活动";
|
||||
if (type === "FULL_RATE") return isDetail ? "满折活动详情" : "添加满折活动";
|
||||
return isDetail ? "满减活动详情" : "添加满减活动";
|
||||
}
|
||||
return route.meta.title || pageTagsList.value.find((item) => item.name === route.name)?.title || '工作台';
|
||||
});
|
||||
const navigationItems = computed(() => flattenNavigation(appStore.navigationMenus || []));
|
||||
const currentPathParts = computed(() => {
|
||||
const matched = findNavigationItem(navigationItems.value, route.name);
|
||||
const matched = findNavigationItem(navigationItems.value, route.name, route.query);
|
||||
const rawParts = matched
|
||||
? [
|
||||
matched.topTitle,
|
||||
|
||||
@@ -44,7 +44,7 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const closedSections = ref([]);
|
||||
const allItems = computed(() => flattenNavigation(appStore.navigationMenus).filter((item) => router.hasRoute(item.name)));
|
||||
const activeItem = computed(() => findNavigationItem(allItems.value, route.name));
|
||||
const activeItem = computed(() => findNavigationItem(allItems.value, route.name, route.query));
|
||||
const sections = computed(() => appStore.navigationMenus.map((top) => ({
|
||||
...top, items: allItems.value.filter((item) => item.topName === top.name),
|
||||
})).filter((top) => top.items.length));
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<el-card>
|
||||
<div class="operation padding-row" v-if="getType !== 'ACTIVITY'">
|
||||
<el-button type="primary" @click="add">添加优惠券</el-button>
|
||||
<el-button class="activity-coupon-button" @click="addActivityCoupon">添加活动优惠券</el-button>
|
||||
<el-button v-if="false" class="activity-coupon-button" @click="addActivityCoupon">添加活动优惠券</el-button>
|
||||
<el-button @click="delAll">批量关闭</el-button>
|
||||
<el-button type="warning" @click="receivePage()">优惠券领取记录</el-button>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,8 @@
|
||||
<!--
|
||||
满额活动列表页(支持满减活动 FULL_MINUS、满折活动 FULL_RATE、满赠活动 FULL_GIFT)
|
||||
@author Bulbasaur
|
||||
@since 2026-11-09
|
||||
-->
|
||||
<template>
|
||||
<div class="search full-cut">
|
||||
<el-card>
|
||||
@@ -48,7 +53,7 @@
|
||||
|
||||
<el-card>
|
||||
<div class="operation padding-row">
|
||||
<el-button type="primary" @click="add">新增</el-button>
|
||||
<el-button type="primary" @click="add">{{ addBtnText }}</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
ref="table"
|
||||
@@ -58,16 +63,27 @@
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="promotionName" label="活动名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="startTime" label="开始时间" width="170" />
|
||||
<el-table-column prop="endTime" label="结束时间" width="170" />
|
||||
<el-table-column prop="storeName" label="店铺名称" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="活动类型" min-width="80">
|
||||
<el-table-column prop="promotionName" label="活动名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="阶梯优惠规则" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.fullMinusFlag ? "满减" : "满折" }}</span>
|
||||
<span v-if="row">{{ formatRowRules(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动状态" min-width="100">
|
||||
<el-table-column prop="startTime" label="开始时间" width="170" />
|
||||
<el-table-column prop="endTime" label="结束时间" width="170" />
|
||||
<el-table-column label="适用范围" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.scopeType === "ALL" ? "全品类" : "指定商品" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动类型" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" size="small" effect="plain" :type="activityTagType(row)">
|
||||
{{ activityTypeText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
|
||||
{{ promotionStatusText(row.promotionStatus) }}
|
||||
@@ -106,13 +122,45 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getFullDiscountList, updateFullDiscount } from "@/api/promotion.js";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 固定活动子类型(FULL_MINUS-满减,FULL_RATE-满折,FULL_GIFT-满赠)
|
||||
*/
|
||||
defaultActivityType: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currentActivityType = computed(() => {
|
||||
if (props.defaultActivityType) {
|
||||
return props.defaultActivityType;
|
||||
}
|
||||
const routePath = String(route.path || "");
|
||||
const routeName = String(route.name || "");
|
||||
if (routePath.includes("full-rate") || routeName.includes("full-rate")) {
|
||||
return "FULL_RATE";
|
||||
}
|
||||
if (routePath.includes("full-gift") || routeName.includes("full-gift")) {
|
||||
return "FULL_GIFT";
|
||||
}
|
||||
return "FULL_MINUS";
|
||||
});
|
||||
|
||||
const addBtnText = computed(() => {
|
||||
if (currentActivityType.value === "FULL_RATE") return "新增满折活动";
|
||||
if (currentActivityType.value === "FULL_GIFT") return "新增满赠活动";
|
||||
return "新增满减活动";
|
||||
});
|
||||
|
||||
const selectDate = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
@@ -121,16 +169,72 @@ const searchForm = reactive({
|
||||
pageSize: 20,
|
||||
sort: "createTime",
|
||||
order: "desc",
|
||||
promotionName: "",
|
||||
promotionStatus: "",
|
||||
});
|
||||
const data = ref([]);
|
||||
|
||||
// searchFormRef:模板 ref 改名避免与 searchForm 变量互相覆盖
|
||||
const searchFormRef = ref(null);
|
||||
|
||||
watch(currentActivityType, () => {
|
||||
searchForm.pageNumber = 1;
|
||||
getDataList();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
function resolveRowType(row) {
|
||||
if (row?.activityType) return row.activityType;
|
||||
if (row?.fullRateFlag) return "FULL_RATE";
|
||||
if (!row?.fullMinusFlag && row?.giftFlag) return "FULL_GIFT";
|
||||
return "FULL_MINUS";
|
||||
}
|
||||
|
||||
function activityTypeText(row) {
|
||||
const type = resolveRowType(row);
|
||||
if (type === "FULL_RATE") return "满折";
|
||||
if (type === "FULL_GIFT") return "满赠";
|
||||
return "满减";
|
||||
}
|
||||
|
||||
function activityTagType(row) {
|
||||
const type = resolveRowType(row);
|
||||
if (type === "FULL_RATE") return "warning";
|
||||
if (type === "FULL_GIFT") return "success";
|
||||
return "danger";
|
||||
}
|
||||
|
||||
function formatRowRules(row) {
|
||||
if (row?.title) return row.title;
|
||||
if (row?.rulesJson) {
|
||||
try {
|
||||
const rules = JSON.parse(row.rulesJson);
|
||||
if (Array.isArray(rules) && rules.length) {
|
||||
const type = resolveRowType(row);
|
||||
return rules
|
||||
.map((r) => {
|
||||
if (type === "FULL_RATE") return `满${r.fullMoney}元打${r.fullRate}折`;
|
||||
if (type === "FULL_GIFT") {
|
||||
const count = Array.isArray(r.giftItems) ? r.giftItems.length : 1;
|
||||
return `满${r.fullMoney}元赠${count}种商品`;
|
||||
}
|
||||
return `满${r.fullMoney}元减${r.fullMinus}元`;
|
||||
})
|
||||
.join(";");
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (row?.fullMoney != null) {
|
||||
if (row.fullRateFlag) return `满${row.fullMoney}元打${row.fullRate}折`;
|
||||
if (row.giftFlag) return `满${row.fullMoney}元享赠品`;
|
||||
return `满${row.fullMoney}元减${row.fullMinus}元`;
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
|
||||
function promotionStatusText(status) {
|
||||
const map = {
|
||||
NEW: "未开始",
|
||||
@@ -156,18 +260,16 @@ function init() {
|
||||
}
|
||||
|
||||
function openOrClose(row) {
|
||||
let name = "开启";
|
||||
if (row.promotionStatus === "NEW" || row.promotionStatus === "START") {
|
||||
name = "关闭";
|
||||
Modal.confirm({
|
||||
title: "提示",
|
||||
content: `确认${name}此活动吗?需要一定时间才能生效,请耐心等待`,
|
||||
content: "确认关闭此活动吗?需要一定时间才能生效,请耐心等待",
|
||||
loading: true,
|
||||
onOk: () => {
|
||||
updateFullDiscount(row.id).then((res) => {
|
||||
Modal.remove();
|
||||
if (res.success) {
|
||||
Message.success(`${name}成功`);
|
||||
Message.success("关闭成功");
|
||||
getDataList();
|
||||
}
|
||||
});
|
||||
@@ -200,7 +302,11 @@ function getDataList() {
|
||||
searchForm.startTime = null;
|
||||
searchForm.endTime = null;
|
||||
}
|
||||
getFullDiscountList(searchForm).then((res) => {
|
||||
const query = {
|
||||
...searchForm,
|
||||
activityType: currentActivityType.value,
|
||||
};
|
||||
getFullDiscountList(query).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result.records;
|
||||
@@ -210,11 +316,17 @@ function getDataList() {
|
||||
}
|
||||
|
||||
function add() {
|
||||
router.push({ name: "full-discount-detail" });
|
||||
router.push({
|
||||
name: "full-discount-detail",
|
||||
query: { activityType: currentActivityType.value },
|
||||
});
|
||||
}
|
||||
|
||||
function goDetail(row) {
|
||||
router.push({ name: "full-discount-detail", query: { id: row.id } });
|
||||
router.push({
|
||||
name: "full-discount-detail",
|
||||
query: { id: row.id, activityType: resolveRowType(row) },
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
14
seller/src/views/promotion/full-discount/full-gift.vue
Normal file
14
seller/src/views/promotion/full-discount/full-gift.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
商家端-满赠活动列表页
|
||||
@author Bulbasaur
|
||||
@since 2026-11-09
|
||||
-->
|
||||
<template>
|
||||
<FullDiscountList default-activity-type="FULL_GIFT" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FullDiscountList from "./full-discount.vue";
|
||||
|
||||
defineOptions({ name: "store-full-gift" });
|
||||
</script>
|
||||
14
seller/src/views/promotion/full-discount/full-rate.vue
Normal file
14
seller/src/views/promotion/full-discount/full-rate.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
商家端-满折活动列表页
|
||||
@author Bulbasaur
|
||||
@since 2026-11-09
|
||||
-->
|
||||
<template>
|
||||
<FullDiscountList default-activity-type="FULL_RATE" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FullDiscountList from "./full-discount.vue";
|
||||
|
||||
defineOptions({ name: "store-full-rate" });
|
||||
</script>
|
||||
@@ -433,9 +433,15 @@
|
||||
<!-- 当选择完活动之后的其他回调 -->
|
||||
<span v-if="item.url.___type == 'marketing'">
|
||||
<span v-if="item.url.___promotion == 'SECKILL'"> 秒杀 </span>
|
||||
<span v-if="item.url.___promotion == 'FULL_DISCOUNT'">
|
||||
<span v-if="item.url.___promotion == 'FULL_DISCOUNT' || item.url.___promotion == 'FULL_MINUS'">
|
||||
满减
|
||||
</span>
|
||||
<span v-if="item.url.___promotion == 'FULL_RATE'">
|
||||
满折
|
||||
</span>
|
||||
<span v-if="item.url.___promotion == 'FULL_GIFT'">
|
||||
满赠
|
||||
</span>
|
||||
<span v-if="item.url.___promotion == 'PINTUAN'"> 拼团 </span>
|
||||
{{ item.url.title || item.url.goodsName }}</span>
|
||||
<!-- 当选择完活动之后的其他回调 -->
|
||||
|
||||
Reference in New Issue
Block a user