Jelajahi Sumber

企微裂变菜单页面and联调;

郭鹏飞 7 bulan lalu
induk
melakukan
bff6256d81

+ 1 - 0
package.json

@@ -61,6 +61,7 @@
     "fabric": "^4.6.0",
     "file-saver": "2.0.1",
     "fuse.js": "3.4.4",
+    "html2canvas": "^1.4.1",
     "jquery": "^3.7.1",
     "js-cookie": "2.2.0",
     "jsencrypt": "3.0.0-rc.1",

+ 74 - 0
src/api/activity/index.js

@@ -0,0 +1,74 @@
+import request from '@/utils/request'
+const service = window.lwConfig.services.system
+
+// 列表
+export function getList(data) {
+  return request({
+    url: service + '/system/weGroupFission/list',
+    method: 'post',
+    data,
+  })
+}
+
+// 新增、修改
+export function saveOrUpdate(data) {
+  return request({
+    url: service + '/system/weGroupFission/addOrUpdate',
+    method: 'post',
+    data,
+  })
+}
+
+// 删除
+export function netDelete(data) {
+  return request({
+    url: service + '/system/weGroupFission/delete',
+    method: 'delete',
+    params: data,
+  })
+}
+
+// 详情
+export function queryById(data) {
+  return request({
+    url: service + '/system/weGroupFission/queryById',
+    method: 'get',
+    params: data,
+  })
+}
+
+// 小程序短链
+export function getWechatGroupFissionUrl(data) {
+  return request({
+    url: service + '/system/weGroupFission/getWechatGroupFissionUrl',
+    method: 'post',
+    data,
+  })
+}
+
+// 获取下拉数据 type 1.商户 2.权益券 3.抽奖
+export function getDictListByType(data) {
+  return request({
+    url: service + '/system/weGroupFission/getDictListByType',
+    method: 'get',
+    params: data,
+  })
+}
+
+// 查询企业微信群列表
+export function getWecomGroupList(data) {
+  return request({
+    url: service + '/system/weGroupFission/getWecomGroupList',
+    method: 'get',
+    params: data,
+  })
+}
+
+// 查询企业微信员工信息列表
+export function getWecomEmployeeList(data) {
+  return request({
+    url: service + '/system/weGroupFission/getWecomEmployeeList',
+    method: 'get',
+    params: data,
+  })
+}

+ 176 - 0
src/views/weGroupFission/list.vue

@@ -0,0 +1,176 @@
+<template>
+    <div class="app-container">
+        <div class="g-card table-page-search-wrapper">
+            <el-form ref="queryForm" size="small" :inline="true" :model="queryParams" @keyup.enter.native="searchQuery">
+                <el-form-item label="活动标题" prop="title">
+                    <el-input v-model="queryParams.title" placeholder="请输入活动标题" clearable></el-input>
+                </el-form-item>
+                <el-form-item label="活动时间" prop="dateRange">
+                    <el-date-picker
+                        type="daterange"
+                        start-placeholder="开始日期"
+                        end-placeholder="结束日期"
+                        value-format="yyyy-MM-dd"
+                        v-model="queryParams.dateRange"
+                    />
+                </el-form-item>
+                <el-form-item label="创建时间" prop="createDate">
+                    <el-date-picker
+                        type="date"
+                        placeholder="选择日期"
+                        value-format="yyyy-MM-dd"
+                        v-model="queryParams.createDate"
+                    />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" @click="searchQuery">查询</el-button>
+                    <el-button icon="el-icon-refresh" @click="searchReset">重置</el-button>
+                </el-form-item>
+            </el-form>
+        </div>
+
+        <div class="g-card">
+            <div class="table-operator">
+                <el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
+            </div>
+
+            <el-table bordered :data="list" v-loading="loading">
+                <!-- <el-table-column type="index" width="60" label="序号" align="center"></el-table-column> -->
+
+                <el-table-column prop="title" label="活动标题" align="center" show-overflow-tooltip></el-table-column>
+                <el-table-column prop="beginDate" label="活动时间" align="center">
+                    <template slot-scope="scope">
+                        <span>{{ `${scope.row.beginDate} ~ ${scope.row.endDate}` }}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="createUserName" label="创建人" align="center"></el-table-column>
+                <el-table-column prop="createTime" label="创建时间" align="center"></el-table-column>
+                <el-table-column prop="activityStatus" label="状态" align="center" width="100">
+                    <!-- <template slot-scope="scope">
+                        <el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
+                            {{ scope.row.status === 1 ? '启用' : '停用' }}
+                        </el-tag>
+                    </template> -->
+                </el-table-column>
+
+                <el-table-column label="操作" align="center" width="250" fixed="right">
+                    <template slot-scope="scope">
+                        <el-button type="text" size="small" @click="handleShare(scope.row)">分享</el-button>
+
+                        <el-button type="text" size="small" @click="handleDetail(scope.row)">详情</el-button>
+
+                        <el-button type="text" size="small" @click="handleEdit(scope.row)" v-if="scope.row.activityStatus == '未开始'">编辑</el-button>
+
+                        <el-popconfirm title="是否确认删除?" @confirm="handleDelete(scope.row)" style="margin: 0 10px;"
+                            v-if="scope.row.activityStatus == '未开始'">
+                            <el-button slot="reference" type="text" size="small" style="color: #f56c6c;">删除</el-button>
+                        </el-popconfirm>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <pagination
+                v-show="total > 0"
+                :total="total"
+                :page.sync="queryParams.pageNum"
+                :limit.sync="queryParams.pageSize"
+                @pagination="getData"
+            />
+        </div>
+
+        <GroupCheckInModal ref="modalForm" @success="onMFormOk" />
+        <GroupCheckInShare ref="modalShare" />
+    </div>
+</template>
+
+<script>
+import { getList, netDelete } from '@/api/activity/index';
+import GroupCheckInModal from './modules/modal.vue';
+import GroupCheckInShare from './modules/share.vue';
+
+export default {
+    name: '',
+    components: {
+        GroupCheckInModal,
+        GroupCheckInShare,
+    },
+    data() {
+        return {
+            ids: [],
+            list: [],
+            total: 0,
+            loading: false,
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                dateRange: [],
+            },
+        };
+    },
+    created() {
+        this.getData();
+    },
+    methods: {
+        getData() {
+            const { dateRange, ...others } = this.queryParams;
+            let params = {
+                ...others,
+            };
+            if (dateRange.length > 0) {
+                params.beginDate = dateRange[0];
+                params.endDate = dateRange[1];
+            }
+            this.loading = true;
+            getList(params).then((res) => {
+                if (res.code === 200) {
+                    this.list = res.rows;
+                    this.total = res.total - 0;
+                }
+            }).finally(() => {
+                this.loading = false;
+            });
+        },
+        // 搜索
+        searchQuery() {
+            this.queryParams.pageNum = 1;
+            this.getData();
+        },
+        // 重置
+        searchReset() {
+            this.resetForm("queryForm");
+            this.searchQuery();
+        },
+        handleAdd() {
+            this.$refs.modalForm.add();
+            this.$refs.modalForm.title = "新增";
+        },
+        handleEdit(record) {
+            this.$refs.modalForm.edit(record);
+            this.$refs.modalForm.title = "编辑";
+        },
+        handleDetail(record) {
+            this.$refs.modalForm.edit(record);
+            this.$refs.modalForm.title = "详情";
+        },
+        handleShare(record) {
+            this.$refs.modalShare.show(record);
+        },
+        handleDelete(record) {
+            netDelete({ id: record.id }).then((res) => {
+                if (res.code === 200) {
+                    this.$message.success('删除成功');
+                    this.getData();
+                } else {
+                    this.$message.warning(res.msg);
+                }
+            });
+        },
+        onMFormOk() {
+            this.getData();
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 169 - 0
src/views/weGroupFission/modules/CompExtras.vue

@@ -0,0 +1,169 @@
+<template>
+    <div class="custom-list">
+        <div style="text-align: right; margin-bottom: 10px;">
+            <el-button type="primary" size="mini" plain icon="el-icon-plus" @click="addExtra">添加奖励</el-button>
+        </div>
+
+        <div class="extra-row" v-for="(item, index) in innerValue" :key="index">
+            <div class="extra-line">
+                <span class="label">完成:</span>
+                <el-select v-model="item.tierIndex" placeholder="请选择阶梯" size="small" style="width: 200px"
+                    @change="handleChange">
+                    <el-option v-for="opt in options" :key="opt.value" :label="opt.label" :value="opt.value"
+                        :disabled="innerValue.some(t => t.tierIndex === opt.value && t !== item)">
+                    </el-option>
+                </el-select>
+                <i class="el-icon-delete delete-icon" @click="removeExtra(index)"></i>
+            </div>
+
+            <div class="extra-line" style="margin-top: 10px;">
+                <span class="label">奖励:</span>
+                <el-select v-model="item.rewardType" placeholder="类型" size="small"
+                    style="width: 200px; margin-right: 10px;" @change="handleChange">
+                    <el-option v-for="item in dicts.rewardType" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue"></el-option>
+                </el-select>
+
+                <div class="dynamic-input">
+                    <el-input-number v-if="item.rewardType == 1" :min="0" v-model="item.rewardValue"
+                        controls-position="right" size="small" placeholder="输入金额"
+                        @change="handleChange"></el-input-number>
+                    <span v-if="item.rewardType == 1" style="margin-left:5px">元</span>
+
+                    <el-select v-else-if="item.rewardType == 2" v-model="item.rewardValue" placeholder="选择权益券"
+                        size="small" @change="handleChange">
+                        <el-option v-for="item in dicts.rightInfoId" :key="item.val" :label="item.name" :value="item.val"></el-option>
+                    </el-select>
+
+                    <el-select v-else-if="item.rewardType == 3" v-model="item.rewardValue" placeholder="选择抽奖活动"
+                        size="small" @change="handleChange">
+                        <el-option v-for="item in dicts.lotteryActivityId" :key="item.val" :label="item.name" :value="item.val"></el-option>
+                    </el-select>
+                </div>
+            </div>
+        </div>
+
+        <div v-if="innerValue.length === 0"
+            style="color: #999; text-align: center; padding: 10px; background: #fafafa; border-radius: 4px;">
+            暂无额外奖励,点击右上角添加
+        </div>
+    </div>
+</template>
+
+<script>
+import { 
+    getDicts 
+} from '@/api/system/menu';
+import { 
+    getDictListByType,
+} from '@/api/activity/index';
+
+export default {
+    name: 'CompExtras',
+    props: {
+        value: {
+            type: Array,
+            default: () => []
+        },
+        options: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data() {
+        return {
+            innerValue: [],
+            dicts: {
+                rewardType: [],
+                rightInfoId: [],
+                lotteryActivityId: []
+            }
+        };
+    },
+    watch: {
+        value: {
+            handler(val) {
+                this.innerValue = val ? JSON.parse(JSON.stringify(val)) : [];
+            },
+            immediate: true,
+            deep: true
+        }
+    },
+    created() {
+        this.initData();
+    },
+    methods: {
+        initData() {
+            // 奖励方式
+            getDicts('reward_groupFission_method').then((res) => {
+                this.dicts.rewardType = res.data;
+            });
+            // 权益券
+            getDictListByType({ type: 2 }).then((res) => {
+                this.dicts.rightInfoId = res.data;
+            });
+            // 抽奖
+            getDictListByType({ type: 3 }).then((res) => {
+                this.dicts.lotteryActivityId = res.data;
+            });
+        },
+        addExtra() {
+            this.innerValue.push({
+                tierIndex: undefined,
+                rewardType: '1',
+                rewardValue: undefined
+            });
+            this.handleChange();
+        },
+        removeExtra(index) {
+            this.innerValue.splice(index, 1);
+            this.handleChange();
+        },
+        handleChange() {
+            this.$emit('input', this.innerValue);
+            this.$emit('change', this.innerValue);
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.custom-list {
+    width: 100%;
+    max-width: 800px;
+
+    .extra-row {
+        color: #606266;
+        padding: 15px;
+        margin-bottom: 15px;
+        border: 1px solid #e8e8e8;
+        border-radius: 4px;
+        background: #fff;
+        position: relative;
+
+        .extra-line {
+            display: flex;
+            align-items: center;
+        }
+
+        .label {
+            width: 80px;
+            text-align: right;
+            padding-right: 10px;
+        }
+
+        .delete-icon {
+            font-size: 16px;
+            color: #f56c6c;
+            cursor: pointer;
+            position: absolute;
+            top: 15px;
+            right: 15px;
+        }
+
+        .dynamic-input {
+            flex: 1;
+            display: flex;
+        }
+    }
+}
+</style>

+ 310 - 0
src/views/weGroupFission/modules/CompPreview.vue

@@ -0,0 +1,310 @@
+<template>
+    <div class="poster-preview">
+        <div class="poster-container" id="poster-box">
+            <img :src="`${staticUrl}/static/activity/bg2.png`" class="bg-img" />
+
+            <div class="header-bg">
+                <img :src="`${staticUrl}/static/activity/img-money.png`" class="header-img" />
+            </div>
+
+            <div class="poster-content">
+                <div class="poster-title">{{ formData.title || '请填写活动名称' }}</div>
+
+                <div class="poster-badge">
+                    <span class="dot">●</span>
+                    <span>活动说明</span>
+                </div>
+
+                <div class="poster-desc">{{ formData.activityInstructions || '请填写活动说明' }}</div>
+
+                <div class="poster-table-wrapper">
+                    <table class="poster-table" cellspacing="0" cellpadding="0">
+                        <thead>
+                            <tr>
+                                <th>有效邀请人数</th>
+                                <th>现金奖励金额</th>
+                                <th>额外福利</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr v-for="(row, idx) in posterTableData" :key="idx">
+                                <td>{{ row.people }}</td>
+                                <td>{{ row.money }}</td>
+                                <td>{{ row.welfare }}</td>
+                            </tr>
+                            <tr v-if="posterTableData.length === 0">
+                                <td colspan="3" style="text-align: center; color: #999;">暂无奖励配置</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+
+            <div class="poster-footer" :style="{ backgroundImage: `url(${staticUrl}/static/activity/bg3.png)` }">
+                <div class="qr-box">
+                    <div id="qrcode"></div>
+                </div>
+                <div class="info-right">
+                    <div class="brand">
+                        <!-- <div class="brand-logo" style="background: #eee;"></div> -->
+                        <span>{{ formData.merchantName || '--' }}</span>
+                    </div>
+                    <div class="date-tag">{{ formData.timeRange.join(' ~ ') || '?? ~ ??' }}</div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import QRCode from 'qrcodejs2';
+export default {
+    name: 'CompPreview',
+    props: {
+        formData: {
+            type: Object,
+            default: () => ({})
+        },
+        staticUrl: {
+            type: String,
+            default: 'https://life.baoxianzhanggui.com/pro-img/cust'
+        }
+    },
+    data() {
+        return {
+            
+        };
+    },
+    computed: {
+        posterTableData() {
+            const inviteTiers = this.formData.inviteTiers || [];
+            const inviteExtras = this.formData.inviteExtras || [];
+
+            return inviteTiers.map((tier, index) => {
+                // 查找该阶梯对应的额外奖励
+                const extras = inviteExtras.filter(e => e.tierIndex === index);
+                let extraText = '无';
+
+                if (extras.length > 0) {
+                    const detail = extras.map(e => {
+                        let t = '';
+                        if (e.rewardType == 1) t = `${e.rewardValue !== undefined ? e.rewardValue : ' '}元现金`;
+                        if (e.rewardType == 2) t = '权益券';
+                        if (e.rewardType == 3) t = '抽奖活动';
+                        if (e.rewardName) t += ` - ${e.rewardName}`;
+                        return t;
+                    }).join(' + ');
+                    extraText = `额外加赠 ${detail}`;
+                }
+
+                return {
+                    people: `${tier.count || 0}人`,
+                    money: `${tier.reward || 0}元现金`,
+                    welfare: extraText
+                };
+            });
+        }
+    },
+    mounted() {
+        // this.getQRCode();
+    },
+    methods: {
+        // 初始化二维码
+        getQRCode(url) {
+            new QRCode('qrcode', {
+                text: url, 
+                width: 64, 
+                height: 64, 
+                colorDark: '#000000', // 二维码前景色
+                colorLight: '#ffffff', // 二维码背景色
+                correctLevel: QRCode.CorrectLevel.H // 容错级别:L/M/Q/H(H最高)
+            });
+        },
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.poster-preview {
+    width: 375px;
+    height: 750px;
+    flex-shrink: 0;
+    background-color: #f0f2f5;
+    border: 4px solid #000;
+    border-radius: 40px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    position: sticky;
+    top: 0;
+
+    &::-webkit-scrollbar {
+        display: none;
+    }
+}
+
+.poster-container {
+    width: 100%;
+    min-height: 100%;
+    padding: 20px;
+    background: linear-gradient(180deg, #FF5B30 0%, #FFC87F 26.23%, #FFF1B8 60.1%, #FFF7ED 100%);
+    position: relative;
+
+    .bg-img {
+        width: 100%;
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        z-index: 0;
+        pointer-events: none;
+    }
+
+    .header-bg {
+        width: 203px;
+        height: 203px;
+        margin: 0 auto;
+        position: relative;
+        z-index: 11;
+
+        .header-img {
+            width: 100%;
+            height: auto;
+            display: block;
+        }
+    }
+
+    .poster-content {
+        margin: -20px 0 0;
+        padding: 16px 10px;
+        background: #fff;
+        border-radius: 10px;
+        box-shadow: 0 1px 8px 0 rgba(236, 190, 109, 0.2);
+        position: relative;
+        z-index: 10;
+
+        .poster-title {
+            font-size: 16px;
+            font-weight: bold;
+            color: #F05A31;
+            text-align: center;
+            line-height: 1.5;
+            margin-bottom: 15px;
+        }
+
+        .poster-badge {
+            color: #fff;
+            font-size: 14px;
+            font-weight: 500;
+            padding: 4px 10px;
+            margin-bottom: 10px;
+            border-radius: 15px;
+            display: inline-flex;
+            align-items: center;
+            background: linear-gradient(90deg, #F75D04 0%, #FB8510 100%);
+
+            .dot {
+                font-size: 12px;
+                margin-right: 8px;
+            }
+        }
+
+        .poster-desc {
+            font-size: 12px;
+            color: #F05A31;
+            line-height: 1.6;
+            margin-bottom: 15px;
+            padding: 0 5px;
+            text-align: left;
+            white-space: pre-wrap;
+        }
+    }
+
+    .poster-table-wrapper {
+        border-radius: 2px;
+        overflow: hidden;
+
+        .poster-table {
+            width: 100%;
+            font-size: 12px;
+            color: #333;
+            border: 1px solid #FFE3CC;
+            border-collapse: collapse;
+
+            th,
+            td {
+                border: 1px solid #FFE3CC;
+            }
+
+            th {
+                padding: 8px 4px;
+                text-align: center;
+                font-weight: bold;
+                background: #FFEFE2;
+            }
+
+            td {
+                padding: 8px 4px;
+                text-align: center;
+            }
+        }
+    }
+
+    .poster-footer {
+        margin: -1px 10px 0;
+        padding: 10px;
+        border-radius: 6px;
+        background-size: 100% 100%;
+        background-repeat: no-repeat;
+        background-position: center center;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        position: relative;
+        z-index: 10;
+
+        .qr-box {
+            width: 70px;
+            height: 70px;
+            background: #fff;
+            border-radius: 4px;
+            padding: 2px;
+            flex-shrink: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        .info-right {
+            margin-left: 25px;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+
+            .brand {
+                font-size: 14px;
+                color: #000;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                margin-bottom: 7px;
+
+                .brand-logo {
+                    width: 22px;
+                    height: 22px;
+                    margin-right: 6px;
+                    border-radius: 50%;
+                }
+            }
+
+            .date-tag {
+                color: #fff;
+                font-size: 12px;
+                padding: 2px 9px;
+                border-radius: 2px;
+                align-self: flex-start;
+                background: linear-gradient(88deg, #FF922A 0%, #F76005 100%);
+            }
+        }
+    }
+}
+</style>

+ 122 - 0
src/views/weGroupFission/modules/CompSign.vue

@@ -0,0 +1,122 @@
+<template>
+    <div class="custom-list">
+        <div style="text-align: right; margin-bottom: 10px;">
+            <el-button type="primary" size="mini" plain icon="el-icon-plus" @click="addTier">添加阶梯</el-button>
+        </div>
+
+        <div class="sign-row" v-for="(item, index) in innerValue" :key="index">
+            <div class="label required-mark">第{{ index + 1 }}阶梯:</div>
+
+            <div class="tier-inputs">
+                <span>邀请≥</span>
+                <el-input-number v-model="item.count" :min="1" :max="maxLimit" placeholder="人数"
+                    controls-position="right" size="small" style="width: 110px;"
+                    @change="handleChange"></el-input-number>
+                <span>人,</span>
+
+                <span>奖励</span>
+                <el-input-number v-model="item.reward" :min="0" placeholder="金额" controls-position="right" size="small"
+                    style="width: 110px;" @change="handleChange"></el-input-number>
+                <span>元</span>
+            </div>
+
+            <i class="el-icon-remove-outline delete-icon" @click="removeTier(index)" v-if="innerValue.length > 1" />
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'CompSign',
+    props: {
+        value: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data() {
+        return {
+            maxLimit: 200,
+            innerValue: []
+        };
+    },
+    watch: {
+        value: {
+            handler(val) {
+                // 深拷贝防止直接修改 props
+                this.innerValue = val ? JSON.parse(JSON.stringify(val)) : [];
+            },
+            immediate: true,
+            deep: true
+        }
+    },
+    methods: {
+        addTier() {
+            this.innerValue.push({ count: undefined, reward: undefined });
+            this.handleChange();
+        },
+        removeTier(index) {
+            // 通知父组件哪个索引被删除了
+            this.$emit('removed', index);
+            this.innerValue.splice(index, 1);
+            this.handleChange();
+        },
+        handleChange() {
+            this.$emit('input', this.innerValue); 
+            this.$emit('change', this.innerValue);
+        },
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.required-mark::before {
+    content: '*';
+    color: #f56c6c;
+    margin-right: 4px;
+}
+
+.custom-list {
+    width: 100%;
+    max-width: 800px;
+
+    .sign-row {
+        color: #606266;
+        padding: 10px;
+        margin-bottom: 10px;
+        border-radius: 4px;
+        background: #fafafa;
+        display: flex;
+        align-items: center;
+
+        .label {
+            width: 110px;
+            text-align: right;
+            padding-right: 10px;
+        }
+
+        .tier-inputs {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+
+            span {
+                font-size: 13px;
+                line-height: 1;
+                flex-shrink: 0;
+            }
+        }
+
+        .delete-icon {
+            font-size: 18px;
+            color: #f56c6c;
+            cursor: pointer;
+            margin-left: 15px;
+
+            &:hover {
+                opacity: 0.8;
+            }
+        }
+    }
+}
+</style>

+ 452 - 0
src/views/weGroupFission/modules/modal.vue

@@ -0,0 +1,452 @@
+<template>
+    <el-drawer :title="title" :visible.sync="visible" direction="rtl" size="85%" :wrapperClosable="false"
+        :destroy-on-close="true">
+        <div class="main-layout">
+            <CompPreview :form-data="formModel" />
+
+            <div class="activity-container">
+                <el-steps :active="currentStep" simple>
+                    <el-step title="基础设置" icon="el-icon-edit"></el-step>
+                    <el-step title="派奖方式" icon="el-icon-setting"></el-step>
+                    <el-step title="奖励设置" icon="el-icon-present"></el-step>
+                    <el-step title="高级设置" icon="el-icon-s-operation"></el-step>
+                </el-steps>
+
+                <div class="form-content-area">
+                    <el-form ref="form" :model="formModel" :rules="rules" :disabled="title === '详情'" label-width="120px" label-suffix=":" size="small">
+                        <div v-if="currentStep === 0">
+                            <el-form-item label="活动标题" prop="title">
+                                <el-input v-model="formModel.title" placeholder="请输入活动标题" maxlength="20"
+                                    show-word-limit></el-input>
+                            </el-form-item>
+
+                            <el-form-item label="活动时间" prop="timeRange">
+                                <el-date-picker ref="timeRange"
+                                    type="datetimerange" value-format="yyyy-MM-dd HH:mm:ss"
+                                    range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
+                                    :picker-options="pickerOptions"  v-model="formModel.timeRange" 
+                                ></el-date-picker>
+                            </el-form-item>
+
+                            <el-form-item label="关联商户" prop="merchantId">
+                                <el-select v-model="formModel.merchantId" placeholder="请选择商户" filterable @change="onChange">
+                                    <el-option v-for="item in dicts.merchantId" :key="item.val" :label="item.name" :value="item.val"></el-option>
+                                </el-select>
+                            </el-form-item>
+                            <el-form-item label="活动负责人" prop="leaderUserId">
+                                <el-select v-model="formModel.leaderUserId" placeholder="请选择活动负责人" filterable>
+                                    <el-option label="测试商户A" value="1"></el-option>
+                                    <el-option label="测试商户B" value="2"></el-option>
+                                </el-select>
+                            </el-form-item>
+                            <el-form-item label="关联企微群" prop="wecomGroupId">
+                                <el-select v-model="formModel.wecomGroupId" placeholder="请选择关联企微群" filterable>
+                                    <el-option v-for="item in dicts.wecomGroupId" :key="item.val" :label="item.name" :value="item.val"></el-option>
+                                </el-select>
+                            </el-form-item>
+                            <el-form-item label="关联员工企微" prop="wecomEmployeeId">
+                                <el-select v-model="formModel.wecomEmployeeId" placeholder="请选择关联员工企微" filterable>
+                                    <el-option v-for="item in dicts.wecomEmployeeId" :key="item.val" :label="item.name" :value="item.val"></el-option>
+                                </el-select>
+                            </el-form-item>
+
+                            <el-form-item label="新客户判定" prop="newUserRule">
+                                <el-radio-group v-model="formModel.newUserRule">
+                                    <el-radio v-for="item in dicts.newUserRule" :key="item.dictValue" :label="item.dictValue">{{ item.dictLabel }}</el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+
+                            <el-form-item label="活动说明" prop="activityInstructions">
+                                <el-input type="textarea" v-model="formModel.activityInstructions" rows="4"></el-input>
+                            </el-form-item>
+                        </div>
+
+                        <div v-if="currentStep === 1">
+                            <el-form-item label="触发条件" prop="invitationCount">
+                                <el-input-number v-model="formModel.invitationCount" :min="1" :max="200" placeholder="请填写邀请人数"></el-input-number>
+                                <span style="margin-left: 10px; font-size: 12px; color:#999;">注:≥1,≤200(邀请新客户进群人数)</span>
+                            </el-form-item>
+
+                            <el-form-item label="派奖方式" prop="payoutMethod">
+                                <el-radio-group v-model="formModel.payoutMethod">
+                                    <el-radio v-for="item in dicts.payoutMethod" :key="item.dictValue" :label="item.dictValue">{{ item.dictLabel }}</el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+
+                            <el-form-item label="发放时机" prop="timingDistribution">
+                                <el-radio-group v-model="formModel.timingDistribution">
+                                    <el-radio v-for="item in dicts.timingDistribution" :key="item.dictValue" :label="item.dictValue">{{ item.dictLabel }}</el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+                        </div>
+
+                        <div v-if="currentStep === 2">
+                            <el-divider content-position="left">签到奖励</el-divider>
+                            <el-form-item label-width="0" prop="inviteTiers">
+                                <CompSign v-model="formModel.inviteTiers" @removed="handleTierChange" />
+                            </el-form-item>
+
+                            <el-divider content-position="left" class="divider">额外奖励</el-divider>
+                            <el-form-item label-width="0" prop="inviteExtras">
+                                <CompExtras :options="tierOptions" v-model="formModel.inviteExtras" />
+                            </el-form-item>
+                        </div>
+
+                        <div v-if="currentStep === 3">
+                            <el-form-item label="助力上限" prop="dailyAssistanceLimit">
+                                <el-input-number v-model="formModel.dailyAssistanceLimit" :min="1" :max="200"></el-input-number>
+                                <span> 人 / 单日</span>
+                            </el-form-item>
+
+                            <el-form-item label="最多助力数" prop="customAssistanceLimit">
+                                <el-input-number v-model="formModel.customAssistanceLimit" :min="1" :max="200"></el-input-number>
+                                <span> 人 / 单客户</span>
+                            </el-form-item>
+
+                            <el-form-item label="进群欢迎语" prop="welcomeMessage">
+                                <el-input v-model="formModel.welcomeMessage" type="textarea" autosize placeholder="请输入欢迎语"></el-input>
+                            </el-form-item>
+                        </div>
+                    </el-form>
+                </div>
+            </div>
+        </div>
+
+        <div class="drawer-footer">
+            <el-button v-if="currentStep > 0" @click="handlePrev">上一步</el-button>
+            <el-button v-if="currentStep < 3" type="primary" @click="handleNext">下一步</el-button>
+            <el-button v-if="currentStep === 3" type="primary" 
+                :loading="btnLoading"
+                @click="handleSubmit"
+            >完成并保存</el-button>
+        </div>
+    </el-drawer>
+</template>
+
+<script>
+import { 
+    getDicts 
+} from '@/api/system/menu';
+import { 
+    queryById,
+    saveOrUpdate, 
+    getDictListByType,
+    getWecomGroupList,
+    getWecomEmployeeList, 
+} from '@/api/activity/index';
+import moment from 'moment';
+import CompSign from './CompSign.vue';
+import CompExtras from './CompExtras.vue';
+import CompPreview from './CompPreview.vue';
+
+export default {
+    name: '',
+    components: {
+        CompSign,
+        CompExtras,
+        CompPreview
+    },
+    data() {
+        return {
+            title: '活动方案配置',
+            visible: false,
+            btnLoading: false,
+            currentStep: 0,
+            isUpdate: false,
+            formModel: {
+                id: '',
+                timeRange: [], 
+                newUserRule: '1',
+                payoutMethod: '1',
+                timingDistribution: '1',
+                inviteTiers: [{ count: undefined, reward: undefined }],
+                inviteExtras: [],
+            },
+            rules: {
+                title: [
+                    { required: true, message: '请输入活动标题', trigger: 'blur' }
+                ],
+                timeRange: [
+                    { required: true, message: '请选择活动时间', trigger: 'change' }
+                ],
+                merchantId: [
+                    { required: true, message: '请选择商户', trigger: 'change' }
+                ],
+                wecomGroupId: [
+                    { required: true, message: '请选择关联企微群', trigger: 'change' }
+                ],
+                wecomEmployeeId: [
+                    { required: true, message: '请选择关联员工企微', trigger: 'change' }
+                ],
+                activityInstructions: [
+                    { required: true, message: '请输入活动说明', trigger: 'change' }
+                ],
+                dailyAssistanceLimit: [
+                    { required: true, message: '请输入单日助力上限', trigger: 'change' }
+                ],
+                customAssistanceLimit: [
+                    { required: true, message: '请输入同一客户最多助力数', trigger: 'change' }
+                ],
+                invitationCount: [
+                    { required: true, message: '请输入邀请人数', trigger: 'change' }
+                ],
+                // 签到奖励校验
+                inviteTiers: [{
+                    required: true,
+                    // trigger: ['blur', 'change'],
+                    validator: (rule, value, callback) => {
+                        if (this.currentStep < 2) return callback();
+                        if (!value || value.length === 0) {
+                            return callback(new Error('请至少添加一个奖励阶梯'));
+                        }
+                        for (let i = 0; i < value.length; i++) {
+                            if (!value[i].count || value[i].reward === undefined) {
+                                return callback(new Error(`请完善第${i + 1}阶梯人数和奖励金额`));
+                            }
+                        }
+                        callback();
+                    }
+                }]
+            },
+            dicts: {
+                merchantId: [],
+                wecomGroupId: [],
+                wecomEmployeeId: [],
+                newUserRule: [],
+                payoutMethod: [],
+                timingDistribution: [],
+            }
+        };
+    },
+    computed: {
+        tierOptions() {
+            let data = (this.formModel.inviteTiers || []).filter(item => item.count > 0).map((item, index) => ({
+                label: `第${index + 1}阶梯 (邀请≥${item.count || 0}人)`,
+                value: index
+            }));
+            return data;
+        },
+        pickerOptions() {
+            return {
+                onPick: ({ minDate }) => {
+                    const isCurrentDay = moment(minDate).isSame(moment(), 'day');
+                    if (isCurrentDay) {
+                        const picker = this.$refs.timeRange.picker;
+                        const { minTimePicker } = picker.$refs;
+                        let s = moment(`${moment().add(2, 'minutes').format('YYYY-MM-DD HH:mm:ss')}`).toDate();
+                        let e = moment(`${moment(minDate).format('YYYY-MM-DD')} 23:59:59`).toDate();
+                        minTimePicker.selectableRange = [[s, e]];
+                    }
+                },
+                disabledDate(time) {
+                    return time.getTime() < Date.now() - 8.64e7
+                }
+            }
+        }
+    },
+    created() {
+        this.initData();
+    },
+    methods: {
+        initData() {
+            // 新客户判定规则
+            getDicts('new_customer_rule_type').then((res) => {
+                this.dicts.newUserRule = res.data;
+            });
+            // 派奖方式
+            getDicts('prize_distribution_method').then((res) => {
+                this.dicts.payoutMethod = res.data;
+            });
+            // 发放时机
+            getDicts('timing_distribution_groupFission_type').then((res) => {
+                this.dicts.timingDistribution = res.data;
+            });
+            // 商户
+            getDictListByType({ type: 1 }).then((res) => {
+                this.dicts.merchantId = res.data;
+            });
+            // 企业微信群
+            getWecomGroupList().then((res) => {
+                this.dicts.wecomGroupId = res.data;
+            });
+            // 企业微信员工
+            getWecomEmployeeList().then((res) => {
+                this.dicts.wecomEmployeeId = res.data;
+            });
+        },
+        add() {
+            this.isUpdate = false;
+            this.resetForm();
+            this.visible = true;
+        },
+        edit(record) {
+            this.isUpdate = true;
+            this.resetForm();
+            this.visible = true;
+            // 加载详情
+            queryById({ id: record.id }).then(res => {
+                if (res.code === 200) {
+                    this.formModel = res.data;
+                    // 时间
+                    if (res.data.beginDate && res.data.endDate) {
+                        this.formModel.timeRange = [res.data.beginDate, res.data.endDate];
+                    }
+                    // 阶梯数组
+                    if (res.data.weGroupFissionStepList) {
+                        this.formModel.inviteTiers = res.data.weGroupFissionStepList.map((item) => ({
+                            reward: item.rewardAmount,
+                            count: item.invitationCount,
+                        }));
+                    }
+                    // 额外奖励数组
+                    if (res.data.weGroupFissionExtraList) {
+                        this.formModel.inviteExtras = res.data.weGroupFissionExtraList.map((item) => {
+                            let nd = {
+                                tierIndex: item.sortNum - 1,
+                                rewardType: item.rewardType,
+                                rewardValue: undefined
+                            };
+                            if (item.rewardType == 1) {
+                                nd.rewardValue = item.rewardAmount;
+                            } else if (item.rewardType == 2) {
+                                nd.rewardName = item.rightInfoName;
+                                nd.rewardValue = item.rightInfoId;
+                            } else if (item.rewardType == 3) {
+                                nd.rewardName = item.lotteryActivityName;
+                                nd.rewardValue = item.lotteryActivityId;
+                            }
+                            return nd;
+                        });
+                    }
+                }
+            });
+        },
+        resetForm() {
+            this.currentStep = 0;
+            // 初始数据
+            const initialData = this.$options.data.call(this);
+            this.formModel = initialData.formModel;
+            this.$nextTick(() => {
+                if (this.$refs.form) this.$refs.form.clearValidate();
+            });
+        },
+        // 商户选择事件
+        onChange(value) {
+            const selectedOption = this.dicts.merchantId.find(item => item.val === value);
+            if (selectedOption) {
+                this.formModel.merchantName = selectedOption.name;
+            }
+        },
+        handleTierChange(deletedIndex) {
+            this.$nextTick(() => {
+                this.$refs.form.validateField('inviteTiers');
+            })
+            let newExtras = this.formModel.inviteExtras.filter(item => item.tierIndex !== deletedIndex);
+            newExtras = newExtras.map(item => {
+                if (item.tierIndex > deletedIndex) {
+                    return { ...item, tierIndex: item.tierIndex - 1 };
+                }
+                return item;
+            });
+            this.formModel.inviteExtras = newExtras;
+        },
+        handleNext() {
+            // 校验
+            this.$refs.form.validate((valid, invalidFields) => {
+                if (valid) {
+                    this.currentStep++;
+                }
+            });
+        },
+        handlePrev() {
+            this.currentStep--;
+        },
+        handleSubmit() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.btnLoading = true;
+                    let { timeRange, inviteTiers, inviteExtras, ...others } = this.formModel;
+                    let params = { ...others };
+                    if (timeRange && timeRange.length === 2) {
+                        params.beginDate = timeRange[0];
+                        params.endDate = timeRange[1];
+                    }
+                    // 阶梯数组
+                    if (inviteTiers) {
+                        params.weGroupFissionStepList = inviteTiers.map((item, index) => ({
+                            sortNum: index + 1,
+                            rewardAmount: item.reward,
+                            invitationCount: item.count,
+                        }));
+                    }
+                    // 额外奖励数组
+                    if (inviteExtras) {
+                        params.weGroupFissionExtraList = inviteExtras.map((item) => {
+                            let nd = {
+                                sortNum: item.tierIndex + 1,
+                                rewardType: item.rewardType,
+                            };
+                            if (item.rewardType == 1) {
+                                nd.rewardAmount = item.rewardValue;
+                            } else if (item.rewardType == 2) {
+                                nd.rightInfoId = item.rewardValue;
+                            } else if (item.rewardType == 3) {
+                                nd.lotteryActivityId = item.rewardValue;
+                            }
+                            return nd;
+                        });
+                    }
+                    // console.log('form', params);
+                    saveOrUpdate(params).then(res => {
+                        if (res.code === 200) {
+                            this.$message.success('保存成功');
+                            this.visible = false;
+                            this.$emit('success');
+                        } else {
+                            this.$message.error(res.msg);
+                        }
+                    }).finally(() => {
+                        this.btnLoading = false;
+                    });
+                }
+            });
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.main-layout {
+    padding: 0 20px;
+    display: flex;
+	gap: 20px;
+}
+
+.activity-container {
+    flex: 1;
+
+    .form-content-area {
+        padding: 24px 24px 80px;
+
+        ::v-deep input[aria-hidden="true"] {
+            display: none !important;
+        }
+
+        .divider {
+            display: inline-block;
+        }
+    }
+}
+
+.drawer-footer {
+    width: calc(100% - 415px);
+    padding: 10px 16px;
+    text-align: right;
+    border-top: 1px solid #e8e8e8;
+    background: #fff;
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    z-index: 99;
+}
+</style>

+ 308 - 0
src/views/weGroupFission/modules/share.vue

@@ -0,0 +1,308 @@
+<template>
+    <el-dialog title="活动分享" :visible.sync="visible" destroy-on-close>
+        <template #title>
+            <div class="modal-title">
+                <span role="img" aria-label="share">✨</span>
+                <span>活动分享</span>
+            </div>
+        </template>
+
+        <div class="share-content">
+            <div class="share-card">
+                <div class="card-header">
+                    <div class="header-icon wechat-icon">
+                        <i class="el-icon-link"></i>
+                    </div>
+                    <span class="header-title">小程序链接分享</span>
+                </div>
+
+                <div class="card-body">
+                    <div class="link-input-wrapper">
+                        <el-input v-model="wechatPath" readonly>
+                            <i slot="suffix" class="el-icon-link action-icon copy-icon" :data-clipboard-text="wechatPath"></i>
+                        </el-input>
+                        <div id="share-qrcode" v-show="false"></div>
+                    </div>
+
+                    <div class="download-row">
+                        <el-link icon="el-icon-download" type="primary" @click="onDownload('miniCode')">下载小程序码</el-link>
+                        <el-link icon="el-icon-picture-outline" type="primary" @click="onDownload('poster')">下载活动海报</el-link>
+                    </div>
+                </div>
+            </div>
+
+            <CompPreview ref="targetDom" :form-data="formData" />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import QRCode from 'qrcodejs2';
+import html2canvas from 'html2canvas';
+import CompPreview from './CompPreview.vue';
+import { 
+    queryById,
+    getWechatGroupFissionUrl
+} from '@/api/activity/index';
+export default {
+    name: '',
+    components: {
+        CompPreview
+    },
+    data() {
+        return {
+            qrcode: null,
+            clipboard: null,
+            visible: false,
+            wechatPath: '获取中...',
+            formData: {
+                timeRange: []
+            }
+        };
+    },
+    mounted() {
+        // 复制功能
+        this.clipboard = new this.ClipboardJS('.copy-icon');
+    },
+    destroyed() {
+        this.clipboard && this.clipboard.destroy();
+    },
+    methods: {
+        async show(record) {
+            this.visible = true;
+            if (record && record.id) {
+                await this.getQRCode(record);
+                this.getData(record);
+            }
+        },
+        // 初始数据
+        getData(record) {
+            // 加载详情
+            queryById({ id: record.id }).then(res => {
+                if (res.code === 200) {
+                    this.formData = res.data;
+                    // 时间
+                    if (res.data.beginDate && res.data.endDate) {
+                        this.formData.timeRange = [res.data.beginDate, res.data.endDate];
+                    }
+                    // 阶梯数组
+                    if (res.data.weGroupFissionStepList) {
+                        this.formData.inviteTiers = res.data.weGroupFissionStepList.map((item) => ({
+                            reward: item.rewardAmount,
+                            count: item.invitationCount,
+                        }));
+                    }
+                    // 额外奖励数组
+                    if (res.data.weGroupFissionExtraList) {
+                        this.formData.inviteExtras = res.data.weGroupFissionExtraList.map((item) => {
+                            let nd = {
+                                tierIndex: item.sortNum - 1,
+                                rewardType: item.rewardType,
+                                rewardValue: undefined
+                            };
+                            if (item.rewardType == 1) {
+                                nd.rewardValue = item.rewardAmount;
+                            } else if (item.rewardType == 2) {
+                                nd.rewardName = item.rightInfoName;
+                                nd.rewardValue = item.rightInfoId;
+                            } else if (item.rewardType == 3) {
+                                nd.rewardName = item.lotteryActivityName;
+                                nd.rewardValue = item.lotteryActivityId;
+                            }
+                            return nd;
+                        });
+                    }
+                    
+                    this.$refs.targetDom.getQRCode(this.wechatPath);
+                }
+            });
+        },
+        // 初始化二维码
+        async getQRCode(record) {
+            await getWechatGroupFissionUrl({
+                // url: 'packageF/pages/activity/weGroupFission/poster',
+                url: 'pages/home/home',
+                version: 'release',
+                groupFissionId: record.id,
+            }).then(res => {
+                if (res.code === 200) {
+                    this.wechatPath = res.data;
+                }
+            });
+            this.qrcode = new QRCode('share-qrcode', {
+                text: this.wechatPath, 
+                width: 200, 
+                height: 200, 
+                colorDark: '#000000', // 二维码前景色
+                colorLight: '#ffffff', // 二维码背景色
+                correctLevel: QRCode.CorrectLevel.H // 容错级别:L/M/Q/H(H最高)
+            });
+        },
+        // 下载功能
+        async onDownload(type) {
+            if (type === 'miniCode') {
+                const qrImg = document.querySelector('#share-qrcode img') || document.querySelector('#share-qrcode canvas');
+                if (!qrImg) {
+                    this.$message.error('二维码生成失败!');
+                    return;
+                }
+
+                // 创建下载链接
+                const a = document.createElement('a');
+                a.download = '小程序码-' + new Date().getTime() + '.png'; // 下载文件名
+
+                // 处理 Canvas 或 Img 转 DataURL
+                if (qrImg.tagName === 'CANVAS') {
+                    a.href = qrImg.toDataURL('image/png');
+                } else {
+                    a.href = qrImg.src;
+                }
+
+                // 触发下载
+                document.body.appendChild(a);
+                a.click();
+                document.body.removeChild(a);
+            } else {
+                try {
+                    // 1. 获取目标 DOM 元素
+                    const targetElement = this.$refs.targetDom.$el;
+                    if (!targetElement) {
+                        this.$message.error('未找到要截图的DOM元素!');
+                        return;
+                    }
+
+                    // 2. 配置 html2canvas 参数(关键:解决样式、跨域等问题)
+                    const canvas = await html2canvas(targetElement, {
+                        useCORS: true, // 允许跨域图片(必开,否则跨域图片会空白)
+                        allowTaint: true, // 允许污染画布
+                        scale: 2, // 缩放比例(2倍生成高清图,避免模糊)
+                        logging: false, // 关闭控制台日志
+                        backgroundColor: '#ffffff', // 背景色(默认透明,建议设置)
+                        // 可选:指定截图区域(x/y 偏移,width/height 尺寸)
+                        // x: 0,
+                        // y: 0,
+                        // width: targetElement.offsetWidth,
+                        // height: targetElement.offsetHeight
+                    });
+
+                    // 3. 转换 Canvas 为 Blob(比 Base64 更适合大文件)
+                    canvas.toBlob((blob) => {
+                        // 4. 创建下载链接
+                        const url = URL.createObjectURL(blob);
+                        const a = document.createElement('a');
+                        a.href = url;
+                        // 自定义下载文件名(时间戳避免重复)
+                        a.download = `活动海报-${new Date().getTime()}.png`;
+                        // 触发下载
+                        document.body.appendChild(a);
+                        a.click();
+                        // 5. 清理资源
+                        document.body.removeChild(a);
+                        URL.revokeObjectURL(url);
+
+                        this.$message.success('图片生成并下载成功!');
+                    }, 'image/png'); // 指定图片格式为 PNG
+
+                } catch (error) {
+                    console.error('生成图片失败:', error);
+                    this.$message.error('图片生成失败,请重试!');
+                }
+            }
+        },
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.modal-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #1f1f1f;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.share-content {
+    .share-card {
+        padding: 20px;
+        background: #f8faff; 
+        border-radius: 12px;
+        border: 1px solid #eff4f9; 
+        transition: all 0.3s;
+
+        &:hover {
+            border-color: #d9e4ff;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 
+        }
+
+        .card-header {
+            display: flex;
+            align-items: center;
+            margin-bottom: 16px;
+
+            .header-icon {
+                width: 28px;
+                height: 28px;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                border-radius: 6px;
+                margin-right: 10px;
+                font-size: 16px;
+
+                &.wechat-icon {
+                    background: #e7f8eb; 
+                    color: #07c160;
+                }
+
+                &.h5-icon {
+                    background: #e6f7ff; 
+                    color: #1890ff;
+                }
+            }
+
+            .header-title {
+                font-size: 15px;
+                font-weight: 600;
+                color: #333;
+            }
+        }
+
+        .card-body {
+            .link-input-wrapper {
+                margin-bottom: 12px;
+
+                .action-icon {
+                    font-size: 22px;
+                    cursor: pointer;
+                    transition: all 0.2s;
+                    color: #1890ff; 
+                    padding: 4px;
+                    border-radius: 4px;
+
+                    &:hover {
+                        color: #40a9ff;
+                    }
+
+                    &.copy-icon:active {
+                        color: #096dd9;
+                    }
+                }
+            }
+
+            .download-row {
+                display: flex;
+                gap: 20px;
+            }
+        }
+    }
+}
+
+.poster-preview {
+    border: 0;
+    border-radius: 0;
+    position: fixed;
+    top: -2000px;
+}
+</style>