mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-13 04:18:55 +08:00
feat(promotion): 新增满折满赠活动及阶梯优惠规则支持
- 满额活动列表页支持满减、满折、满赠三种类型筛选与标签展示 - 新增阶梯优惠规则列,按活动类型格式化展示各级优惠详情 - 活动详情页支持1至3级阶梯优惠配置及满赠赠品选择 - 页面装修组件增加满折、满赠活动类型标签展示 - 隐藏"添加活动优惠券"按钮入口 - 优化活动类型、状态、时间等表格列宽与样式
This commit is contained in:
@@ -1,42 +0,0 @@
|
|||||||
# IM 客服端 Vue 3 迁移说明
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- Vue 3.5 + Vue Router 4 + Pinia 4
|
|
||||||
- Element Plus(替代 element-ui)
|
|
||||||
- 自研右键菜单 `src/plugins/contextmenu.js`(替代 vue-contextmenujs)
|
|
||||||
- `vue-virtual-scroller@3`、`vue-cropper@1.x`、`vue-prism-editor@2.0.0-alpha.2`
|
|
||||||
|
|
||||||
## 组合式 API 迁移(2026-10 完成)
|
|
||||||
|
|
||||||
全部 53 个 `.vue` 组件已从 Options API 转换为 `<script setup>`,并完成 vuex → pinia:
|
|
||||||
|
|
||||||
- **状态管理**:`src/stores/` 下 8 个 store(app/user/talk/notify/settings/emoticon/dialogue/note),对应原 vuex 根 state + 7 个模块;mutation 全部转为同名 SCREAMING_CASE action。原 `src/store/` 已删除。
|
|
||||||
- **全局 mixin 保留**:`main.js` 的 `app.mixin(MainMixin)` 依然生效(对 `<script setup>` 组件同样注入),mixin 内部已改用 pinia。**注意**:mixin 经 `this` 回调组件成员时,`<script setup>` 的绑定不会出现在公共实例上——`views/message/index.vue` 采用了 ctx 桥接(`Object.defineProperties(instance.ctx, ...)`)方案,后续若有组件被全局 mixin 回调需用同样方案。
|
|
||||||
- **socket 事件层**(`im-server/event/*`、`plugins/ws-socket.js`、`utils/talk.js` 等纯 JS):vuex 引用全部改为函数内部 `useXxxStore()`。
|
|
||||||
- **$user() 名片弹层**(`components/user/user-card/index.js`):独立子 app 改用独立 `createPinia()`。
|
|
||||||
- **app-bridge.js**:保留了 `$store.state.talks.items` 兼容门面(惰性 getter),供旧调用形态使用。
|
|
||||||
|
|
||||||
## 启动
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd im
|
|
||||||
yarn install
|
|
||||||
yarn dev # 端口 8000
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 迁移要点
|
|
||||||
|
|
||||||
1. **入口** `src/main.js` — `createApp`,全局过滤器 `$filters`
|
|
||||||
2. **路由** `createRouter` + `createWebHistory`,404 路由 `/:pathMatch(.*)*`
|
|
||||||
3. **通知** `im-server/event/talk.js` 通过 `app-bridge` 发 `ElNotification`,避免循环引用
|
|
||||||
4. **用户卡片** `$user()` 插件用 `createApp` 挂载
|
|
||||||
5. **指令** `v-focus` / `v-paste` / `v-drag` / `v-outside` 已改为 Vue 3 钩子
|
|
||||||
6. **SVG** `svg-sprite-loader` + `svg-icon` 全局组件
|
|
||||||
7. **生命周期** `destroyed` → `beforeUnmount`;`$set` / `$delete` 已移除
|
|
||||||
|
|
||||||
## 上线前检查
|
|
||||||
|
|
||||||
统一清单见仓库根目录:[../MIGRATION-VUE3.md](../MIGRATION-VUE3.md)
|
|
||||||
IM 专项见该文档 **「IM 客服端」** 一节。
|
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
|
<!--
|
||||||
|
楼层装修链接选择器-营销活动选择面板(满额活动拆分为满减、满折、满赠三项)
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-09
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="list">
|
<div class="list">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in Object.keys(promotionList)"
|
v-for="(item, index) in promotionKeys"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="list-item"
|
class="list-item"
|
||||||
:class="{ active: selectedIndex == index }"
|
:class="{ active: selectedIndex == index }"
|
||||||
@@ -76,7 +81,7 @@ const emit = defineEmits(["selected"]);
|
|||||||
|
|
||||||
const totals = ref("");
|
const totals = ref("");
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const promotionList = ref("");
|
const promotionList = ref({});
|
||||||
const selectedIndex = ref(0);
|
const selectedIndex = ref(0);
|
||||||
const promotions = ref("");
|
const promotions = ref("");
|
||||||
const index = ref(999);
|
const index = ref(999);
|
||||||
@@ -86,6 +91,12 @@ const params = ref({
|
|||||||
});
|
});
|
||||||
const showPromotionList = ref([]);
|
const showPromotionList = ref([]);
|
||||||
|
|
||||||
|
const promotionKeys = computed(() => {
|
||||||
|
return Object.keys(promotionList.value || {}).filter(
|
||||||
|
(k) => k !== "FULL_DISCOUNT" && Boolean(typeOption(k).title)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const isSeckillMode = computed(() => {
|
const isSeckillMode = computed(() => {
|
||||||
return promotions.value === "SECKILL";
|
return promotions.value === "SECKILL";
|
||||||
});
|
});
|
||||||
@@ -106,12 +117,18 @@ function seckillStatusText(v) {
|
|||||||
}
|
}
|
||||||
function sortGoods(type) {
|
function sortGoods(type) {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
showPromotionList.value = promotionList.value[type];
|
showPromotionList.value = promotionList.value[type] || [];
|
||||||
}
|
}
|
||||||
function typeOption(type) {
|
function typeOption(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case "FULL_MINUS":
|
||||||
|
return { title: "满减活动", methodsed: () => { showPromotionList.value = []; sortGoods("FULL_MINUS"); } };
|
||||||
|
case "FULL_RATE":
|
||||||
|
return { title: "满折活动", methodsed: () => { showPromotionList.value = []; sortGoods("FULL_RATE"); } };
|
||||||
|
case "FULL_GIFT":
|
||||||
|
return { title: "满赠活动", methodsed: () => { showPromotionList.value = []; sortGoods("FULL_GIFT"); } };
|
||||||
case "FULL_DISCOUNT":
|
case "FULL_DISCOUNT":
|
||||||
return { title: "满减", methodsed: () => { showPromotionList.value = []; sortGoods("FULL_DISCOUNT"); } };
|
return { title: "满减活动", methodsed: () => { showPromotionList.value = []; sortGoods("FULL_DISCOUNT"); } };
|
||||||
case "PINTUAN":
|
case "PINTUAN":
|
||||||
return { title: "拼团", methodsed: () => { showPromotionList.value = []; sortGoods("PINTUAN"); } };
|
return { title: "拼团", methodsed: () => { showPromotionList.value = []; sortGoods("PINTUAN"); } };
|
||||||
case "KANJIA":
|
case "KANJIA":
|
||||||
@@ -143,15 +160,18 @@ function getPromotion(res) {
|
|||||||
if (res.result) {
|
if (res.result) {
|
||||||
promotionList.value = res.result;
|
promotionList.value = res.result;
|
||||||
delete promotionList.value.COUPON;
|
delete promotionList.value.COUPON;
|
||||||
Object.keys(res.result)[0] && typeOption(Object.keys(res.result)[0]).methodsed();
|
const keys = promotionKeys.value;
|
||||||
promotions.value = Object.keys(res.result)[0];
|
if (keys.length) {
|
||||||
|
promotions.value = keys[0];
|
||||||
|
typeOption(keys[0]) && typeOption(keys[0]).methodsed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function clickPromotion(val, i) {
|
function clickPromotion(val, i) {
|
||||||
promotions.value = val;
|
promotions.value = val;
|
||||||
selectedIndex.value = i;
|
selectedIndex.value = i;
|
||||||
params.value.pageNumber = 1;
|
params.value.pageNumber = 1;
|
||||||
typeOption(val) && typeOption(val).methodsed(promotionList.value[val].id);
|
typeOption(val) && typeOption(val).methodsed(promotionList.value[val]?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -408,6 +408,15 @@ util.initRouterNode = function(routers, data) {
|
|||||||
}
|
}
|
||||||
const menu = Object.assign({}, item);
|
const menu = Object.assign({}, item);
|
||||||
const hasChildren = item.children && item.children.length > 0;
|
const hasChildren = item.children && item.children.length > 0;
|
||||||
|
if (
|
||||||
|
!hasChildren &&
|
||||||
|
(menu.frontRoute === "promotions/full-discount/full-discount" ||
|
||||||
|
menu.name === "full-discount" ||
|
||||||
|
menu.name === "full-cut-activity") &&
|
||||||
|
menu.title === "满额活动"
|
||||||
|
) {
|
||||||
|
menu.title = "满减活动";
|
||||||
|
}
|
||||||
|
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
menu.children = [];
|
menu.children = [];
|
||||||
|
|||||||
@@ -49,6 +49,24 @@ const menuList = computed(() => appStore.menuList);
|
|||||||
const navList = computed(() => appStore.navList);
|
const navList = computed(() => appStore.navList);
|
||||||
const currNav = computed(() => appStore.currNav);
|
const currNav = computed(() => appStore.currNav);
|
||||||
const activeMenuName = computed(() => {
|
const activeMenuName = computed(() => {
|
||||||
|
if (route.name === "full-discount-detail") {
|
||||||
|
const type = route.query?.activityType;
|
||||||
|
const candidates =
|
||||||
|
type === "FULL_GIFT"
|
||||||
|
? ["manager-full-gift", "full-gift", "full-discount", "full-cut-activity"]
|
||||||
|
: type === "FULL_RATE"
|
||||||
|
? ["manager-full-rate", "full-rate", "full-discount", "full-cut-activity"]
|
||||||
|
: ["full-discount", "full-cut-activity"];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
for (const item of menuList.value) {
|
||||||
|
for (const menu of item.children || []) {
|
||||||
|
if (menu.name === candidate || menu.path === candidate) {
|
||||||
|
return menu.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const routePath = util.normalizeRoutePath(route.path);
|
const routePath = util.normalizeRoutePath(route.path);
|
||||||
for (const item of menuList.value) {
|
for (const item of menuList.value) {
|
||||||
for (const menu of item.children || []) {
|
for (const menu of item.children || []) {
|
||||||
|
|||||||
@@ -433,9 +433,15 @@
|
|||||||
<!-- 当选择完活动之后的其他回调 -->
|
<!-- 当选择完活动之后的其他回调 -->
|
||||||
<span v-if="item.url.___type == 'marketing'">
|
<span v-if="item.url.___type == 'marketing'">
|
||||||
<span v-if="item.url.___promotion == 'SECKILL'"> 秒杀 </span>
|
<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>
|
||||||
|
<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>
|
<span v-if="item.url.___promotion == 'PINTUAN'"> 拼团 </span>
|
||||||
{{ item.url.title || item.url.goodsName }}</span>
|
{{ item.url.title || item.url.goodsName }}</span>
|
||||||
<!-- 当选择完活动之后的其他回调 -->
|
<!-- 当选择完活动之后的其他回调 -->
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
<!--
|
||||||
|
管理端-满减/满折/满赠活动详情只读视图(适配 1~3 级阶梯及满赠多赠品展示)
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-09
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="full-discount-detail-page">
|
<div class="full-discount-detail-page">
|
||||||
<!-- 顶部标题与返回栏 -->
|
<!-- 顶部标题与返回栏 -->
|
||||||
@@ -5,7 +10,7 @@
|
|||||||
<div class="page-header-row">
|
<div class="page-header-row">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
<span>满优惠活动详情</span>
|
<span>{{ activityTypeTitle }}详情</span>
|
||||||
<el-tag type="info" size="small" effect="plain" class="mode-tag">
|
<el-tag type="info" size="small" effect="plain" class="mode-tag">
|
||||||
只读模式
|
只读模式
|
||||||
</el-tag>
|
</el-tag>
|
||||||
@@ -30,13 +35,17 @@
|
|||||||
<span class="font-semibold text-dark">{{ form.promotionName || "-" }}</span>
|
<span class="font-semibold text-dark">{{ form.promotionName || "-" }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="活动类型">
|
||||||
|
<el-tag size="small" :type="activityTagType">{{ activityTypeTitle }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="活动范围">
|
<el-descriptions-item label="活动范围">
|
||||||
<el-tag size="small" effect="light" :type="form.scopeType === 'ALL' ? 'success' : 'primary'">
|
<el-tag size="small" effect="light" :type="form.scopeType === 'ALL' ? 'success' : 'primary'">
|
||||||
{{ form.scopeType === "ALL" ? "全品类通用" : "指定商品适用" }}
|
{{ form.scopeType === "ALL" ? "全品类通用" : "指定商品适用" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="活动时间" :span="2">
|
<el-descriptions-item label="活动时间">
|
||||||
<span class="font-mono text-dark">
|
<span class="font-mono text-dark">
|
||||||
{{ form.startTime || "-" }} 至 {{ form.endTime || "-" }}
|
{{ form.startTime || "-" }} 至 {{ form.endTime || "-" }}
|
||||||
</span>
|
</span>
|
||||||
@@ -48,67 +57,50 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 优惠规则与赠品权益卡片 -->
|
<!-- 阶梯优惠规则卡片 -->
|
||||||
<el-card shadow="never" class="info-card mt_10">
|
<el-card shadow="never" class="info-card mt_10">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-indicator"></span>
|
<span class="section-indicator"></span>
|
||||||
<span class="section-title">优惠规则与权益</span>
|
<span class="section-title">阶梯优惠规则</span>
|
||||||
|
<span class="section-badge">共 {{ (form.ruleList || []).length }} 级阶梯(仅生效满足的最高一级)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-descriptions :column="2" border class="custom-descriptions">
|
<el-descriptions :column="1" border class="custom-descriptions">
|
||||||
<el-descriptions-item label="优惠门槛">
|
<el-descriptions-item
|
||||||
|
v-for="(tier, idx) in form.ruleList || []"
|
||||||
|
:key="idx"
|
||||||
|
:label="`第 ${idx + 1} 级阶梯`"
|
||||||
|
>
|
||||||
<span class="money-highlight font-mono font-semibold">
|
<span class="money-highlight font-mono font-semibold">
|
||||||
满 ¥ {{ Number(form.fullMoney || 0).toFixed(2) }}
|
满 ¥ {{ Number(tier.fullMoney || 0).toFixed(2) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-secondary ml_10" style="font-size: 12px">(达到当前金额可享优惠)</span>
|
<template v-if="form.activityType === 'FULL_MINUS'">
|
||||||
</el-descriptions-item>
|
<el-tag size="small" effect="light" type="danger" class="ml_10">减现金</el-tag>
|
||||||
|
|
||||||
<el-descriptions-item label="优惠形式">
|
|
||||||
<template v-if="form.discountType === 'fullMinusFlag'">
|
|
||||||
<el-tag size="small" effect="light" type="danger">减现金</el-tag>
|
|
||||||
<span class="money-highlight font-mono font-semibold ml_10">
|
<span class="money-highlight font-mono font-semibold ml_10">
|
||||||
减 ¥ {{ Number(form.fullMinus || 0).toFixed(2) }}
|
立减 ¥ {{ Number(tier.fullMinus || 0).toFixed(2) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="form.discountType === 'fullRateFlag'">
|
<template v-else-if="form.activityType === 'FULL_RATE'">
|
||||||
<el-tag size="small" effect="light" type="warning">打折</el-tag>
|
<el-tag size="small" effect="light" type="warning" class="ml_10">打折</el-tag>
|
||||||
<span class="money-highlight font-mono font-semibold ml_10">
|
<span class="money-highlight font-mono font-semibold ml_10">
|
||||||
打 {{ form.fullRate }} 折
|
打 {{ tier.fullRate }} 折
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="form.activityType === 'FULL_GIFT'">
|
||||||
<span class="text-secondary">无直接立减/打折</span>
|
<el-tag size="small" effect="light" type="success" class="ml_10">满赠</el-tag>
|
||||||
|
<div class="tier-gift-tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="(gift, gIdx) in tier.giftItems || []"
|
||||||
|
:key="gIdx"
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ formatGiftName(gift) }} × {{ gift.num || 1 }}件
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="附加赠送权益" :span="2">
|
|
||||||
<div class="extra-rights-row">
|
|
||||||
<el-tag :type="form.freeFreightFlag ? 'success' : 'info'" effect="plain" size="small">
|
|
||||||
{{ form.freeFreightFlag ? "✓ 包邮(免运费)" : "不包邮" }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag :type="form.couponFlag ? 'success' : 'info'" effect="plain" size="small">
|
|
||||||
{{ form.couponFlag ? "✓ 赠送优惠券" : "不送券" }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag :type="form.giftFlag ? 'success' : 'info'" effect="plain" size="small">
|
|
||||||
{{ form.giftFlag ? "✓ 赠送赠品" : "不送赠品" }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag :type="form.pointFlag ? 'success' : 'info'" effect="plain" size="small">
|
|
||||||
{{ form.pointFlag ? "✓ 赠送积分" : "不送积分" }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item v-if="form.couponFlag" label="赠送优惠券" :span="2">
|
|
||||||
<span class="font-semibold text-dark">{{ couponNameText }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item v-if="form.giftFlag" label="赠品信息" :span="2">
|
|
||||||
<span class="font-semibold text-dark">{{ giftNameText }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item v-if="form.pointFlag" label="赠送积分数" :span="2">
|
|
||||||
<span class="font-mono font-semibold text-dark">{{ form.point ?? 0 }} 积分</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@@ -186,7 +178,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { getPlatformCouponList, getFullDiscountById } from "@/api/promotion";
|
import { getFullDiscountById } from "@/api/promotion";
|
||||||
import { getGoodsSkuData } from "@/api/goods";
|
import { getGoodsSkuData } from "@/api/goods";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
@@ -198,64 +190,83 @@ const appStore = useAppStore();
|
|||||||
|
|
||||||
const id = ref(route.query.id);
|
const id = ref(route.query.id);
|
||||||
const form = ref({
|
const form = ref({
|
||||||
discountType: "fullMinusFlag",
|
activityType: "FULL_MINUS",
|
||||||
scopeType: "ALL",
|
scopeType: "ALL",
|
||||||
promotionGoodsList: [],
|
promotionGoodsList: [],
|
||||||
|
ruleList: [],
|
||||||
});
|
});
|
||||||
const couponList = ref([]);
|
|
||||||
const giftList = ref([]);
|
const giftList = ref([]);
|
||||||
|
|
||||||
const couponNameText = computed(() => {
|
const activityTypeTitle = computed(() => {
|
||||||
if (!form.value.couponId) return "未指定优惠券";
|
if (form.value.activityType === "FULL_RATE") return "满折活动";
|
||||||
const found = couponList.value.find((c) => String(c.id) === String(form.value.couponId));
|
if (form.value.activityType === "FULL_GIFT") return "满赠活动";
|
||||||
return found ? found.couponName : `优惠券 ID: ${form.value.couponId}`;
|
return "满减活动";
|
||||||
});
|
});
|
||||||
|
|
||||||
const giftNameText = computed(() => {
|
const activityTagType = computed(() => {
|
||||||
if (!form.value.giftId) return "未指定赠品";
|
if (form.value.activityType === "FULL_RATE") return "warning";
|
||||||
const found = giftList.value.find((g) => String(g.id) === String(form.value.giftId));
|
if (form.value.activityType === "FULL_GIFT") return "success";
|
||||||
return found ? found.goodsName : `赠品 ID: ${form.value.giftId}`;
|
return "danger";
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (id.value) {
|
if (id.value) {
|
||||||
getDetail();
|
getDetail();
|
||||||
}
|
}
|
||||||
getCouponList();
|
|
||||||
getGiftList();
|
getGiftList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function formatGiftName(gift) {
|
||||||
|
if (gift?.goodsName) return gift.goodsName;
|
||||||
|
const found = giftList.value.find((g) => String(g.id) === String(gift?.skuId));
|
||||||
|
return found ? found.goodsName : `赠品 ID: ${gift?.skuId || "-"}`;
|
||||||
|
}
|
||||||
|
|
||||||
function getDetail() {
|
function getDetail() {
|
||||||
getFullDiscountById(id.value).then((res) => {
|
getFullDiscountById(id.value).then((res) => {
|
||||||
if (res && res.success && res.result) {
|
if (res && res.success && res.result) {
|
||||||
let data = res.result;
|
const data = res.result;
|
||||||
if (data.scopeType !== "PORTION_GOODS") {
|
if (data.scopeType !== "PORTION_GOODS") {
|
||||||
data.promotionGoodsList = [];
|
data.promotionGoodsList = [];
|
||||||
}
|
}
|
||||||
if (data.fullRateFlag) {
|
if (!data.activityType) {
|
||||||
data.discountType = "fullRateFlag";
|
if (data.fullRateFlag) {
|
||||||
} else {
|
data.activityType = "FULL_RATE";
|
||||||
data.discountType = "fullMinusFlag";
|
} else if (!data.fullMinusFlag && data.giftFlag) {
|
||||||
|
data.activityType = "FULL_GIFT";
|
||||||
|
} else {
|
||||||
|
data.activityType = "FULL_MINUS";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Array.isArray(data.ruleList) || !data.ruleList.length) {
|
||||||
|
if (data.rulesJson) {
|
||||||
|
try {
|
||||||
|
data.ruleList = JSON.parse(data.rulesJson);
|
||||||
|
} catch (e) {
|
||||||
|
data.ruleList = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!data.ruleList || !data.ruleList.length) {
|
||||||
|
data.ruleList = [
|
||||||
|
{
|
||||||
|
fullMoney: data.fullMoney,
|
||||||
|
fullMinus: data.fullMinus,
|
||||||
|
fullRate: data.fullRate,
|
||||||
|
giftItems: data.giftId
|
||||||
|
? String(data.giftId)
|
||||||
|
.split(",")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((skuId) => ({ skuId, goodsName: data.giftSkuName, num: 1 }))
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
form.value = data;
|
form.value = data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCouponList() {
|
|
||||||
const params = {
|
|
||||||
pageSize: 50,
|
|
||||||
pageNumber: 1,
|
|
||||||
getType: "ACTIVITY",
|
|
||||||
promotionStatus: "START",
|
|
||||||
};
|
|
||||||
getPlatformCouponList(params).then((res) => {
|
|
||||||
if (res && res.success && res.result) {
|
|
||||||
couponList.value = res.result.records || [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGiftList() {
|
function getGiftList() {
|
||||||
const params = {
|
const params = {
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
@@ -357,11 +368,11 @@ function back() {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.extra-rights-row {
|
.tier-gift-tags {
|
||||||
|
margin-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-table {
|
.custom-table {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
<!--
|
||||||
|
管理端-满额活动列表页(支持满减活动 FULL_MINUS、满折活动 FULL_RATE、满赠活动 FULL_GIFT)
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-09
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="search full-cut">
|
<div class="search full-cut">
|
||||||
<el-card>
|
<el-card>
|
||||||
@@ -55,16 +60,23 @@
|
|||||||
class="mt_10"
|
class="mt_10"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column prop="promotionName" label="活动名称" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="promotionName" label="活动名称" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column prop="startTime" label="开始时间" width="170" />
|
<el-table-column prop="storeName" label="店铺名称" min-width="120" show-overflow-tooltip />
|
||||||
<el-table-column prop="endTime" label="结束时间" width="170" />
|
<el-table-column label="阶梯优惠规则" min-width="240" show-overflow-tooltip>
|
||||||
<el-table-column prop="storeName" label="店铺名称" min-width="100" show-overflow-tooltip />
|
|
||||||
<el-table-column label="活动类型" min-width="80">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.fullMinusFlag ? "满减" : "满折" }}</span>
|
<span v-if="row">{{ formatRowRules(row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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="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 }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
|
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
|
||||||
{{ promotionStatusText(row.promotionStatus) }}
|
{{ promotionStatusText(row.promotionStatus) }}
|
||||||
@@ -101,14 +113,39 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { getFullDiscountList, updateFullDiscount } from "@/api/promotion.js";
|
import { getFullDiscountList, updateFullDiscount } from "@/api/promotion.js";
|
||||||
import { Message, Modal } from "@/utils/message";
|
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 router = useRouter();
|
||||||
|
|
||||||
// 模板 ref(script 未使用,仅避免与 data 同名互相覆盖)
|
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 searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
const tableRef = ref(null);
|
const tableRef = ref(null);
|
||||||
|
|
||||||
@@ -120,9 +157,67 @@ const searchForm = reactive({
|
|||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
sort: "createTime",
|
sort: "createTime",
|
||||||
order: "desc",
|
order: "desc",
|
||||||
|
promotionName: "",
|
||||||
|
promotionStatus: "",
|
||||||
});
|
});
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
|
|
||||||
|
watch(currentActivityType, () => {
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
getDataList();
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
function promotionStatusText(status) {
|
||||||
const map = {
|
const map = {
|
||||||
NEW: "未开始",
|
NEW: "未开始",
|
||||||
@@ -145,18 +240,16 @@ function init() {
|
|||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
function openOrClose(row) {
|
function openOrClose(row) {
|
||||||
let name = "开启";
|
|
||||||
if (row.promotionStatus === "NEW" || row.promotionStatus === "START") {
|
if (row.promotionStatus === "NEW" || row.promotionStatus === "START") {
|
||||||
name = "关闭";
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "提示",
|
title: "提示",
|
||||||
content: `确认${name}此活动吗?需要一定时间才能生效,请耐心等待`,
|
content: "确认关闭此活动吗?需要一定时间才能生效,请耐心等待",
|
||||||
loading: true,
|
loading: true,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
updateFullDiscount(row.id).then((res) => {
|
updateFullDiscount(row.id).then((res) => {
|
||||||
Modal.remove();
|
Modal.remove();
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
Message.success(`${name}成功`);
|
Message.success("关闭成功");
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -185,7 +278,11 @@ function getDataList() {
|
|||||||
searchForm.startTime = null;
|
searchForm.startTime = null;
|
||||||
searchForm.endTime = null;
|
searchForm.endTime = null;
|
||||||
}
|
}
|
||||||
getFullDiscountList(searchForm).then((res) => {
|
const query = {
|
||||||
|
...searchForm,
|
||||||
|
activityType: currentActivityType.value,
|
||||||
|
};
|
||||||
|
getFullDiscountList(query).then((res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
data.value = res.result.records;
|
data.value = res.result.records;
|
||||||
@@ -194,7 +291,10 @@ function getDataList() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function view(row) {
|
function view(row) {
|
||||||
router.push({ name: "full-discount-detail", query: { id: row.id } });
|
router.push({
|
||||||
|
name: "full-discount-detail",
|
||||||
|
query: { id: row.id, activityType: resolveRowType(row) },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
14
manager/src/views/promotions/full-discount/full-gift.vue
Normal file
14
manager/src/views/promotions/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: "manager-full-gift" });
|
||||||
|
</script>
|
||||||
14
manager/src/views/promotions/full-discount/full-rate.vue
Normal file
14
manager/src/views/promotions/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: "manager-full-rate" });
|
||||||
|
</script>
|
||||||
@@ -57,7 +57,9 @@ const activityMetrics = ref([
|
|||||||
{ key: "couponActiveNum", label: "进行中优惠券" },
|
{ key: "couponActiveNum", label: "进行中优惠券" },
|
||||||
{ key: "seckillActiveNum", label: "进行中秒杀" },
|
{ key: "seckillActiveNum", label: "进行中秒杀" },
|
||||||
{ key: "pintuanActiveNum", label: "进行中拼团" },
|
{ key: "pintuanActiveNum", label: "进行中拼团" },
|
||||||
{ key: "fullDiscountActiveNum", label: "进行中满减" },
|
{ key: "fullMinusActiveNum", label: "进行中满减" },
|
||||||
|
{ key: "fullRateActiveNum", label: "进行中满折" },
|
||||||
|
{ key: "fullGiftActiveNum", label: "进行中满赠" },
|
||||||
{ key: "kanjiaActiveNum", label: "进行中砍价商品" },
|
{ key: "kanjiaActiveNum", label: "进行中砍价商品" },
|
||||||
{ key: "pointsGoodsActiveNum", label: "进行中积分商品" },
|
{ key: "pointsGoodsActiveNum", label: "进行中积分商品" },
|
||||||
]);
|
]);
|
||||||
@@ -128,7 +130,7 @@ h4 {
|
|||||||
}
|
}
|
||||||
.metric-grid {
|
.metric-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
.metric-item {
|
.metric-item {
|
||||||
|
|||||||
@@ -45,3 +45,16 @@ test('搜索清空与空权限账号正确处理', () => {
|
|||||||
assert.deepEqual(searchNavigation(flattenNavigation([]), ''), []);
|
assert.deepEqual(searchNavigation(flattenNavigation([]), ''), []);
|
||||||
assert.equal(findNavigationItem(items, 'unknown-detail'), null);
|
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) {
|
export function findNavigationItem(items, routeName, routeQuery = {}) {
|
||||||
return items.find((item) => item.name === routeName) ||
|
return (
|
||||||
(DETAIL_MENUS[routeName] || []).map((name) => items.find((item) => item.name === name)).find(Boolean) || null;
|
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 menu = Object.assign({}, item);
|
||||||
const hasChildren = item.children && item.children.length > 0;
|
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) {
|
if (hasChildren) {
|
||||||
menu.children = [];
|
menu.children = [];
|
||||||
|
|||||||
@@ -128,10 +128,19 @@ const load = ref(false);
|
|||||||
const loading = computed(() => appStore.loading);
|
const loading = computed(() => appStore.loading);
|
||||||
const unreadMessageCount = computed(() => appStore.unreadMessageCount);
|
const unreadMessageCount = computed(() => appStore.unreadMessageCount);
|
||||||
const pageTagsList = computed(() => appStore.storeOpenedList);
|
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 navigationItems = computed(() => flattenNavigation(appStore.navigationMenus || []));
|
||||||
const currentPathParts = computed(() => {
|
const currentPathParts = computed(() => {
|
||||||
const matched = findNavigationItem(navigationItems.value, route.name);
|
const matched = findNavigationItem(navigationItems.value, route.name, route.query);
|
||||||
const rawParts = matched
|
const rawParts = matched
|
||||||
? [
|
? [
|
||||||
matched.topTitle,
|
matched.topTitle,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const closedSections = ref([]);
|
const closedSections = ref([]);
|
||||||
const allItems = computed(() => flattenNavigation(appStore.navigationMenus).filter((item) => router.hasRoute(item.name)));
|
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) => ({
|
const sections = computed(() => appStore.navigationMenus.map((top) => ({
|
||||||
...top, items: allItems.value.filter((item) => item.topName === top.name),
|
...top, items: allItems.value.filter((item) => item.topName === top.name),
|
||||||
})).filter((top) => top.items.length));
|
})).filter((top) => top.items.length));
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<el-card>
|
<el-card>
|
||||||
<div class="operation padding-row" v-if="getType !== 'ACTIVITY'">
|
<div class="operation padding-row" v-if="getType !== 'ACTIVITY'">
|
||||||
<el-button type="primary" @click="add">添加优惠券</el-button>
|
<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 @click="delAll">批量关闭</el-button>
|
||||||
<el-button type="warning" @click="receivePage()">优惠券领取记录</el-button>
|
<el-button type="warning" @click="receivePage()">优惠券领取记录</el-button>
|
||||||
</div>
|
</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>
|
<template>
|
||||||
<div class="search full-cut">
|
<div class="search full-cut">
|
||||||
<el-card>
|
<el-card>
|
||||||
@@ -48,7 +53,7 @@
|
|||||||
|
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="operation padding-row">
|
<div class="operation padding-row">
|
||||||
<el-button type="primary" @click="add">新增</el-button>
|
<el-button type="primary" @click="add">{{ addBtnText }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
ref="table"
|
ref="table"
|
||||||
@@ -58,16 +63,27 @@
|
|||||||
class="mt_10"
|
class="mt_10"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column prop="promotionName" label="活动名称" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="promotionName" label="活动名称" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column prop="startTime" label="开始时间" width="170" />
|
<el-table-column label="阶梯优惠规则" min-width="240" show-overflow-tooltip>
|
||||||
<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">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.fullMinusFlag ? "满减" : "满折" }}</span>
|
<span v-if="row">{{ formatRowRules(row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
|
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
|
||||||
{{ promotionStatusText(row.promotionStatus) }}
|
{{ promotionStatusText(row.promotionStatus) }}
|
||||||
@@ -106,13 +122,45 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { getFullDiscountList, updateFullDiscount } from "@/api/promotion.js";
|
import { getFullDiscountList, updateFullDiscount } from "@/api/promotion.js";
|
||||||
import { Message, Modal } from "@/utils/message";
|
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 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 selectDate = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -121,16 +169,72 @@ const searchForm = reactive({
|
|||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
sort: "createTime",
|
sort: "createTime",
|
||||||
order: "desc",
|
order: "desc",
|
||||||
|
promotionName: "",
|
||||||
|
promotionStatus: "",
|
||||||
});
|
});
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
|
|
||||||
// searchFormRef:模板 ref 改名避免与 searchForm 变量互相覆盖
|
|
||||||
const searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
|
|
||||||
|
watch(currentActivityType, () => {
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
getDataList();
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
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) {
|
function promotionStatusText(status) {
|
||||||
const map = {
|
const map = {
|
||||||
NEW: "未开始",
|
NEW: "未开始",
|
||||||
@@ -156,18 +260,16 @@ function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openOrClose(row) {
|
function openOrClose(row) {
|
||||||
let name = "开启";
|
|
||||||
if (row.promotionStatus === "NEW" || row.promotionStatus === "START") {
|
if (row.promotionStatus === "NEW" || row.promotionStatus === "START") {
|
||||||
name = "关闭";
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "提示",
|
title: "提示",
|
||||||
content: `确认${name}此活动吗?需要一定时间才能生效,请耐心等待`,
|
content: "确认关闭此活动吗?需要一定时间才能生效,请耐心等待",
|
||||||
loading: true,
|
loading: true,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
updateFullDiscount(row.id).then((res) => {
|
updateFullDiscount(row.id).then((res) => {
|
||||||
Modal.remove();
|
Modal.remove();
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
Message.success(`${name}成功`);
|
Message.success("关闭成功");
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -200,7 +302,11 @@ function getDataList() {
|
|||||||
searchForm.startTime = null;
|
searchForm.startTime = null;
|
||||||
searchForm.endTime = null;
|
searchForm.endTime = null;
|
||||||
}
|
}
|
||||||
getFullDiscountList(searchForm).then((res) => {
|
const query = {
|
||||||
|
...searchForm,
|
||||||
|
activityType: currentActivityType.value,
|
||||||
|
};
|
||||||
|
getFullDiscountList(query).then((res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
data.value = res.result.records;
|
data.value = res.result.records;
|
||||||
@@ -210,11 +316,17 @@ function getDataList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function add() {
|
function add() {
|
||||||
router.push({ name: "full-discount-detail" });
|
router.push({
|
||||||
|
name: "full-discount-detail",
|
||||||
|
query: { activityType: currentActivityType.value },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function goDetail(row) {
|
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>
|
</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.___type == 'marketing'">
|
||||||
<span v-if="item.url.___promotion == 'SECKILL'"> 秒杀 </span>
|
<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>
|
||||||
|
<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>
|
<span v-if="item.url.___promotion == 'PINTUAN'"> 拼团 </span>
|
||||||
{{ item.url.title || item.url.goodsName }}</span>
|
{{ item.url.title || item.url.goodsName }}</span>
|
||||||
<!-- 当选择完活动之后的其他回调 -->
|
<!-- 当选择完活动之后的其他回调 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user