Kaynağa Gözat

feat(红包管理): 新增链接复制、操作记录及数据统计功能

- 添加 vue-clipboard3 依赖以支持复制链接功能
- 新增 RedPacketLinkDialog 组件用于展示和复制红包链接
- 新增 RedPacketLogDialog 组件用于查看操作记录
- 重构 RedPacketDataDialog 组件,优化模态框显示逻辑
- 更新红包管理主页面,对接真实 API 接口数据
- 修复分享优惠规则中状态码判断逻辑,统一使用 200
- 优化红包对话框表单,支持编辑和新建两种模式
- 添加红包数量统计功能,显示普通和惊喜红包的已建数量
ylong 2 hafta önce
ebeveyn
işleme
33961c4934

+ 1 - 0
package.json

@@ -42,6 +42,7 @@
     "v-viewer": "^3.0.21",
     "viewerjs": "^1.11.7",
     "vue": "~3.4.38",
+    "vue-clipboard3": "^2.0.0",
     "vue-echarts": "~7.0.3",
     "vue-router": "~4.4.3",
     "vuedraggable": "^4.1.0",

+ 37 - 11
pnpm-lock.yaml

@@ -92,6 +92,9 @@ importers:
       vue:
         specifier: ~3.4.38
         version: 3.4.38
+      vue-clipboard3:
+        specifier: ^2.0.0
+        version: 2.0.0
       vue-echarts:
         specifier: ~7.0.3
         version: 7.0.3(@vue/runtime-core@3.4.38)(echarts@5.5.1)(vue@3.4.38)
@@ -99,7 +102,7 @@ importers:
         specifier: ~4.4.3
         version: 4.4.5(vue@3.4.38)
       vuedraggable:
-        specifier: ~4.1.0
+        specifier: ^4.1.0
         version: 4.1.0(vue@3.4.38)
       xgplayer:
         specifier: ~3.0.20
@@ -487,61 +490,51 @@ packages:
     resolution: {integrity: sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==}
     cpu: [arm]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-arm-musleabihf@4.28.1':
     resolution: {integrity: sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==}
     cpu: [arm]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-linux-arm64-gnu@4.28.1':
     resolution: {integrity: sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-arm64-musl@4.28.1':
     resolution: {integrity: sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-linux-loongarch64-gnu@4.28.1':
     resolution: {integrity: sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==}
     cpu: [loong64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-powerpc64le-gnu@4.28.1':
     resolution: {integrity: sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==}
     cpu: [ppc64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-riscv64-gnu@4.28.1':
     resolution: {integrity: sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==}
     cpu: [riscv64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-s390x-gnu@4.28.1':
     resolution: {integrity: sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==}
     cpu: [s390x]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-x64-gnu@4.28.1':
     resolution: {integrity: sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-x64-musl@4.28.1':
     resolution: {integrity: sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-win32-arm64-msvc@4.28.1':
     resolution: {integrity: sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==}
@@ -816,6 +809,9 @@ packages:
     resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
     engines: {node: '>= 8.10.0'}
 
+  clipboard@2.0.11:
+    resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
+
   codemirror-ssr@0.65.0:
     resolution: {integrity: sha512-ofTAfPkQV56SYFfymNMYJ1ELo3+Jnkw3mOLgnIiQjhonwNmNzX1OFvnihAnYRXL0PWl2kT7s0gKrLc2ExshK4g==}
     peerDependencies:
@@ -1200,6 +1196,9 @@ packages:
     resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
     engines: {node: '>=8'}
 
+  good-listener@1.2.2:
+    resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
+
   graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
 
@@ -1913,6 +1912,9 @@ packages:
   select-files@1.0.1:
     resolution: {integrity: sha512-8h4DSpjfFa0hyMP3z3ye4SxyhdaE5RgaXeScRpH7xl4YblnZSHwexmLdLNdSKwTO8H9ccDKj7Votz0io+18+BQ==}
 
+  select@1.1.2:
+    resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
+
   semver@7.6.3:
     resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
     engines: {node: '>=10'}
@@ -2014,6 +2016,9 @@ packages:
   thenify@3.3.1:
     resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
 
+  tiny-emitter@2.1.0:
+    resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
+
   tinymce@5.10.9:
     resolution: {integrity: sha512-5bkrors87X9LhYX2xq8GgPHrIgJYHl87YNs+kBcjQ5I3CiUgzo/vFcGvT3MZQ9QHsEeYMhYO6a5CLGGffR8hMg==}
 
@@ -2189,6 +2194,9 @@ packages:
       terser:
         optional: true
 
+  vue-clipboard3@2.0.0:
+    resolution: {integrity: sha512-Q9S7dzWGax7LN5iiSPcu/K1GGm2gcBBlYwmMsUc5/16N6w90cbKow3FnPmPs95sungns4yvd9/+JhbAznECS2A==}
+
   vue-demi@0.13.11:
     resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
     engines: {node: '>=12'}
@@ -2956,6 +2964,12 @@ snapshots:
     optionalDependencies:
       fsevents: 2.3.3
 
+  clipboard@2.0.11:
+    dependencies:
+      good-listener: 1.2.2
+      select: 1.1.2
+      tiny-emitter: 2.1.0
+
   codemirror-ssr@0.65.0(@types/codemirror@5.60.15):
     dependencies:
       '@types/codemirror': 5.60.15
@@ -3400,6 +3414,10 @@ snapshots:
     dependencies:
       type-fest: 0.20.2
 
+  good-listener@1.2.2:
+    dependencies:
+      delegate: 3.2.0
+
   graceful-fs@4.2.11: {}
 
   graphemer@1.4.0: {}
@@ -4284,6 +4302,8 @@ snapshots:
 
   select-files@1.0.1: {}
 
+  select@1.1.2: {}
+
   semver@7.6.3: {}
 
   setimmediate@1.0.5: {}
@@ -4409,6 +4429,8 @@ snapshots:
     dependencies:
       any-promise: 1.3.0
 
+  tiny-emitter@2.1.0: {}
+
   tinymce@5.10.9: {}
 
   tippy.js@6.3.7:
@@ -4609,6 +4631,10 @@ snapshots:
       fsevents: 2.3.3
       sass: 1.77.8
 
+  vue-clipboard3@2.0.0:
+    dependencies:
+      clipboard: 2.0.11
+
   vue-demi@0.13.11(vue@3.4.38):
     dependencies:
       vue: 3.4.38

+ 30 - 37
src/views/marketing/redPacket/components/RedPacketDataDialog.vue

@@ -1,5 +1,6 @@
 <template>
-    <ele-modal v-model="visible" title="红包数据" width="1280px" :footer="null" custom-class="red-packet-data-dialog" top="4vh">
+    <ele-modal v-model="visible" title="红包数据" width="1280px" :footer="null" custom-class="red-packet-data-dialog"
+        top="4vh">
         <el-tabs v-model="activeTab">
             <el-tab-pane label="模板数据" name="template">
                 <TemplateData v-if="visible && activeTab === 'template'" />
@@ -12,46 +13,38 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
-import TemplateData from './TemplateData.vue';
-import CollectionQuery from './CollectionQuery.vue';
-
-const props = defineProps({
-    modelValue: {
-        type: Boolean,
-        default: false
-    },
-    redPacketId: {
-        type: [String, Number],
-        default: ''
-    }
-});
+    import { ref, computed } from 'vue';
+    import TemplateData from './TemplateData.vue';
+    import CollectionQuery from './CollectionQuery.vue';
+
+    const visible = ref(false);
+    const redPacketId = ref('');
 
-const emit = defineEmits(['update:modelValue']);
+    const activeTab = ref('template');
 
-const visible = computed({
-    get: () => props.modelValue,
-    set: (val) => emit('update:modelValue', val)
-});
+    const open = (id) => {
+        redPacketId.value = id;
+        visible.value = true;
+    };
 
-const activeTab = ref('template');
+    defineExpose({ open });
 </script>
 
 <style scoped>
-:deep(.red-packet-data-dialog .el-dialog__header) {
-    display: none;
-    /* Hide default header if we want tabs at the very top, but EleModal usually has a header. */
-    /* If we want tabs inside the modal body, we can keep the header or set title to empty string */
-}
-
-/* Adjustments to make tabs look like the screenshot */
-:deep(.el-tabs__nav-wrap::after) {
-    height: 1px;
-}
-
-:deep(.el-tabs__item) {
-    font-size: 16px;
-    height: 50px;
-    line-height: 50px;
-}
+    :deep(.red-packet-data-dialog .el-dialog__header) {
+        display: none;
+        /* Hide default header if we want tabs at the very top, but EleModal usually has a header. */
+        /* If we want tabs inside the modal body, we can keep the header or set title to empty string */
+    }
+
+    /* Adjustments to make tabs look like the screenshot */
+    :deep(.el-tabs__nav-wrap::after) {
+        height: 1px;
+    }
+
+    :deep(.el-tabs__item) {
+        font-size: 16px;
+        height: 50px;
+        line-height: 50px;
+    }
 </style>

+ 231 - 161
src/views/marketing/redPacket/components/RedPacketDialog.vue

@@ -1,33 +1,33 @@
 <template>
     <ele-modal :width="680" v-model="visible" :title="dialogTitle" :append-to-body="true" :close-on-click-modal="false">
         <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" @submit.prevent>
-            <el-form-item label="红包名称" prop="name">
-                <el-input v-model="form.name" placeholder="请输入红包名称" maxlength="20" show-word-limit
+            <el-form-item label="红包名称" prop="couponName">
+                <el-input v-model="form.couponName" placeholder="请输入红包名称" maxlength="20" show-word-limit
                     :disabled="isEditActive" />
             </el-form-item>
 
-            <el-form-item label="红包数量" prop="quantity">
-                <el-input-number v-model="form.quantity" :min="1" :max="100000" placeholder="请输入" class="!w-[200px]" />
+            <el-form-item label="红包数量" prop="totalNum">
+                <el-input-number v-model="form.totalNum" :min="1" :max="100000" placeholder="请输入" class="!w-[200px]" />
                 <span class="ml-2 text-gray-500">张</span>
             </el-form-item>
 
             <el-form-item label="红包面额" required>
                 <div class="flex items-center space-x-2">
-                    <template v-if="type === 'surprise'">
-                        <el-form-item prop="minAmount" class="mb-0 !mr-0">
-                            <el-input-number v-model="form.minAmount" :min="0.01" :precision="2" :controls="false"
+                    <template v-if="type === '2'">
+                        <el-form-item prop="minMoney" class="mb-0 !mr-0">
+                            <el-input-number v-model="form.minMoney" :min="0.01" :precision="2" :controls="false"
                                 placeholder="请输入" class="!w-[100px]" :disabled="isEditActive" />
                         </el-form-item>
                         <span>-</span>
-                        <el-form-item prop="maxAmount" class="mb-0 !mr-0">
-                            <el-input-number v-model="form.maxAmount" :min="0.01" :precision="2" :controls="false"
+                        <el-form-item prop="maxMoney" class="mb-0 !mr-0">
+                            <el-input-number v-model="form.maxMoney" :min="0.01" :precision="2" :controls="false"
                                 placeholder="请输入" class="!w-[100px]" :disabled="isEditActive" />
                         </el-form-item>
                         <span class="text-gray-500">元随机数字</span>
                     </template>
                     <template v-else>
-                        <el-form-item prop="amount" class="mb-0 !mr-0">
-                            <el-input-number v-model="form.amount" :min="0.01" :precision="2" :controls="false"
+                        <el-form-item prop="faceMoney" class="mb-0 !mr-0">
+                            <el-input-number v-model="form.faceMoney" :min="0.01" :precision="2" :controls="false"
                                 placeholder="请输入" class="!w-[150px]" :disabled="isEditActive" />
                         </el-form-item>
                         <span class="text-gray-500">元</span>
@@ -40,22 +40,29 @@
                     <el-radio label="none">无门槛</el-radio>
                     <el-radio label="full">满</el-radio>
                 </el-radio-group>
-                <el-form-item v-if="form.thresholdType === 'full'" prop="thresholdAmount"
+                <el-form-item v-if="form.thresholdType === 'full'" prop="thresholdMoney"
                     class="inline-block ml-2 mb-0 !mr-0">
-                    <el-input-number v-model="form.thresholdAmount" :min="0.01" :precision="2" :controls="false"
+                    <el-input-number v-model="form.thresholdMoney" :min="0.01" :precision="2" :controls="false"
                         placeholder="请输入" class="!w-[100px]" :disabled="isEditActive" />
                     <span class="ml-2 text-gray-500">元 可用</span>
                 </el-form-item>
             </el-form-item>
 
-            <el-form-item label="使用商品" prop="productType">
-                <el-radio-group v-model="form.productType" :disabled="isEditActive">
-                    <el-radio label="all">所有商品</el-radio>
-                    <el-radio label="specific">指定商品</el-radio>
+            <el-form-item label="可否叠加使用" prop="stackType">
+                <el-radio-group v-model="form.stackType" :disabled="isEditActive">
+                    <el-radio label="0">否</el-radio>
+                    <el-radio label="1">是</el-radio>
                 </el-radio-group>
-                <div v-if="form.productType === 'specific'" class="mt-2">
+            </el-form-item>
+
+            <el-form-item label="使用商品" prop="scope">
+                <el-radio-group v-model="form.scope" :disabled="isEditActive">
+                    <el-radio :label="'1'">所有商品</el-radio>
+                    <el-radio :label="'2'">指定商品</el-radio>
+                </el-radio-group>
+                <div v-if="form.scope === '2'" class="mt-2">
                     <el-button type="primary" link @click="openProductSelect" :disabled="isEditActive">
-                        已选择 {{ form.products.length }} 个商品
+                        已选择 {{ form.products ? form.products.length : 0 }} 个商品
                     </el-button>
                 </div>
             </el-form-item>
@@ -63,31 +70,22 @@
             <el-form-item label="有效期" required>
                 <div class="flex flex-col space-y-4 w-full">
                     <div class="flex items-center">
-                        <el-radio v-model="form.validityType" label="range" :disabled="isEditActive" class="!mr-4">日期范围</el-radio>
+                        <el-radio v-model="form.validityType" label="range" :disabled="isEditActive" @change="handleValidityTypeChange"
+                            class="!mr-4">日期范围</el-radio>
                         <el-form-item prop="validityRange" class="mb-0">
-                            <el-date-picker
-                                v-model="form.validityRange"
-                                type="daterange"
-                                range-separator="至"
-                                start-placeholder="开始日期"
-                                end-placeholder="结束日期"
-                                value-format="YYYY-MM-DD"
-                                :disabled="isEditActive || form.validityType !== 'range'"
-                                style="width: 320px;"
-                            />
+                            <el-date-picker v-model="form.validityRange" type="daterange" range-separator="至"
+                                start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:ss"
+                                :disabled="isEditActive || form.validityType !== 'range'" style="width: 320px;" />
                         </el-form-item>
                     </div>
-                    
+
                     <div class="flex items-center">
-                        <el-radio v-model="form.validityType" label="days" :disabled="isEditActive" class="!mr-4">有效日期</el-radio>
+                        <el-radio v-model="form.validityType" label="days" :disabled="isEditActive" @change="handleValidityTypeChange"
+                            class="!mr-4">有效日期</el-radio>
                         <div class="flex items-center">
-                            <el-form-item prop="validityDays" class="mb-0 !mr-2">
-                                <el-input
-                                    v-model="form.validityDays"
-                                    placeholder="请输入"
-                                    style="width: 200px;"
-                                    :disabled="isEditActive || form.validityType !== 'days'"
-                                />
+                            <el-form-item prop="effectDays" class="mb-0 !mr-2">
+                                <el-input-number v-model="form.effectDays" placeholder="请输入" style="width: 200px;"
+                                    :min="1" :disabled="isEditActive || form.validityType !== 'days'" />
                             </el-form-item>
                             <span>天</span>
                         </div>
@@ -95,20 +93,21 @@
                 </div>
             </el-form-item>
 
-            <el-form-item label="每人限领" prop="limit">
-                <el-input-number v-model="form.limit" :min="1" placeholder="请输入" class="!w-[200px]"
+            <el-form-item label="每人限领" prop="limitedPerUser">
+                <el-input-number v-model="form.limitedPerUser" :min="1" placeholder="请输入" class="!w-[200px]"
                     :disabled="isEditActive" />
                 <span class="ml-2 text-gray-500">张</span>
             </el-form-item>
 
-            <el-form-item v-if="type === 'surprise'" label="口令" prop="password">
-                <el-input v-model="form.password" placeholder="请输入口令" :disabled="isEditActive" />
+            <el-form-item v-if="type === '2'" label="口令" prop="commandWord">
+                <el-input v-model="form.commandWord" placeholder="请输入口令" :disabled="isEditActive" />
                 <div class="text-gray-400 text-xs mt-1">
                     用户搜索后弹出惊喜红包
                 </div>
             </el-form-item>
         </el-form>
 
+
         <template #footer>
             <div class="flex justify-end items-center">
                 <el-button @click="visible = false">取消</el-button>
@@ -125,130 +124,201 @@
 </template>
 
 <script setup>
-import { ref, computed, reactive, watch } from 'vue';
-import { EleMessage } from 'ele-admin-plus/es';
-import GoodsSelectDialog from '@/views/salesOps/components/GoodsSelectDialog.vue';
-
-const props = defineProps({
-    modelValue: Boolean,
-    type: {
-        type: String,
-        default: 'ordinary', // 'ordinary' | 'surprise'
-    },
-    data: Object,
-});
-
-const emit = defineEmits(['update:modelValue', 'success']);
-
-const visible = computed({
-    get: () => props.modelValue,
-    set: (val) => emit('update:modelValue', val),
-});
-
-const formRef = ref(null);
-const loading = ref(false);
-const productDialogVisible = ref(false);
-
-const isEdit = computed(() => !!props.data?.id);
-const isEditActive = computed(() => !!props.data?.isActive); // Assuming 'isActive' flag passed if active
-
-const dialogTitle = computed(() => {
-    const typeName = props.type === 'surprise' ? '惊喜红包' : '普通红包';
-    return isEdit.value ? `编辑${typeName}` : `新建${typeName}`;
-});
-
-const form = reactive({
-    name: '',
-    quantity: undefined,
-    minAmount: undefined,
-    maxAmount: undefined,
-    amount: undefined,
-    thresholdType: 'none',
-    thresholdAmount: undefined,
-    productType: 'all',
-    products: [],
-    validityType: 'range',
-    validityRange: [],
-    validityDays: undefined,
-    limit: 1,
-    password: '',
-});
-
-const rules = {
-    name: [{ required: true, message: '请输入红包名称', trigger: 'blur' }],
-    quantity: [{ required: true, message: '请输入红包数量', trigger: 'blur' }],
-    amount: [{ required: true, message: '请输入红包面额', trigger: 'blur' }],
-    minAmount: [{ required: true, message: '请输入最小面额', trigger: 'blur' }],
-    maxAmount: [{ required: true, message: '请输入最大面额', trigger: 'blur' }],
-    thresholdAmount: [{ required: true, message: '请输入门槛金额', trigger: 'blur' }],
-    validityRange: [{ required: true, message: '请选择有效期范围', trigger: 'change' }],
-    validityDays: [{ required: true, message: '请输入有效天数', trigger: 'blur' }],
-    limit: [{ required: true, message: '请输入限领数量', trigger: 'blur' }],
-    password: [{ required: true, message: '请输入口令', trigger: 'blur' }],
-};
-
-watch(
-    () => props.modelValue,
-    (val) => {
-        if (val) {
-            resetForm();
-            if (props.data) {
-                Object.assign(form, props.data);
-                // Handle complex types mapping if needed
+    import { ref, computed, reactive, nextTick } from 'vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+    import GoodsSelectDialog from '@/views/salesOps/components/GoodsSelectDialog.vue';
+    import request from '@/utils/request';
+
+    const emit = defineEmits(['success']);
+
+    const visible = ref(false);
+    const type = ref('1'); // '1' (Common) | '2' (Surprise)
+    const dialogData = ref(null);
+
+    const formRef = ref(null);
+    const loading = ref(false);
+    const productDialogVisible = ref(false);
+
+    const isEdit = computed(() => !!dialogData.value?.id);
+    const isEditActive = computed(() => dialogData.value?.status === 1); // Status 1 is Normal/Active
+
+    const dialogTitle = computed(() => {
+        const typeName = (type.value === '2') ? '惊喜红包' : '普通红包';
+        return isEdit.value ? `编辑${typeName}` : `新建${typeName}`;
+    });
+
+    const form = reactive({
+        id: undefined,
+        couponName: '',
+        totalNum: undefined,
+        minMoney: undefined,
+        maxMoney: undefined,
+        faceMoney: undefined,
+        thresholdType: 'none',
+        thresholdMoney: undefined,
+        stackType: '0',
+        scope: '1', // 1: All, 2: Specific
+        products: [],
+        validityType: 'range',
+        validityRange: [],
+        effectDays: undefined,
+        limitedPerUser: 1,
+        commandWord: '',
+    });
+
+    const rules = computed(() => ({
+        couponName: [{ required: true, message: '请输入红包名称', trigger: 'blur' }],
+        totalNum: [{ required: true, message: '请输入红包数量', trigger: 'blur' }],
+        faceMoney: [{ required: true, message: '请输入红包面额', trigger: 'blur' }],
+        minMoney: [{ required: true, message: '请输入最小面额', trigger: 'blur' }],
+        maxMoney: [{ required: true, message: '请输入最大面额', trigger: 'blur' }],
+        thresholdMoney: [{ required: true, message: '请输入门槛金额', trigger: 'blur' }],
+        validityRange: [{ required: form.validityType === 'range', message: '请选择有效期范围', trigger: 'change' }],
+        effectDays: [{ required: form.validityType === 'days', message: '请输入有效天数', trigger: 'blur' }],
+        limitedPerUser: [{ required: true, message: '请输入限领数量', trigger: 'blur' }],
+        commandWord: [{ required: true, message: '请输入口令', trigger: 'blur' }],
+    }));
+
+    const handleValidityTypeChange = () => {
+        if (formRef.value) {
+            formRef.value.clearValidate(['validityRange', 'effectDays']);
+        }
+    };
+
+    const open = (newType, data) => {
+        type.value = newType;
+        dialogData.value = data;
+        visible.value = true;
+
+        resetForm();
+        if (data) {
+            Object.assign(form, data);
+
+            // Handle threshold type
+            form.thresholdType = (form.thresholdMoney && form.thresholdMoney > 0) ? 'full' : 'none';
+
+            // Handle validity type
+            if (form.effectDays && form.effectDays > 0) {
+                form.validityType = 'days';
             } else {
-                // Default values based on type
-                if (props.type === 'surprise') {
-                    form.validityType = 'days';
-                } else {
-                    form.validityType = 'range';
+                form.validityType = 'range';
+                if (form.effectStartTime && form.effectEndTime) {
+                    form.validityRange = [form.effectStartTime, form.effectEndTime];
                 }
             }
+
+            // Handle scope and products (if data contains product info)
+            form.scope = String(form.scope || '1');
+            form.stackType = String(form.stackType || '0');
+
+        } else {
+            // Default values based on type
+            if (type.value === '2') {
+                form.validityType = 'days';
+            } else {
+                form.validityType = 'range';
+            }
         }
-    }
-);
-
-const resetForm = () => {
-    if (formRef.value) formRef.value.resetFields();
-    form.products = [];
-    form.thresholdType = 'none';
-    form.productType = 'all';
-    form.validityType = 'range';
-    form.limit = 1;
-};
-
-const openProductSelect = () => {
-    productDialogVisible.value = true;
-};
-
-const handleProductConfirm = (selected) => {
-    form.products = selected;
-    productDialogVisible.value = false;
-};
-
-const handleSubmit = async () => {
-    if (!formRef.value) return;
-
-    await formRef.value.validate((valid, fields) => {
-        if (valid) {
-            if (props.type === 'surprise') {
-                if (form.minAmount > form.maxAmount) {
-                    EleMessage.error('最小面额不能大于最大面额');
+    };
+
+    const resetForm = () => {
+        if (formRef.value) formRef.value.resetFields();
+        form.id = undefined;
+        form.couponName = '';
+        form.totalNum = undefined;
+        form.minMoney = undefined;
+        form.maxMoney = undefined;
+        form.faceMoney = undefined;
+        form.thresholdType = 'none';
+        form.thresholdMoney = undefined;
+        form.stackType = '0';
+        form.scope = '1';
+        form.products = [];
+        form.validityType = 'range';
+        form.validityRange = [];
+        form.effectDays = undefined;
+        form.limitedPerUser = 1;
+        form.commandWord = '';
+    };
+
+    const openProductSelect = () => {
+        productDialogVisible.value = true;
+    };
+
+    const handleProductConfirm = (selected) => {
+        form.products = selected; // selected should be array of objects or IDs?
+        // Assuming selected is array of objects, and we need ISBNs for API
+        productDialogVisible.value = false;
+    };
+
+    const handleSubmit = async () => {
+        if (!formRef.value) return;
+
+        await formRef.value.validate(async (valid) => {
+            if (valid) {
+                if (type.value === '2' || type.value === 2) {
+                    if (parseFloat(form.minMoney) > parseFloat(form.maxMoney)) {
+                        EleMessage.error('最小面额不能大于最大面额');
+                        return;
+                    }
+                }
+                if (form.scope === '2' && (!form.products || form.products.length === 0)) {
+                    EleMessage.error('请选择适用商品');
                     return;
                 }
+
+                loading.value = true;
+                try {
+                    const payload = {
+                        type: String(type.value),
+                        couponName: form.couponName,
+                        totalNum: form.totalNum,
+                        faceMoney: form.faceMoney,
+                        minMoney: form.minMoney,
+                        maxMoney: form.maxMoney,
+                        thresholdMoney: form.thresholdType === 'none' ? 0 : form.thresholdMoney,
+                        stackType: form.stackType,
+                        scope: form.scope,
+                        isbnList: form.products.map(p => p.isbn || p.id), // Adjust based on product object structure
+                        scenario: '3', // Default to All
+                        effectDays: form.validityType === 'days' ? form.effectDays : 0,
+                        effectStartTime: form.validityType === 'range' ? form.validityRange[0] : null,
+                        effectEndTime: form.validityType === 'range' ? form.validityRange[1] : null,
+                        limitedPerUser: form.limitedPerUser,
+                        commandWord: form.commandWord,
+                    };
+
+                    let res;
+                    if (isEdit.value) {
+                        // Update
+                        res = await request.post('/shop/couponInfo/update', {
+                            id: form.id,
+                            totalNum: form.totalNum,
+                            // Add other fields if update supports them
+                            couponName: form.couponName,
+                            // ...
+                        });
+                    } else {
+                        // Add
+                        res = await request.post('/shop/couponInfo/add', payload);
+                    }
+
+                    if (res.data.code === 200) {
+                        EleMessage.success(isEdit.value ? '修改成功' : '添加成功');
+                        emit('success');
+                        visible.value = false;
+                    } else {
+                        EleMessage.error(res.data.msg || (isEdit.value ? '修改失败' : '添加失败'));
+                    }
+                } catch (error) {
+                    console.error(error);
+                    EleMessage.error(isEdit.value ? '修改失败' : '添加失败');
+                } finally {
+                    loading.value = false;
+                }
             }
-            if (form.productType === 'specific' && form.products.length === 0) {
-                EleMessage.error('请选择适用商品');
-                return;
-            }
+        });
+    };
 
-            loading.value = true;
-            setTimeout(() => {
-                loading.value = false;
-                EleMessage.success('保存成功');
-                emit('success', { ...form, type: props.type });
-                visible.value = false;
-            }, 500);
-        }
-    });
-};
+    defineExpose({ open });
 </script>

+ 73 - 0
src/views/marketing/redPacket/components/RedPacketLinkDialog.vue

@@ -0,0 +1,73 @@
+<template>
+    <ele-modal v-model="visible" title="链接" :width="500" :footer="null">
+        <div class="flex flex-col items-center justify-center py-6">
+            <el-input v-model="linkUrl" readonly placeholder="请输入" class="mb-6 w-full" />
+            <el-button type="success" @click="handleCopy" class="!px-8 !py-5 text-base">复制链接</el-button>
+        </div>
+    </ele-modal>
+</template>
+
+<script setup>
+    import { ref } from 'vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+    import request from '@/utils/request';
+    import useClipboard from 'vue-clipboard3';
+
+    const visible = ref(false);
+    const redPacketId = ref('');
+    const linkUrl = ref('');
+    const { toClipboard } = useClipboard();
+
+    const open = async (id) => {
+        redPacketId.value = id;
+        visible.value = true;
+        if (id) {
+            await fetchLink();
+        } else {
+            linkUrl.value = '';
+        }
+    };
+
+    const fetchLink = async () => {
+        try {
+            const res = await request.get('/shop/couponInfo/getCouponUrl', {
+                params: {
+                    couponId: redPacketId.value
+                }
+            });
+            if (res.data.code === 200) {
+                linkUrl.value = res.data.data;
+            } else {
+                EleMessage.error(res.data.msg || '获取链接失败');
+            }
+        } catch (error) {
+            console.error(error);
+            EleMessage.error('获取链接失败');
+        }
+    };
+
+    const handleCopy = async () => {
+        if (!linkUrl.value) {
+            EleMessage.warning('链接为空');
+            return;
+        }
+        try {
+            await toClipboard(linkUrl.value);
+            EleMessage.success('复制成功');
+            visible.value = false;
+        } catch (e) {
+            EleMessage.error('复制失败');
+        }
+    };
+
+    defineExpose({ open });
+</script>
+
+<style scoped>
+    :deep(.el-button--success) {
+        --el-button-bg-color: #3ab54a;
+        --el-button-border-color: #3ab54a;
+        --el-button-hover-bg-color: #4ac75a;
+        --el-button-hover-border-color: #4ac75a;
+    }
+</style>

+ 41 - 0
src/views/marketing/redPacket/components/RedPacketLogDialog.vue

@@ -0,0 +1,41 @@
+<template>
+    <ele-modal v-model="visible" title="操作记录" :width="800" :footer="null">
+        <common-table ref="tableRef" :columns="columns" :page-config="pageConfig" :bodyStyle="{ padding: 0 }">
+        </common-table>
+    </ele-modal>
+</template>
+
+<script setup>
+    import { ref, reactive } from 'vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+    import CommonTable from '@/components/CommonPage/CommonTable.vue';
+    import request from '@/utils/request';
+
+    const visible = ref(false);
+    const redPacketId = ref('');
+    const tableRef = ref(null);
+
+    const columns = ref([
+        { label: '操作账号', prop: 'createName', minWidth: 120 },
+        { label: '操作时间', prop: 'createTime', minWidth: 160 },
+        { label: '操作内容', prop: 'content', minWidth: 200, showOverflowTooltip: true }
+    ]);
+
+    const pageConfig = reactive({
+        rowKey: 'id',
+        tool: false,
+        pageUrl: '/shop/couponInfo/getUpdateLogPagelist',
+        params: { couponId: '' }
+    });
+
+    const open = (id) => {
+        redPacketId.value = id;
+        visible.value = true;
+        if (id) {
+            pageConfig.params.couponId = id;
+            tableRef.value?.reload();
+        }
+    };
+
+    defineExpose({ open });
+</script>

+ 144 - 185
src/views/marketing/redPacket/index.vue

@@ -8,9 +8,9 @@
                         <div>
                             <div class="text-xl font-bold mb-2">普通红包</div>
                             <div class="text-gray-500 text-sm mb-4">所有商品通用</div>
-                            <div class="text-gray-600">已建数量:12</div>
+                            <div class="text-gray-600">已建数量:{{ count1 }}</div>
                         </div>
-                        <el-button type="primary" @click="handleCreate('ordinary')" :icon="Lightning">新建</el-button>
+                        <el-button type="primary" @click="handleCreate('1')" :icon="Lightning">新建</el-button>
                     </div>
                     <!-- Decorative Icon/Shape if needed -->
                 </div>
@@ -19,9 +19,9 @@
                         <div>
                             <div class="text-xl font-bold mb-2">惊喜红包</div>
                             <div class="text-gray-500 text-sm mb-4">所有商品可用</div>
-                            <div class="text-gray-600">已建数量:12</div>
+                            <div class="text-gray-600">已建数量:{{ count2 }}</div>
                         </div>
-                        <el-button type="primary" @click="handleCreate('surprise')" :icon="Lightning">新建
+                        <el-button type="primary" @click="handleCreate('2')" :icon="Lightning">新建
                         </el-button>
                     </div>
                 </div>
@@ -29,22 +29,19 @@
 
             <!-- Tabs -->
             <el-tabs v-model="activeTab" @tab-click="handleTabClick" class="mb-2">
-                <el-tab-pane label="红包列表" name="all"></el-tab-pane>
-                <el-tab-pane label="惊喜红包" name="surprise"></el-tab-pane>
-                <el-tab-pane label="普通红包" name="ordinary"></el-tab-pane>
+                <el-tab-pane label="红包列表" :name="undefined"></el-tab-pane>
+                <el-tab-pane label="惊喜红包" name="2"></el-tab-pane>
+                <el-tab-pane label="普通红包" name="1"></el-tab-pane>
             </el-tabs>
 
             <!-- Table -->
-            <common-table ref="tableRef" :columns="columns" :datasource="datasource"
-                :page-config="{ rowKey: 'id', tool: false }" :bodyStyle="{ padding: 0 }">
+            <common-table ref="tableRef" :columns="columns" :page-config="pageConfig" :bodyStyle="{ padding: 0 }">
                 <template #toolbar>
                     <div class="flex items-center space-x-4">
                         <el-input v-model="searchForm.name" placeholder="名称" class="!w-[200px]" clearable />
-                        <el-select v-model="searchForm.status" placeholder="请选择活动状态" class="!w-[200px]" clearable>
-                            <el-option label="未开始" value="pending" />
-                            <el-option label="进行中" value="active" />
-                            <el-option label="已结束" value="ended" />
-                        </el-select>
+
+                        <dict-data code="red_packet_status" v-model="searchForm.status" placeholder="请选择活动状态"
+                            class="!w-[200px]"></dict-data>
                         <el-button type="primary" @click="reload">搜索</el-button>
                         <el-button @click="reset" style="margin-left: 10px;">重置</el-button>
                     </div>
@@ -52,37 +49,33 @@
 
                 <!-- Columns Slots -->
                 <template #status="{ row }">
-                    <span :class="{
-                        'text-gray-400': row.status === 'pending',
-                        'text-green-500': row.status === 'active',
-                        'text-red-500': row.status === 'ended'
-                    }">
-                        {{ getStatusLabel(row.status) }}
-                    </span>
+                    <dict-data code="red_packet_status" v-model="row.status" type="tag"></dict-data>
                 </template>
 
                 <template #amount="{ row }">
-                    <span v-if="row.type === 'surprise'">{{ row.minAmount }}-{{ row.maxAmount }}元</span>
-                    <span v-else>{{ row.amount }}元</span>
+                    <span v-if="row.type === 2 || row.type === '2'">{{ row.minMoney }}-{{ row.maxMoney }}元</span>
+                    <span v-else>{{ row.faceMoney }}元</span>
                 </template>
 
                 <template #threshold="{ row }">
-                    <span v-if="row.thresholdType === 'none'">无门槛</span>
-                    <span v-else>满{{ row.thresholdAmount }}减{{ row.amount }}</span>
+                    <span v-if="!row.thresholdMoney || row.thresholdMoney === 0">无门槛</span>
+                    <span v-else>满{{ row.thresholdMoney }}可用</span>
                 </template>
 
                 <template #distribution="{ row }">
-                    <div>已领 {{ row.distributed }}</div>
-                    <div class="text-gray-400 text-xs">总数 {{ row.total }}</div>
+                    <div v-if="row.restNum !== undefined">已领 {{ (row.totalNum || 0) - row.restNum }}</div>
+                    <div v-else-if="row.sendNum !== undefined">已领 {{ row.sendNum }}</div>
+                    <div v-else>已领 -</div>
+                    <div class="text-gray-400 text-xs">总数 {{ row.totalNum }}</div>
                 </template>
 
                 <template #time="{ row }">
-                    <div v-if="row.validityType === 'range'">
-                        <div>起:{{ row.startTime }}</div>
-                        <div>止:{{ row.endTime }}</div>
+                    <div v-if="!row.effectDays || row.effectDays === 0">
+                        <div>起:{{ row.effectStartTime }}</div>
+                        <div>止:{{ row.effectEndTime }}</div>
                     </div>
                     <div v-else>
-                        自领取之后{{ row.validityDays }}自然日之内有效
+                        自领取之后{{ row.effectDays }}自然日之内有效
                     </div>
                 </template>
 
@@ -91,9 +84,7 @@
                         <div class="flex space-x-2">
                             <el-button type="primary" link @click="handleViewLink(row)">查看链接</el-button>
                             <el-button type="warning" link @click="handleCopyData(row)">复制</el-button>
-                            <el-button type="primary" link @click="handleEdit(row)">
-                                {{ row.status === 'active' ? '修改' : '修改' }}
-                            </el-button>
+                            <el-button type="primary" link @click="handleEdit(row)">修改</el-button>
                         </div>
                         <div class="flex space-x-2">
                             <el-button type="success" link @click="handleShowData(row)">数据</el-button>
@@ -106,176 +97,144 @@
             </common-table>
         </div>
 
-        <RedPacketDialog v-model="dialogVisible" :type="dialogType" :data="dialogData" @success="reload" />
-        <RedPacketDataDialog v-model="dataDialogVisible" :red-packet-id="currentRedPacketId" />
+        <RedPacketDialog ref="redPacketDialogRef" @success="reload" />
+        <RedPacketDataDialog ref="redPacketDataDialogRef" />
+        <RedPacketLinkDialog ref="redPacketLinkDialogRef" />
+        <RedPacketLogDialog ref="redPacketLogDialogRef" />
     </ele-page>
 </template>
 
 <script setup>
-import { ref, reactive } from 'vue';
-import { EleMessage } from 'ele-admin-plus/es';
-import CommonTable from '@/components/CommonPage/CommonTable.vue';
-import RedPacketDialog from './components/RedPacketDialog.vue';
-import RedPacketDataDialog from './components/RedPacketDataDialog.vue';
-import { Lightning } from '@element-plus/icons-vue';
-
-defineOptions({ name: 'RedPacketManage' });
-
-const tableRef = ref(null);
-const activeTab = ref('all');
-const dialogVisible = ref(false);
-const dialogType = ref('ordinary');
-const dialogData = ref(null);
-
-// Data Dialog
-const dataDialogVisible = ref(false);
-const currentRedPacketId = ref('');
-
-const handleShowData = (row) => {
-    currentRedPacketId.value = row.id;
-    dataDialogVisible.value = true;
-};
-
-const searchForm = reactive({
-    name: '',
-    status: '',
-});
-
-const columns = ref([
-    { label: '活动名称', prop: 'name', minWidth: 150 },
-    { label: '状态', slot: 'status', width: 100 },
-    { label: '红包面额', slot: 'amount', minWidth: 100 },
-    { label: '优惠方式', slot: 'threshold', minWidth: 120 },
-    { label: '发放量', slot: 'distribution', minWidth: 120 },
-    { label: '使用时间', slot: 'time', minWidth: 220 },
-    { label: '操作', slot: 'action', width: 200, fixed: 'right' },
-]);
-
-const getStatusLabel = (status) => {
-    const map = {
-        pending: '未开始',
-        active: '领取中',
-        ended: '已结束',
+    import { ref, reactive, onMounted } from 'vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+    import CommonTable from '@/components/CommonPage/CommonTable.vue';
+    import RedPacketDialog from './components/RedPacketDialog.vue';
+    import RedPacketDataDialog from './components/RedPacketDataDialog.vue';
+    import RedPacketLinkDialog from './components/RedPacketLinkDialog.vue';
+    import RedPacketLogDialog from './components/RedPacketLogDialog.vue';
+    import { Lightning } from '@element-plus/icons-vue';
+    import request from '@/utils/request';
+
+    defineOptions({ name: 'RedPacketManage' });
+
+    const tableRef = ref(null);
+    const activeTab = ref(undefined);
+    const count1 = ref(0);
+    const count2 = ref(0);
+
+    // Dialog Refs
+    const redPacketDialogRef = ref(null);
+    const redPacketDataDialogRef = ref(null);
+    const redPacketLinkDialogRef = ref(null);
+    const redPacketLogDialogRef = ref(null);
+
+    const handleShowData = (row) => {
+        redPacketDataDialogRef.value?.open(row.id);
     };
-    return map[status] || status;
-};
-
-// Mock Data Source
-const datasource = async ({ page, limit }) => {
-    // Simulate API delay
-    await new Promise(resolve => setTimeout(resolve, 300));
 
-    let list = [
-        {
-            id: 1,
-            name: '普通红包1',
-            type: 'ordinary',
-            status: 'active',
-            amount: 5,
-            thresholdType: 'full',
-            thresholdAmount: 15,
-            distributed: 2986,
-            total: 100000,
-            validityType: 'range',
-            startTime: '2024-05-20 00:00:00',
-            endTime: '2024-12-31 23:59:59',
-            isActive: true,
-        },
-        {
-            id: 2,
-            name: '惊喜红包1',
-            type: 'surprise',
-            status: 'active',
-            minAmount: 1,
-            maxAmount: 2,
-            thresholdType: 'none',
-            distributed: 2986,
-            total: 100000,
-            validityType: 'days',
-            validityDays: 3,
-            isActive: true,
-            password: '惊喜',
-        },
-        {
-            id: 3,
-            name: '普通红包-未开始',
-            type: 'ordinary',
-            status: 'pending',
-            amount: 10,
-            thresholdType: 'none',
-            distributed: 0,
-            total: 500,
-            validityType: 'range',
-            startTime: '2024-12-01 00:00:00',
-            endTime: '2024-12-31 23:59:59',
-            isActive: false,
+    const searchForm = reactive({
+        name: '',
+        status: '',
+    });
+
+    const columns = ref([
+        { label: '活动名称', prop: 'couponName', minWidth: 150 },
+        { label: '状态', slot: 'status', width: 100 },
+        { label: '红包面额', slot: 'amount', minWidth: 100 },
+        { label: '优惠方式', slot: 'threshold', minWidth: 120 },
+        { label: '发放量', slot: 'distribution', minWidth: 120 },
+        { label: '是否可以叠加使用', slot: 'stackType', minWidth: 120, formatter: (row) => row.stackType === '1' ? '是' : '否' },
+        { label: '使用时间', slot: 'time', minWidth: 220 },
+        { label: '操作', slot: 'action', width: 200, fixed: 'right' },
+    ]);
+
+    const fetchCounts = async () => {
+        try {
+            const res1 = await request.get('/shop/couponInfo/pagelist?type=1&page=1&limit=1');
+            if (res1.data.code === 200) {
+                count1.value = res1.data.total || 0;
+            }
+
+            const res2 = await request.get('/shop/couponInfo/pagelist?type=2&page=1&limit=1');
+            if (res2.data.code === 200) {
+                count2.value = res2.data.total || 0;
+            }
+        } catch (error) {
+            console.error('Failed to fetch counts:', error);
         }
-    ];
-
-    // Filter by tab
-    if (activeTab.value !== 'all') {
-        list = list.filter(item => item.type === activeTab.value);
-    }
+    };
 
-    // Filter by search
-    if (searchForm.name) {
-        list = list.filter(item => item.name.includes(searchForm.name));
-    }
-    if (searchForm.status) {
-        list = list.filter(item => item.status === searchForm.status);
-    }
+    const pageConfig = reactive({
+        pageUrl: '/shop/couponInfo/pagelist',
+        rowKey: 'id',
+        params: {
+            type: activeTab.value,
+        },
+    });
 
-    return list;
-};
+    const reload = (where) => {
+        tableRef.value?.reload(where);
+        fetchCounts();
+    };
 
-const reload = () => {
-    tableRef.value?.reload();
-};
+    const reset = () => {
+        searchForm.name = '';
+        searchForm.status = '';
+        reload();
+    };
 
-const reset = () => {
-    searchForm.name = '';
-    searchForm.status = '';
-    reload();
-};
+    const handleTabClick = () => {
+        pageConfig.params.type = activeTab.value;
+        reload();
+    };
 
-const handleTabClick = () => {
-    reload();
-};
+    const handleCreate = (type) => {
+        redPacketDialogRef.value?.open(type, null);
+    };
 
-const handleCreate = (type) => {
-    dialogType.value = type;
-    dialogData.value = null;
-    dialogVisible.value = true;
-};
+    const handleEdit = (row) => {
+        redPacketDialogRef.value?.open(String(row.type), { ...row });
+    };
 
-const handleEdit = (row) => {
-    dialogType.value = row.type;
-    dialogData.value = { ...row }; // Clone data
-    dialogVisible.value = true;
-};
+    const handleViewLink = (row) => {
+        redPacketLinkDialogRef.value?.open(row.id);
+    };
 
-const handleViewLink = (row) => {
-    EleMessage.info(`查看链接: ${row.id}`);
-};
+    const handleCopyData = (row) => {
+        EleMessage.success('复制数据成功');
+    };
 
-const handleCopyData = (row) => {
-    EleMessage.success('复制数据成功');
-};
+    const handleOperationRecord = (row) => {
+        redPacketLogDialogRef.value?.open(row.id);
+    };
 
-const handleOperationRecord = (row) => {
-    EleMessage.info('查看操作记录');
-};
+    const handleEnd = (row) => {
+        EleMessage.confirm(
+            '结束后,该优惠券不能再被领取,领券链接失效(请及时删除推广或装修),但已领取的优惠券仍能使用。是否确认结束优惠券?',
+            { title: '结束活动', type: 'warning' }
+        )
+            .then(async () => {
+                try {
+                    const res = await request.post('/shop/couponInfo/end', { couponId: row.id });
+                    if (res.data.code === 200) {
+                        EleMessage.success('结束活动成功');
+                        reload();
+                    } else {
+                        EleMessage.error(res.data.msg || '结束活动失败');
+                    }
+                } catch (error) {
+                    console.error(error);
+                    EleMessage.error('结束活动失败');
+                }
+            })
+            .catch(() => { });
+    };
 
-const handleEnd = (row) => {
-    EleMessage.confirm('确定要结束该活动吗?')
-        .then(() => {
-            EleMessage.success('活动已结束');
-            reload();
-        })
-        .catch(() => { });
-};
+    onMounted(() => {
+        fetchCounts();
+    });
 </script>
 
 <style scoped>
-/* Add any custom styles here */
+    /* Add any custom styles here */
 </style>

+ 4 - 4
src/views/marketing/shareDiscount/rules/index.vue

@@ -141,7 +141,7 @@
         loading.value = true;
         try {
             const res = await request.get('/activity/reduce/manage/rule/get');
-            if (res.data.code === 0 || res.data.code === 200) {
+            if (res.data.code === 200) {
                 const data = res.data.data;
                 Object.assign(form, data);
                 form.upsellValidScenario = data.upsellValidScenario.map(v => v.toString());
@@ -160,7 +160,7 @@
     const updateRule = async () => {
         try {
             const res = await request.post('/activity/reduce/manage/rule/update', form);
-            if (res.data.code === 0 || res.data.code === 200) {
+            if (res.data.code === 200) {
                 EleMessage.success('保存成功');
                 form.code = '';
             } else {
@@ -175,7 +175,7 @@
     const handleResendCode = async () => {
         try {
             const res = await request.post('/activity/reduce/manage/rule/getCode');
-            if (res.data.code === 0 || res.data.code === 200) {
+            if (res.data.code === 200) {
                 if (res.data.data === 1) {
                     EleMessage.success('验证码已发送');
                     startCooldown();
@@ -200,7 +200,7 @@
             } else {
                 // Check if verification is needed
                 const res = await request.post('/activity/reduce/manage/rule/getCode');
-                if (res.data.code === 0 || res.data.code === 200) {
+                if (res.data.code === 200) {
                     if (res.data.data === 1) {
                         needVerification.value = true;
                         EleMessage.success('验证码已发送,请输入验证码');