Fan 4 дней назад
Родитель
Сommit
57a5b85417

+ 2 - 1
ui/smarttrade-platform/src/api/layout/index.js

@@ -10,4 +10,5 @@ export const getUserInfo = () => post('/router/rest/sys/auth/getCurrentUserInfo'
 export const updateUserPwd = (data) => post('/router/rest/sys/auth/modifyPass', data)
 export const updateRemark = (data) => post('/router/rest/sys/auth/updateRemark', data)
 export const getLoginData = (data) => post('/router/rest/sys/login/log/pageList', data)
-export const getListTree = (data) => post('/router/rest/sys/user/pagelist', data)
+export const getDeptTree = (data) => post('/router/rest/sys/dept/listTree', data)
+export const queryAllUser = (data) => post('/router/rest/sys/user/pagelist', data)

+ 2 - 1
ui/smarttrade-platform/src/api/marketing/index.js

@@ -10,4 +10,5 @@ export const addMarketing = (data = {}) => post('/router/rest/marketing/add', da
 export const editMarketing = (data = {}) => post('/router/rest/marketing/edit', data)
 export const marketingAnalysis = () => get('/router/rest/marketing/dashboard/stats')
 export const getMarketingCustomer = (data = {}) => post('/router/rest/marketing/campaign/sop-detail/customers', data)
-export const getTradeCustomer = (data = {}) => post('/router/rest/customerTrade/list', data)
+export const getTradeCustomer = (data = {}) => post('/router/rest/customerTrade/list', data)
+export const saveGenerateEmail = (data = {}) => post('/router/rest/sopDetail/edit', data)

+ 5 - 11
ui/smarttrade-platform/src/components/addMarketing.vue

@@ -101,11 +101,12 @@
                     <div class="form-row">
                         <div class="form-group">
                             <label class="form-label">负责人</label>
-                            <el-select v-model="newCampaign.owner" multiple collapse-tags placeholder="选择负责人">
+                            <!-- <el-select v-model="newCampaign.owner" multiple collapse-tags placeholder="选择负责人">
                                 <el-option label="张经理" value="sales1" />
                                 <el-option label="李主管" value="sales2" />
                                 <el-option label="王小明" value="sales3" />
-                            </el-select>
+                            </el-select> -->
+                            <SelectUser v-model="newCampaign.owner" multiple />
                         </div>
                     </div>
                     <div class="form-group">
@@ -733,10 +734,10 @@
 <script setup>
     import { ref, computed } from 'vue'
     import { getSopList, getMarketingById, addMarketing, editMarketing, addSop, editSop, getTradeCustomer } from '@/api/marketing'
-    import { getListTree } from '@/api/layout'
     import { getUserEmails } from '@/api/emails'
     import { region } from '@/utils/region';
     import CustomerDetail from '@/components/marketing/customerDetail.vue'
+    import SelectUser from '@/components/selectUser.vue'
 
     const emits = defineEmits(['refresh'])
 
@@ -755,7 +756,7 @@
         },
         goals: { sent: 500, openRate: 40, replyRate: 10, opportunities: 15 },
         channels: ['email'],
-        emailSender: '', emailSendTime: 'optimal',
+        emailSender: 'fan@storlead.com', emailSendTime: 'optimal',
         trackOpen: true, trackClick: true, trackReply: true,
         whatsappAccount: '业务账号1', whatsappInterval: '60', whatsappTemplate: '',
         linkedinAction: 'message', linkedinLimit: '30',
@@ -1107,13 +1108,6 @@
         }
         getSop(data?.sopId || '')
         getEmails()
-        getTree()
-    }
-
-    const getTree = () => {
-        getListTree({isIncludeResign: false, reviewTag: 1, pageIndex: 1, pageSize: 10000 }).then(res => {
-            console.log('res :>> ', res);
-        })
     }
 
     const regionValue = ref([])

+ 29 - 5
ui/smarttrade-platform/src/components/ckeditor/index.vue

@@ -31,18 +31,42 @@ export default {
 				editorplaceholder: self.placeholder || '',
 				on: {
 					instanceReady: function() {
-						// 获取编辑区域 iframe 的 document
-						var doc = this.document;
+						var editor = this;
+
+						// 定义注入样式的方法
+						var injectStyle = function() {
+						// 1. 获取编辑区域 iframe 的 document
+						var doc = editor.document;
+						if (!doc) return; // 安全判断
+
+						// 2. 检查是否已经注入过(防止重复添加)
+						var styleId = 'custom-theme-style';
+						var existingStyle = doc.getById(styleId);
 						
-						// 创建 style 标签
+						if (existingStyle) {
+							// 如果已经存在,直接更新内容即可(支持 isDark 动态切换)
+							var newCss = `body { background-color: ${self.isDark ? '#0F172B' : '#FFF'}; color: ${self.isDark ? '#FFF' : '#333'}; }`;
+							existingStyle.$.textContent = newCss;
+							return;
+						}
+
+						// 3. 如果不存在,创建 style 标签
 						var style = doc.createElement('style');
 						style.setAttribute('type', 'text/css');
+						style.setAttribute('id', styleId); // 给个 ID 方便后续查找更新
 						
-						// 写入 CSS 规则
-						var css = `body { background-color: ${ self.isDark ? '#0F172B' : '#FFF'}; color: ${ self.isDark ? '#FFF' : '#333'}`;
+						// 4. 写入 CSS 规则(注意:我帮你补齐了最后缺失的闭合大括号 })
+						var css = `body { background-color: ${self.isDark ? '#0F172B' : '#FFF'}; color: ${self.isDark ? '#FFF' : '#333'}; }`;
 						
 						style.$.appendChild(doc.$.createTextNode(css));
 						doc.getHead().append(style);
+						};
+
+						// 初次加载时执行一次
+						injectStyle();
+
+						// 监听 contentDom 事件:每次执行 setData() 或者模式切换导致 iframe 重建时,自动重新注入
+						editor.on('contentDom', injectStyle);
 					}
    				},
 				toolbar: self.type == 'mini' ? 'Mini' : null

+ 1 - 0
ui/smarttrade-platform/src/components/layout/Sidebar.vue

@@ -468,6 +468,7 @@ onMounted(() => {
   getUserInfo().then((res) => {
     if(res.code == 200) {
       userData.value = res.result
+      localStorage.setItem('userInfo', JSON.stringify(res.result))
     }
   })
 })

+ 126 - 42
ui/smarttrade-platform/src/components/marketing/aiGenerator.vue

@@ -11,27 +11,24 @@
               <h4>输入要求</h4>
               <div class="form-group">
                 <label class="form-label">邮件类型</label>
-                <el-select v-model="aiGenerator.type">
-                  <el-option value="introduction" label="首次介绍信"></el-option>
-                  <el-option value="followup" label="跟进邮件"></el-option>
-                  <el-option value="product" label="产品推荐"></el-option>
-                  <el-option value="offer" label="报价邮件"></el-option>
+                <el-select v-model="aiGenerator.inputs.scene">
+                  <el-option v-for="item in typeData" :value="item.name" :label="item.name"></el-option>
                 </el-select>
               </div>
               <div class="form-group">
                 <label class="form-label">产品/服务</label>
-                <el-select v-model="aiGenerator.product">
-                  <el-option value="ssd" label="固态硬盘 SSD"></el-option>
-                  <el-option value="hdd" label="机械硬盘 HDD"></el-option>
-                  <el-option value="memory" label="内存条"></el-option>
-                  <el-option value="storage" label="存储方案"></el-option>
+                <el-select v-model="aiGenerator.inputs.products">
+                  <el-option value="固态硬盘 SSD" label="固态硬盘 SSD"></el-option>
+                  <el-option value="机械硬盘 HDD" label="机械硬盘 HDD"></el-option>
+                  <el-option value="内存条" label="内存条"></el-option>
+                  <el-option value="存储方案" label="存储方案"></el-option>
                 </el-select>
               </div>
               <div class="form-group">
                 <label class="form-label">核心卖点</label>
                 <div>
-                  <el-checkbox-group v-model="aiGenerator.selectedPoints">
-                    <el-checkbox :label="point.label" :value="point.id" v-for="point in sellingPoints" />
+                  <el-checkbox-group v-model="aiGenerator.inputs.core_selling_points">
+                    <el-checkbox :label="point.label" :value="point.label" v-for="point in sellingPoints" />
                   </el-checkbox-group>
                 </div>
               </div>
@@ -39,25 +36,23 @@
                 <label class="form-label">语气风格</label>
                 <div class="tone-options">
                   <span v-for="tone in toneOptions" :key="tone.value"
-                        class="tone-option" :class="{ active: aiGenerator.tone === tone.value }"
-                        @click="aiGenerator.tone = tone.value">{{ tone.label }}</span>
+                        class="tone-option" :class="{ active: aiGenerator.inputs.style === tone.label }"
+                        @click="aiGenerator.inputs.style = tone.label">{{ tone.label }}</span>
                 </div>
               </div>
               <div class="form-group">
                 <label class="form-label">语言</label>
                 <div class="language-options">
                   <span v-for="lang in languageOptions" :key="lang.value"
-                        class="language-option" :class="{ active: aiGenerator.language === lang.value }"
-                        @click="aiGenerator.language = lang.value">{{ lang.label }}</span>
+                        class="language-option" :class="{ active: aiGenerator.inputs.language === lang.label }"
+                        @click="aiGenerator.inputs.language = lang.label">{{ lang.label }}</span>
                 </div>
               </div>
               <div class="form-group">
                 <label class="form-label">额外要求</label>
-                <el-input type="textarea" v-model="aiGenerator.additionalRequirements" :rows="3" resize="none" placeholder="例如:强调价格优势、提及促销信息"></el-input>
+                <el-input type="textarea" v-model="aiGenerator.inputs.extra" :rows="3" resize="none" placeholder="例如:强调价格优势、提及促销信息"></el-input>
               </div>
-              <button class="btn btn-primary btn-block" @click="generateEmail" :disabled="isGenerating">
-                {{ isGenerating ? '生成中...' : '生成邮件' }}
-              </button>
+              <el-button style="height: 38px;" class="btn btn-primary btn-block" @click="generateEmail" :loading="isGenerating">生成邮件</el-button>
             </div>
             <div class="generator-output">
               <h4>可用参数</h4>
@@ -72,19 +67,19 @@
                 </div>
                 <div>
                     <label>企业名称:</label>
-                    <span>${customerName}$</span>
+                    <span>${companyName}$</span>
                 </div>
                 <div>
                     <label>企业网址:</label>
-                    <span>${customerName}$</span>
+                    <span>${companyUrl}$</span>
                 </div>
                 <div>
                     <label>联系人名称:</label>
-                    <span>${customerName}$</span>
+                    <span>${liaisonName}$</span>
                 </div>
                 <div>
                     <label>联系人邮箱:</label>
-                    <span>${customerName}$</span>
+                    <span>${email}$</span>
                 </div>
               </div>
               <div class="msg-item">
@@ -92,15 +87,14 @@
                         <ckeditor
                             type="mini"
                             class="send-ckeditor"
-                            placeholder="智能写信新体验,试试AI润色让文字焕发活力"
+                            placeholder=""
                             @sendContnet="getContent"
                             :content="emailContent"
                             ref="ckeditorRef"
                             isDark="true"
                         ></ckeditor>
-                        <div class="ai-button" v-if="aiShow">
+                        <!-- <div class="ai-button" v-if="aiShow">
                             <el-button v-for="item in aiButton" @click="aiButtonClick(item)">
-                                <!-- <SvgIcon :iconClass="`ai-${ item.value }`" /> -->
                                  <img :src="getImageUrl(item.value)" />
                                 {{ item.label }}
                             </el-button>
@@ -108,7 +102,7 @@
                         <div class="ai-div">
                             <p>智能写信新体验,试试AI润色让文字焕发活力</p>
                             <el-button type="primary" @click="aiShow = !aiShow">AI润色</el-button>
-                        </div>
+                        </div> -->
                     </div>
                     <div class="file-div" v-loading="fileLoading">
                         <div class="file-header">
@@ -146,11 +140,23 @@
 <script setup>
     import { ref } from 'vue'
     import ckeditor from '@/components/ckeditor/index.vue'
+    import { getSopById, saveGenerateEmail } from '@/api/marketing'
+    import { getToken } from '@/utils/auth'
 
     const showAIModal = ref(false)
+    const userInfo = JSON.parse(localStorage.getItem('userInfo'))
     const aiGenerator = ref({
-        type: 'introduction', product: 'ssd', selectedPoints: ['quality', 'price'],
-        tone: 'professional', language: 'en', additionalRequirements: ''
+        inputs: {
+            scene: '',
+            products: '',
+            core_selling_points: [],
+            style: '',
+            language: '',
+            extra: ''
+        },
+        responseMode: 'streaming',
+        user: userInfo.id,
+        appId: 'app-55665dC9YBxTbuyEVxCs8iVw'
     })
     const sellingPoints = [
         { id: 'quality', label: '高品质' }, { id: 'price', label: '价格优势' },
@@ -169,8 +175,21 @@
     ]
 
     const marketingData = ref()
+    const typeData = ref([])
     const open = (data) => {
         marketingData.value = data
+        getSopById(data.sopId).then(res => {
+            typeData.value = res.result.details || []
+        })
+        aiGenerator.value.inputs = {
+            scene: '',
+            products: '',
+            core_selling_points: [],
+            style: '',
+            language: '',
+            extra: ''
+        }
+        emailContent.value = ''
         showAIModal.value = true
     }
 
@@ -207,6 +226,7 @@
     })
     const fileList = ref([])
     const fileSize = ref(0)
+    const fileLoading = ref(false)
     const changeFiles = (file) => {
         // if(file.raw) {
         //     files.value.push(file.raw)
@@ -254,21 +274,86 @@
     }
 
     const isGenerating = ref(false)
-    const generateEmail = () => {
+    const generateEmail = async() => {
         isGenerating.value = true
-        setTimeout(() => {
-            generatedEmail.value = {
-                subject: 'Reliable SSD Solutions for Your Growing Business',
-                body: `Dear [Contact Name],<br><br>I hope this email finds you well. I noticed that <strong>[Company Name]</strong> specializes in SSD distribution, and I wanted to explore potential collaboration opportunities.<br><br>Our company has been a leading SSD manufacturer for over 15 years:<br><br>• <strong>Competitive Pricing:</strong> Factory-direct pricing<br>• <strong>Quality Assurance:</strong> ISO 9001 certified, 3-year warranty<br>• <strong>Fast Delivery:</strong> 15-20 days to major ports<br><br>I'd love to schedule a quick call. Would next Tuesday work?`,
-                wordCount: 95,
-                readTime: 30,
-                readability: 8.5
+        let params = JSON.parse(JSON.stringify(aiGenerator.value))
+        params.inputs.core_selling_points = params.inputs.core_selling_points.join(',')
+        const url = import.meta.env.VITE_API_BASE_URL + 'router/rest/workflows/streaming';
+        try {
+            const response = await fetch(url, {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json', 'token': getToken() },
+                body: JSON.stringify(params)
+            });
+
+            if (!response.ok) {
+                throw new Error(`HTTP error! status: ${response.status}`);
+            }
+
+            // 1. 获取响应体的 Reader
+            const reader = response.body.getReader();
+            // 2. 创建文本解码器(将 Uint8Array 字节流解码为字符串)
+            const decoder = new TextDecoder('utf-8');
+            
+            // 3. 循环读取流式数据
+            let buffer = ''; // 缓冲区
+            let doneData = '<p>'
+            while (true) {
+                const { done, value } = await reader.read();
+                if (done) break;
+
+                buffer += decoder.decode(value, { stream: true });
+                
+                // 按换行符分割
+                const lines = buffer.split('\n');
+                // 保留最后一个可能不完整的行
+                buffer = lines.pop();
+
+                for (const line of lines) {
+                    // 去除空格,忽略空行或注释行
+                    const trimmedLine = line.trim();
+                    if (!trimmedLine || trimmedLine.startsWith(':')) continue;
+
+                    if (trimmedLine === 'data: [DONE]') {
+                    console.log('流传输结束');
+                    doneData += '</p>'
+                    return;
+                    }
+
+                    if (trimmedLine.startsWith('data:')) {
+                        const jsonStr = trimmedLine.replace('data:', '');
+                        try {
+                            const data = JSON.parse(jsonStr);
+                            if(data.trim()){
+                                doneData += data
+                            }else{
+                                doneData += '</p><p>'
+                            }
+                        } catch (e) {
+                            console.error('JSON解析失败:', jsonStr);
+                        }
+                    }
+                }
             }
+            emailContent.value = doneData
             isGenerating.value = false
-            toast('邮件已生成', 'success')
-        }, 2000)
+            ElMessage.success('生成成功')
+        } catch (error) {
+            console.error('Fetch error:', error);
+        }
+    }
+    const useEmail = () => {
+        let params = {
+            id: typeData.value.filter(item => item.name == aiGenerator.value.inputs.scene)[0].id,
+            emailContent: emailContent.value
+        }
+        saveGenerateEmail(params).then(res => {
+            ElMessage.success('保存成功')
+            showAIModal.value = false
+        }).catch(err => {
+            ElMessage.error('保存失败')
+        })
     }
-    const useEmail = () => { showAIModal.value = false; toast('邮件已应用到活动', 'success') }
 
     defineExpose({ open })
 </script>
@@ -342,7 +427,6 @@
 }
 .msg-item .cke_div :deep(.cke_chrome){
     border: 1px solid #344156;
-    border-bottom: none;
 }
 .msg-item .cke_div :deep(.cke_button_off):hover, .msg-item .cke_div :deep(.cke_combo_button):hover{
     background: transparent;

+ 260 - 0
ui/smarttrade-platform/src/components/marketing/marketingDetail.vue

@@ -0,0 +1,260 @@
+<template>
+    <div v-if="showCampaignDetailModal" class="modal-overlay" @click.self="showCampaignDetailModal = false">
+      <div class="modal campaign-detail-modal">
+        <div class="modal-header">
+          <div class="modal-title">
+            <i class="fas fa-chart-bar"></i> 活动详情
+            <span class="detail-status" :class="selectedCampaign?.status">{{ getStatusLabel(selectedCampaign?.status) }}</span>
+          </div>
+          <button class="modal-close" @click="showCampaignDetailModal = false"><i class="fas fa-times"></i></button>
+        </div>
+        <div class="modal-body" v-if="selectedCampaign">
+          <!-- 基本信息 -->
+          <div class="detail-section">
+            <div class="section-header">
+              <h4><i class="fas fa-info-circle"></i> 基本信息</h4>
+            </div>
+            <div class="detail-grid">
+              <div class="detail-item">
+                <label>活动名称</label>
+                <span>{{ selectedCampaign.name }}</span>
+              </div>
+              <div class="detail-item">
+                <label>活动类型</label>
+                <span>{{ getTypeLabel(selectedCampaign.type) }}</span>
+              </div>
+              <div class="detail-item">
+                <label>优先级</label>
+                <span>{{ getPriorityLabel(selectedCampaign.priority) }}</span>
+              </div>
+              <div class="detail-item">
+                <label>负责人</label>
+                <span>{{ selectedCampaign.owner }}</span>
+              </div>
+              <div class="detail-item">
+                <label>创建时间</label>
+                <span>{{ selectedCampaign.createTime }}</span>
+              </div>
+              <div class="detail-item">
+                <label>活动周期</label>
+                <span>{{ selectedCampaign.beginData }} 至 {{ selectedCampaign.endDate }}</span>
+              </div>
+              <div class="detail-item full-width">
+                <label>活动描述</label>
+                <span>{{ selectedCampaign.description }}</span>
+              </div>
+            </div>
+          </div>
+
+          <!-- 核心数据 -->
+          <div class="detail-section">
+            <div class="section-header">
+              <h4><i class="fas fa-chart-line"></i> 核心数据</h4>
+              <div class="progress-info">达成 {{ selectedCampaign.progressPercent || 0 }}%</div>
+            </div>
+            <div class="detail-progress-bar">
+              <div class="progress-fill" :style="{ width: (selectedCampaign.progressPercent || 0) + '%' }"></div>
+            </div>
+            <div class="metrics-grid">
+              <div class="metric-card">
+                <div class="metric-icon blue"><i class="fas fa-paper-plane"></i></div>
+                <div class="metric-content">
+                  <div class="metric-value">{{ formatNumber(selectedCampaign.sendVolume) }}</div>
+                  <div class="metric-label">已发送</div>
+                </div>
+              </div>
+              <div class="metric-card">
+                <div class="metric-icon green"><i class="fas fa-envelope-open"></i></div>
+                <div class="metric-content">
+                  <div class="metric-value">{{ Math.round(selectedCampaign.sendVolume * selectedCampaign.openRate / 100) }}</div>
+                  <div class="metric-label">打开数 ({{ selectedCampaign.openRate }}%)</div>
+                </div>
+              </div>
+              <div class="metric-card">
+                <div class="metric-icon purple"><i class="fas fa-reply"></i></div>
+                <div class="metric-content">
+                  <div class="metric-value">{{ Math.round(selectedCampaign.sendVolume * selectedCampaign.replyRate / 100) }}</div>
+                  <div class="metric-label">回复数 ({{ selectedCampaign.replyRate }}%)</div>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 触达渠道 -->
+          <div class="detail-section">
+            <div class="section-header">
+              <h4><i class="fas fa-bullhorn"></i> 触达渠道</h4>
+            </div>
+            <div class="channel-detail-list">
+              <div class="channel-detail-item">
+                <div class="channel-icon-wrapper" :style="{ background: 'rgba(99, 102, 241, 0.2)', color: 'var(--primary)' }">
+                  <i class="fas fa-envelope"></i>
+                </div>
+                <div class="channel-detail-info">
+                  <div class="channel-detail-name">邮件</div>
+                  <div class="channel-detail-stats">
+                    <span>发送: {{ Math.round(selectedCampaign.sendVolume) }}</span>
+                    <span>打开率: {{ selectedCampaign.openRate }}%</span>
+                    <span>回复率: {{ selectedCampaign.replyRate }}%</span>
+                  </div>
+                </div>
+                <div class="channel-status">
+                  <span class="status-dot active"></span> 运行中
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 目标客户 -->
+          <div class="detail-section">
+            <div class="section-header">
+              <h4><i class="fas fa-users"></i> 目标客户</h4>
+            </div>
+            <div class="target-info">
+              <div class="target-stat">
+                <div class="target-value">{{ customerData.length }}</div>
+                <div class="target-label">目标客户数</div>
+              </div>
+              <div class="target-stat">
+                <div class="target-value">0</div>
+                <div class="target-label">已触达</div>
+              </div>
+              <div class="target-stat">
+                <div class="target-value">0</div>
+                <div class="target-label">待触达</div>
+              </div>
+            </div>
+          </div>
+
+          <!-- SOP 序列 -->
+          <div class="detail-section">
+            <div class="section-header">
+              <h4><i class="fas fa-robot"></i> SOP 自动化序列</h4>
+            </div>
+            <div class="sop-detail">
+              <div class="sop-header-info">
+                <span class="sop-name">{{ sopName }}</span>
+                <span class="sop-steps">{{ sopDetails.length }} 步序列</span>
+              </div>
+              <div class="sop-timeline-detail">
+                <div class="sop-step-item pending" v-for="item in sopDetails" :key="item.id">
+                  <div class="step-marker">{{ item.step }}</div>
+                  <div class="step-content">
+                    <div class="step-title">{{ item.name }}</div>
+                    <div class="step-meta">第{{ item.days }}天 · 邮件 · 待执行</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 最近互动 -->
+          <div class="detail-section">
+            <div class="section-header">
+              <h4><i class="fas fa-comments"></i> 最近互动</h4>
+              <button class="btn-link" @click="showAllInteractions">查看全部</button>
+            </div>
+            <div class="interactions-list">
+              <div class="interaction-item">
+                <div class="interaction-avatar">M</div>
+                <div class="interaction-content">
+                  <div class="interaction-header">
+                    <span class="interaction-name">Mohammed Al-Rashid</span>
+                    <span class="interaction-time">10分钟前</span>
+                  </div>
+                  <div class="interaction-text">回复了您的邮件,询问价格和交期</div>
+                  <div class="interaction-tags">
+                    <span class="tag reply">回复</span>
+                    <span class="tag interested">高意向</span>
+                  </div>
+                </div>
+              </div>
+              <div class="interaction-item">
+                <div class="interaction-avatar">A</div>
+                <div class="interaction-content">
+                  <div class="interaction-header">
+                    <span class="interaction-name">Ahmed Hassan</span>
+                    <span class="interaction-time">1小时前</span>
+                  </div>
+                  <div class="interaction-text">打开了您的邮件</div>
+                  <div class="interaction-tags">
+                    <span class="tag open">打开</span>
+                  </div>
+                </div>
+              </div>
+              <div class="interaction-item">
+                <div class="interaction-avatar">S</div>
+                <div class="interaction-content">
+                  <div class="interaction-header">
+                    <span class="interaction-name">Sarah Williams</span>
+                    <span class="interaction-time">3小时前</span>
+                  </div>
+                  <div class="interaction-text">点击了邮件中的产品链接</div>
+                  <div class="interaction-tags">
+                    <span class="tag click">点击</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="modal-footer">
+          <button class="btn btn-secondary" @click="duplicateCampaign(selectedCampaign)">
+            <i class="fas fa-copy"></i> 复制活动
+          </button>
+          <button class="btn btn-secondary" @click="exportCampaignReport(selectedCampaign)">
+            <i class="fas fa-download"></i> 导出报告
+          </button>
+          <button class="btn btn-secondary" @click="showCampaignDetailModal = false">关闭</button>
+          <button class="btn btn-primary" @click="editCampaign(selectedCampaign)">
+            <i class="fas fa-edit"></i> 编辑活动
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <AddMarketing ref="addMarketingRef" @refresh="getData(selectedCampaign.id)" />
+</template>
+
+<script setup>
+    import { ref } from 'vue'
+    import { getMarketingById } from '@/api/marketing'
+    import AddMarketing from '../../components/addMarketing.vue'
+
+    const getStatusLabel = (s) => ({ 0: '未开始', 1: '进行中', 2: '已暂停', 3: '已完成' }[s] || s)
+    const getTypeLabel = (type) => ({ 1: '潜客开发', 2: '客户培养', 3: '客户唤醒', 4: '产品推广', 5: '节日营销', 6: '展会跟进' }[type] || type)
+    const getPriorityLabel = (priority) => ({ 1: '紧急', 2: '高优先', 3: '中等', 4: '低优先' }[priority] || priority)
+    const formatNumber = (n) => n >= 1000 ? (n / 1000).toFixed(1) + 'K' : n.toLocaleString()
+
+    const showCampaignDetailModal = ref(false)
+    const selectedCampaign = ref()
+    const sopDetails = ref([])
+    const customerData = ref([])
+    const open = (data) => {
+        getData(data.id)
+        showCampaignDetailModal.value = true
+    }
+
+    const sopName = ref('')
+    const getData = (id) => {
+        getMarketingById(id).then(res => {
+            selectedCampaign.value = res.result.campaign
+            sopDetails.value = res.result.sopDetailList || []
+            customerData.value = res.result.customerList || []
+            sopName.value = res.result.sopEntity.name
+        })
+    }
+
+    const addMarketingRef = ref()
+    const editCampaign = (campaign) => {
+        addMarketingRef.value.open(campaign)
+    }
+
+    defineExpose({
+        open
+    })
+</script>
+
+<style scoped>
+@import '../../assets/styles/marketing.css';
+</style>

+ 525 - 0
ui/smarttrade-platform/src/components/selectUser.vue

@@ -0,0 +1,525 @@
+<template>
+    <div class="sle-user-el-input "  @click="show"
+        :style="`width: ${props.inputWidth ? props.inputWidth + 'px' : '100%'};height:${props.inputHeight ? props.inputHeight + 'px' : '32px'}; flex-grow: ${props.inputWidth ? 'none' :'1' };`">
+        <span style="color: #a8abb2" v-if="finalUsers?.length == 0">{{ placeholder }}</span>
+        <template v-else-if="finalUsers?.length == 1">
+            <el-tag style="margin: 3px 5px 3px 0px" v-for="item in finalUsers" :key="item.id" closable
+                    @close="removeFinalUser(item)">
+                {{ item.realName }}
+            </el-tag>
+        </template>
+        <template v-else>
+            <el-tag style="margin: 3px 5px 3px 0px" >
+                {{ finalUsers[0].realName }}
+            </el-tag>
+            <el-tag style="margin: 3px 5px 3px 0px" >
+                {{ finalUsers.length }} +
+            </el-tag>
+        </template>
+        <el-input v-model="userIds" style="display: none;"></el-input>
+    </div>
+
+    <div v-if="visible" class="modal-overlay" @click.self="visible">
+        <div class="modal create-modal" style="max-width: 1100px;">
+            <div class="modal-header">
+                <div class="modal-title">
+                    选择用户
+                </div>
+                <button class="modal-close" @click="closeCreateModal">
+                    <i class="fas fa-times"></i>
+                </button>
+            </div>
+            <div class="modal-body">
+                <el-row>
+                    <el-col :span="4" style="border-right: 1px solid #344156;overflow-x: auto">
+                        <el-scrollbar max-height="650px" min-height="650px" style="overflow-x: auto">
+                            <el-tree :data="deptOptions" ref="deptRef" :default-checked-keys="defaultCheckedKeys"
+                                    show-checkbox @check="getCheckedNodes" node-key="treeId"
+                                :check-strictly="!deptCheckStrictly" empty-text="加载中,请稍候"
+                                :props="{ label: 'title', children: 'children' }"></el-tree>
+                        </el-scrollbar>
+                    </el-col>
+                    <el-col v-loading="loadingGetData"
+                            :span="15" style="padding: 0 10px;">
+
+                        <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+                            <el-form-item prop="blurry" style="margin-right: 5px">
+                                <el-input v-model="blurry" style="width: 160px" @clear="handleQuery" placeholder="请输入用户名称" clearable
+                                    @keyup.stop="handleQuery" />
+                            </el-form-item>
+                            <el-form-item>
+                                <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
+                                <el-checkbox v-if="multiple" style="margin-left: 5px;margin-right: 5px"
+                                            v-model="checkAll"
+                                            @change="selectAll"
+                                            label="全选/全不选" border></el-checkbox>
+        <!--                        <el-checkbox style="margin-left: 5px;margin-right: 5px" border v-model="reSelectValu" @change="reSelect" label="反选"></el-checkbox>-->
+
+                                <el-checkbox style="margin-left: 5px;margin-right: 5px" border v-model="isIncludeResign"  @change="getIncludeResign" label="离职人员"></el-checkbox>
+                            </el-form-item>
+                        </el-form>
+                        <el-row type="flex">
+                            <div class="userList" style="max-height: 600px;overflow-y: scroll">
+                                <el-checkbox-group v-if="multiple" v-model="checkUserIds">
+                                    <template v-for="(item, index) in userList" :key="index">
+                                        <el-checkbox v-show="!item.hiden" :value="item.id" style="margin: 0 5px 5px 0" border>
+                                            {{ item.realName }}({{ item.deptName }}) {{item.resignDate ? '(离职)' : ''}}
+                                        </el-checkbox>
+                                    </template>
+                                </el-checkbox-group>
+
+                                <el-radio-group v-else v-model="checkUserId">
+                                    <template v-for="(item, index) in userList" :key="index">
+                                        <el-radio style="margin: 0 5px 5px 0" v-show="!item.hiden" :value="item.id" border>
+                                            {{ item.realName }}({{ item.deptName }}){{item.resignDate ? '(离职)' : ''}}
+                                        </el-radio>
+                                    </template>
+                                </el-radio-group>
+
+
+
+                            </div>
+
+                        </el-row>
+                    </el-col>
+                    <el-col :span="5">
+
+
+                        <el-card shadow="never" style="height: 100%">
+                            <template #header>
+                                <div class="card-header">
+                                    <span>已选择用户{{ multiple ? '(多选)' : '(单选)' }}</span>
+                                </div>
+                            </template>
+                            <el-scrollbar max-height="550px" min-height="400px">
+                                <el-row v-for="(item, index) in selectUsers" :key="index" style="margin-bottom: 5px">
+                                    <el-col :span="24">
+                                        <el-tag closable @close="remove(item)">
+                                            <el-icon color="#1874FF"><Select /></el-icon>
+                                            {{ item.realName }}
+                                        </el-tag>
+                                    </el-col>
+                                </el-row>
+                            </el-scrollbar>
+                        </el-card>
+
+                    </el-col>
+                </el-row>
+            </div>
+            <div class="modal-footer">
+                <el-button @click="reset">取 消</el-button>
+                <el-button type="primary" @click="handleSelectUser">提交</el-button>
+            </div>
+        </div>
+    </div>
+
+</template>
+
+<script setup>
+import {ref, reactive, watch, computed, onMounted} from 'vue'
+import { getDeptTree, queryAllUser } from '@/api/layout'
+import { Search, Select } from '@element-plus/icons-vue'
+// import {useAllUserStore} from "@/store/modules/allUser";
+
+// const allUserStore = useAllUserStore()
+const emits = defineEmits(['onOk'])
+//组织树是否父子关联
+const deptCheckStrictly = ref(true)
+//是否全选
+const checkAll = ref(false)
+const reSelectValu = ref(false)
+const blurry = ref('')
+const isSearching = ref(false)
+const loadingGetData = ref(false)
+const isIncludeResign = ref(false)
+const deptRef = ref();
+// 组织树
+const deptOptions = ref([])
+const defaultCheckedKeys = ref([])
+//多选
+const checkUserIds = ref([]);
+//单选
+const checkUserId = ref();
+const userIds = defineModel('modelValue', { required: true })
+
+// const router = useRouter();
+//定义props
+const props = defineProps({
+
+    multiple: {
+        type: Boolean,
+        default: false
+    },
+    arrayToString: {
+        type: Boolean,
+        default: false
+    },
+    //建议使用复选框
+    showDeptCheckBox: {
+        type: Boolean,
+        default: true
+    },
+    inputWidth: {
+        type: Number,
+    },
+    inputHeight: {
+        type: Number,
+    },
+    deptIds: {
+        type: [Array, String, Number],
+    },
+
+    placeholder: {
+        type: String,
+        default: '请选择用户'
+    },
+    authorty:{
+        type: Boolean,
+        default: false
+    }
+})
+
+watch(() => userIds.value, async (value) => {
+    init()
+})
+watch(() => props.deptIds, async (value) => {
+
+    if(!value){
+        defaultCheckedKeys.value = []
+        return
+    }
+    //判断value如果是数组
+    if (Array.isArray(value)) {
+        if(value.length == 0) {
+            defaultCheckedKeys.value = []
+            return
+        }
+        defaultCheckedKeys.value = value.map(item => "30-" + item)
+        return
+    }
+    defaultCheckedKeys.value = ["30-" + value]
+
+})
+
+
+const remove = (item)=>{
+    if (props.multiple) {
+        checkUserIds.value = checkUserIds.value.filter(i => i != item.id)
+    } else {
+        checkUserId.value = undefined
+    }
+}
+
+
+const removeFinalUser = (item) => {
+    if (props.multiple) {
+        finalUsers.value = finalUsers.value.filter(i => i.id != item.id)
+        userIds.value = finalUsers.value.map(item => item.id)
+        return
+    }
+    finalUsers.value = []
+    userIds.value = undefined
+}
+//获取当前实例
+// const {proxy} = getCurrentInstance();
+//用户列表
+const userList = ref([]);
+//弹窗
+const visible = ref(false);
+//一共多少条数据
+const total = ref(0);
+
+//部门过滤id
+const filterDeptIdList = ref([]);
+
+const queryParams = reactive({
+    pageIndex: 1,
+    pageSize: 10000,
+})
+const selectUsers = computed(() => {
+    if (props.multiple) {
+        return userList.value.filter(item => checkUserIds.value.includes(item.id))
+    }
+    return userList.value.filter(item => checkUserId.value === item.id)
+})
+const finalUsers = ref([])
+const getIncludeResign = async (val)=>{
+    // allUserStore.clearUser()
+    // allUserStore.setIsIncludeResign(val)
+    await init()
+    if(checkAll.value) {
+        await selectAll()
+    }
+    userList.value.forEach(item => {
+        if(filterDeptIdList.value.length > 0) {
+            if (filterDeptIdList.value.some(i => i == item.deptId)) {
+                item.hiden = false
+            } else {
+                item.hiden = true
+            }
+        }
+
+    })
+}
+
+const init = async () => {
+    await getList()
+    if (props.multiple) {
+        if (!!!userIds.value) {
+            checkUserIds.value = []
+        } else {
+            if (!Array.isArray(userIds.value)) {
+                throw new Error("接收到不是数组,请不要配置multiple属性")
+                return
+            }
+            checkUserIds.value = userIds.value.concat()
+        }
+        finalUsers.value = userList.value.filter(item => checkUserIds.value.includes(item.id))
+    } else {
+        if (Array.isArray(userIds.value)) {
+            throw new Error("接收到数组,请配置multiple属性")
+            return
+        }
+        // checkUserId.value = userIds.value || ''
+
+        finalUsers.value = userList.value.filter(item => userIds.value == item.id)
+    }
+
+}
+
+const queryFormRef = ref();
+
+// const menuId = computed(()=>{
+//     if(props.authorty){
+//         return router.currentRoute.value?.meta?.menuId
+//     }
+//     return ''
+// })
+
+/** 查询组织树 */
+const handleDataAuthUpdate = async () => {
+    let typs = [10, 20, 30]
+    const res = await getDeptTree({ orgTypes: [10, 20, 30], reviewTag: 1 });
+    deptOptions.value = res.result;
+}
+
+const show = async () => {
+    filterDeptIdList.value = []
+    // isIncludeResign.value = allUserStore.getIsIncludeResign()
+    blurry.value = ''
+    await init()
+    filterByDeptId( defaultCheckedKeys.value)
+    handleDataAuthUpdate()
+    visible.value = true;
+}
+const getCheckedNodes = () => {
+    if (!props.showDeptCheckBox) return
+    if(checkAll.value ){
+
+        checkAll.value = false
+    }
+
+    //获取部门
+    let nodes = deptRef.value?.getCheckedNodes() ?? []
+    filterDeptIdList.value = []
+
+    if (nodes.length == 0) {
+        userList.value.forEach(item => {
+            item.hiden = false
+        })
+        filterDeptIdList.value = []
+        // checkUserIds.value = []
+        return
+    }
+
+    let arr = nodes.map(item=>item.treeId)
+    filterByDeptId(arr)
+    // nodes.forEach(item => {
+    //     let str: string[] = item.treeId.split('-')
+    //     if (str[0] == '30') {
+    //         filterDeptIdList.value.push(str[1])
+    //     }
+    // })
+    // userList.value.forEach(item => {
+    //     if (filterDeptIdList.value.some(i => i == item.deptId)) {
+    //         item.hiden = false
+    //     } else {
+    //         item.hiden = true
+    //     }
+    // })
+    // let filterUser = userList.value.filter(item => !item.hiden)
+    // console.log(filterUser, "filterUser")
+    // let ids = filterUser.map(item =>  item.id)
+    // checkUserIds.value = ids
+}
+
+const filterByDeptId = (deptIds)=>{
+    if(!deptIds || deptIds.length == 0) return
+    deptIds.forEach(item => {
+        let str = item.split('-')
+        if (str[0] == '30') {
+            filterDeptIdList.value.push(str[1])
+        }
+    })
+    userList.value.forEach(item => {
+        if (filterDeptIdList.value.some(i => i == item.deptId)) {
+            item.hiden = false
+        } else {
+            item.hiden = true
+        }
+    })
+
+}
+
+
+const difference = (arr1, arr2) => {
+    return arr1.filter(x => !arr2.includes(x));
+}
+//全选/全不选
+const selectAll = async () => {
+
+    let filterUser = userList.value.filter(item => !item.hiden)
+    let ids = []
+    if(filterDeptIdList.value && filterDeptIdList.value.length > 0) {
+        ids = filterUser.map(item => filterDeptIdList.value.some(i => i == item.deptId) && item.id)
+    } else {
+        ids = filterUser.map(item => item.id)
+    }
+
+    if (!checkAll.value) {
+        checkUserIds.value = difference(checkUserIds.value, ids);
+        return
+    }
+    checkUserIds.value.push(...ids)
+}
+//reSelect 反选
+const reSelect = () => {
+    let filterUser = userList.value.filter(item => !item.hiden)
+    let ids = filterUser.map(item => item.id)
+    //已经选中的id集合
+    let checkedIds = ids.filter(item => checkUserIds.value.some(id => id == item))
+    //没有选中的集合
+    let noCheckedIds = ids.filter(item => !checkUserIds.value.includes(item))
+    console.log(checkedIds, noCheckedIds)
+    checkUserIds.value = difference(checkUserIds.value, checkedIds);
+    checkUserIds.value.push(...noCheckedIds)
+}
+
+
+
+
+/** 查询数据 */
+const getList = async () => {
+
+    // const res = await listUser(queryParams);
+    // if (allUserStore.getUser().length <= 0 || props.authorty) {
+        loadingGetData.value = true
+        let res = await queryAllUser({ isIncludeResign: isIncludeResign.value, reviewTag: 1, pageIndex: 1, pageSize: 10000 })
+        userList.value = res.result.records
+        loadingGetData.value = false
+        return
+    // }
+    // userList.value = JSON.parse(JSON.stringify(allUserStore.getUser()) )?? [];
+    // total.value = res.result.total;
+
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+
+    userList.value.forEach(item => {
+
+        if (!!!blurry.value) {
+            if(filterDeptIdList.value.length <= 0){
+                item.hiden = false
+            }else {
+                if (filterDeptIdList.value.some(i => i == item.deptId)) {
+                    item.hiden = false
+                } else {
+                    item.hiden = true
+                }
+            }
+        } else {
+            if(filterDeptIdList.value.length <= 0){
+                if ((item.realName.includes(blurry.value) || item.userName.includes(blurry.value)) ) {
+                    item.hiden = false
+                } else {
+                    item.hiden = true
+                }
+            } else {
+                if (filterDeptIdList.value.some(i => i == item.deptId)) {
+                    if ((item.realName.includes(blurry.value) || item.userName.includes(blurry.value)) ) {
+                        item.hiden = false
+                    } else {
+                        item.hiden = true
+                    }
+                } else {
+                    item.hiden = true
+                }
+
+            }
+
+        }
+    })
+}
+/** 重置按钮操作 */
+const reset = () => {
+    visible.value = false
+}
+
+/**选择最终确认 */
+const handleSelectUser = async () => {
+    finalUsers.value = selectUsers.value
+    if (props.multiple) {
+        userIds.value = checkUserIds.value
+    } else {
+        userIds.value = checkUserId.value
+    }
+    emits('onOk', finalUsers.value)
+    visible.value = false
+}
+// 暴露
+defineExpose({
+    show,
+});
+onMounted(async () => {
+    init()
+})
+</script>
+
+<style scoped>
+.sle-user-el-input {
+    display: flex;
+    overflow-y: hidden;
+    overflow-x: hidden;
+    /*flex-grow: 1;*/
+    align-items: center;
+    justify-content: flex-start;
+    padding: 1px 11px;
+    background-color: var(--bg-dark);
+    background-image: none;
+    border: 1px solid var(--border);
+    border-radius: 8px;
+    transition: var(--el-transition-box-shadow);
+    position: relative;
+}
+.el-tree{
+    background: transparent;
+}
+.el-tree :deep(.el-text){
+    color: #FFF;
+}
+.el-tree :deep(.el-tree-node__content):hover{
+    background: transparent;
+}
+.el-row :deep(.el-checkbox__label){
+    color: #FFF;
+}
+.el-card{
+    background: transparent;
+    color: #FFF;
+    border-color: #344156;
+}
+.el-card :deep(.el-card__header){
+    border-bottom: 1px solid #344156;
+}
+</style>

+ 5 - 234
ui/smarttrade-platform/src/views/MarketingView.vue

@@ -142,219 +142,7 @@
     </div>
 
     <!-- Campaign Detail Modal -->
-    <div v-if="showCampaignDetailModal" class="modal-overlay" @click.self="showCampaignDetailModal = false">
-      <div class="modal campaign-detail-modal">
-        <div class="modal-header">
-          <div class="modal-title">
-            <i class="fas fa-chart-bar"></i> 活动详情
-            <span class="detail-status" :class="selectedCampaign?.status">{{ getStatusLabel(selectedCampaign?.status) }}</span>
-          </div>
-          <button class="modal-close" @click="showCampaignDetailModal = false"><i class="fas fa-times"></i></button>
-        </div>
-        <div class="modal-body" v-if="selectedCampaign">
-          <!-- 基本信息 -->
-          <div class="detail-section">
-            <div class="section-header">
-              <h4><i class="fas fa-info-circle"></i> 基本信息</h4>
-            </div>
-            <div class="detail-grid">
-              <div class="detail-item">
-                <label>活动名称</label>
-                <span>{{ selectedCampaign.name }}</span>
-              </div>
-              <div class="detail-item">
-                <label>活动类型</label>
-                <span>{{ getTypeLabel(selectedCampaign.type) }}</span>
-              </div>
-              <div class="detail-item">
-                <label>优先级</label>
-                <span>{{ getPriorityLabel(selectedCampaign.priority) }}</span>
-              </div>
-              <div class="detail-item">
-                <label>负责人</label>
-                <span>{{ selectedCampaign.owner || '张经理' }}</span>
-              </div>
-              <div class="detail-item">
-                <label>创建时间</label>
-                <span>{{ selectedCampaign.createTime }}</span>
-              </div>
-              <div class="detail-item">
-                <label>活动周期</label>
-                <span>{{ selectedCampaign.beginData }} 至 {{ selectedCampaign.endDate }}</span>
-              </div>
-              <div class="detail-item full-width">
-                <label>活动描述</label>
-                <span>{{ selectedCampaign.description }}</span>
-              </div>
-            </div>
-          </div>
-
-          <!-- 核心数据 -->
-          <div class="detail-section">
-            <div class="section-header">
-              <h4><i class="fas fa-chart-line"></i> 核心数据</h4>
-              <div class="progress-info">达成 {{ selectedCampaign.progressPercent || 0 }}%</div>
-            </div>
-            <div class="detail-progress-bar">
-              <div class="progress-fill" :style="{ width: (selectedCampaign.progressPercent || 0) + '%' }"></div>
-            </div>
-            <div class="metrics-grid">
-              <div class="metric-card">
-                <div class="metric-icon blue"><i class="fas fa-paper-plane"></i></div>
-                <div class="metric-content">
-                  <div class="metric-value">{{ formatNumber(selectedCampaign.sendVolume) }}</div>
-                  <div class="metric-label">已发送</div>
-                </div>
-              </div>
-              <div class="metric-card">
-                <div class="metric-icon green"><i class="fas fa-envelope-open"></i></div>
-                <div class="metric-content">
-                  <div class="metric-value">{{ Math.round(selectedCampaign.sendVolume * selectedCampaign.openRate / 100) }}</div>
-                  <div class="metric-label">打开数 ({{ selectedCampaign.openRate }}%)</div>
-                </div>
-              </div>
-              <div class="metric-card">
-                <div class="metric-icon purple"><i class="fas fa-reply"></i></div>
-                <div class="metric-content">
-                  <div class="metric-value">{{ Math.round(selectedCampaign.sendVolume * selectedCampaign.replyRate / 100) }}</div>
-                  <div class="metric-label">回复数 ({{ selectedCampaign.replyRate }}%)</div>
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <!-- 触达渠道 -->
-          <div class="detail-section">
-            <div class="section-header">
-              <h4><i class="fas fa-bullhorn"></i> 触达渠道</h4>
-            </div>
-            <div class="channel-detail-list">
-              <div class="channel-detail-item">
-                <div class="channel-icon-wrapper" :style="getChannelStyleByName('邮件')">
-                  <i :class="getChannelIcon('邮件')"></i>
-                </div>
-                <div class="channel-detail-info">
-                  <div class="channel-detail-name">邮件</div>
-                  <div class="channel-detail-stats">
-                    <span>发送: {{ Math.round(selectedCampaign.sendVolume) }}</span>
-                    <span>打开率: {{ selectedCampaign.openRate }}%</span>
-                    <span>回复率: {{ selectedCampaign.replyRate }}%</span>
-                  </div>
-                </div>
-                <div class="channel-status">
-                  <span class="status-dot active"></span> 运行中
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <!-- 目标客户 -->
-          <div class="detail-section">
-            <div class="section-header">
-              <h4><i class="fas fa-users"></i> 目标客户</h4>
-            </div>
-            <div class="target-info">
-              <div class="target-stat">
-                <div class="target-value">{{ selectedCampaign.targetCount || 847 }}</div>
-                <div class="target-label">目标客户数</div>
-              </div>
-              <div class="target-stat">
-                <div class="target-value">{{ selectedCampaign.reachedCount || 623 }}</div>
-                <div class="target-label">已触达</div>
-              </div>
-              <div class="target-stat">
-                <div class="target-value">{{ selectedCampaign.pendingCount || 224 }}</div>
-                <div class="target-label">待触达</div>
-              </div>
-            </div>
-          </div>
-
-          <!-- SOP 序列 -->
-          <div class="detail-section">
-            <div class="section-header">
-              <h4><i class="fas fa-robot"></i> SOP 自动化序列</h4>
-            </div>
-            <div class="sop-detail">
-              <div class="sop-header-info">
-                <span class="sop-name">{{ sopDetails.name }}</span>
-                <span class="sop-steps">{{ sopDetails.details.length }} 步序列</span>
-              </div>
-              <div class="sop-timeline-detail">
-                <div class="sop-step-item pending" v-for="item in sopDetails.details" :key="item.id">
-                  <div class="step-marker">{{ item.step }}</div>
-                  <div class="step-content">
-                    <div class="step-title">{{ item.name }}</div>
-                    <div class="step-meta">第{{ item.days }}天 · 邮件 · 待执行</div>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <!-- 最近互动 -->
-          <div class="detail-section">
-            <div class="section-header">
-              <h4><i class="fas fa-comments"></i> 最近互动</h4>
-              <button class="btn-link" @click="showAllInteractions">查看全部</button>
-            </div>
-            <div class="interactions-list">
-              <div class="interaction-item">
-                <div class="interaction-avatar">M</div>
-                <div class="interaction-content">
-                  <div class="interaction-header">
-                    <span class="interaction-name">Mohammed Al-Rashid</span>
-                    <span class="interaction-time">10分钟前</span>
-                  </div>
-                  <div class="interaction-text">回复了您的邮件,询问价格和交期</div>
-                  <div class="interaction-tags">
-                    <span class="tag reply">回复</span>
-                    <span class="tag interested">高意向</span>
-                  </div>
-                </div>
-              </div>
-              <div class="interaction-item">
-                <div class="interaction-avatar">A</div>
-                <div class="interaction-content">
-                  <div class="interaction-header">
-                    <span class="interaction-name">Ahmed Hassan</span>
-                    <span class="interaction-time">1小时前</span>
-                  </div>
-                  <div class="interaction-text">打开了您的邮件</div>
-                  <div class="interaction-tags">
-                    <span class="tag open">打开</span>
-                  </div>
-                </div>
-              </div>
-              <div class="interaction-item">
-                <div class="interaction-avatar">S</div>
-                <div class="interaction-content">
-                  <div class="interaction-header">
-                    <span class="interaction-name">Sarah Williams</span>
-                    <span class="interaction-time">3小时前</span>
-                  </div>
-                  <div class="interaction-text">点击了邮件中的产品链接</div>
-                  <div class="interaction-tags">
-                    <span class="tag click">点击</span>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div class="modal-footer">
-          <button class="btn btn-secondary" @click="duplicateCampaign(selectedCampaign)">
-            <i class="fas fa-copy"></i> 复制活动
-          </button>
-          <button class="btn btn-secondary" @click="exportCampaignReport(selectedCampaign)">
-            <i class="fas fa-download"></i> 导出报告
-          </button>
-          <button class="btn btn-secondary" @click="showCampaignDetailModal = false">关闭</button>
-          <button class="btn btn-primary" @click="editCampaign(selectedCampaign)">
-            <i class="fas fa-edit"></i> 编辑活动
-          </button>
-        </div>
-      </div>
-    </div>
+    <MarketingDetail ref="marketingDetailRef" />
 
     <!-- All Interactions Modal -->
     <div v-if="showInteractionsModal" class="modal-overlay" @click.self="showInteractionsModal = false">
@@ -1115,6 +903,7 @@ import { getMarketingList, getSopById, marketingAnalysis } from '../api/marketin
 import OpenedCustomers from '../components/marketing/openedCustomers.vue'
 import AiGenerator from '../components/marketing/aiGenerator.vue'
 import EmailList from '../components/marketing/emailList.vue'
+import MarketingDetail from '../components/marketing/marketingDetail.vue'
 
 const filterTab = ref('all')
 
@@ -1124,7 +913,7 @@ const showHistoryDetailModal = ref(false)
 const selectedHistoryCampaign = ref(null)
 const showTemplatePreview = ref(false)
 const showCreateTemplateModal = ref(false)
-const showCampaignDetailModal = ref(false)
+
 const showInteractionsModal = ref(false)
 const selectedCampaignInteractions = ref([])
 const selectedCampaign = ref(null)
@@ -1285,8 +1074,6 @@ const formatNumber = (n) => n >= 1000 ? (n / 1000).toFixed(1) + 'K' : n.toLocale
 const getStatusLabel = (s) => ({ 0: '未开始', 1: '进行中', 2: '已暂停', 3: '已完成' }[s] || s)
 const getChannelIcon = (c) => ({ '邮件': 'fas fa-envelope', 'WhatsApp': 'fab fa-whatsapp', 'LinkedIn': 'fab fa-linkedin' }[c] || 'fas fa-comment')
 
-const getTypeLabel = (type) => ({ 1: '潜客开发', 2: '客户培养', 3: '客户唤醒', 4: '产品推广', 5: '节日营销', 6: '展会跟进' }[type] || type)
-const getPriorityLabel = (priority) => ({ 1: '紧急', 2: '高优先', 3: '中等', 4: '低优先' }[priority] || priority)
 const getRegionLabel = (region) => ({ 'middle-east': '中东', 'europe': '欧洲', 'north-america': '北美', 'southeast-asia': '东南亚', 'africa': '非洲', 'south-america': '南美' }[region] || '全部地区')
 const getIndustryLabel = (industry) => ({ 'datacenter': '数据中心', 'consumer': '消费电子', 'distributor': '电子分销', 'retail': '零售', 'manufacturing': '制造业' }[industry] || '全部行业')
 const getChannelName = (id) => ({ email: '邮件营销', whatsapp: 'WhatsApp', linkedin: 'LinkedIn', wechat: '微信企业号' }[id] || id)
@@ -1331,15 +1118,10 @@ const copyEmail = () => {
 const useTemplate = (tpl) => { showTemplatesModal.value = false; toast(`已应用模板: ${tpl.subject}`, 'success') }
 const toggleCampaign = (c) => { c.status = c.status === 'paused' ? 'active' : 'paused'; toast(c.status === 'paused' ? '已暂停' : '已恢复', 'success') }
 
-const sopDetails = ref()
+const marketingDetailRef = ref()
 const showCampaignDetail = (campaign) => {
   selectedCampaign.value = campaign
-  showCampaignDetailModal.value = true
-  getSopById(campaign.sopId).then(res => {
-    if(res.code == 200){
-      sopDetails.value = res.result
-    }
-  })
+  marketingDetailRef.value.open(campaign)
 }
 
 const showAllInteractions = () => {
@@ -1584,12 +1366,6 @@ const resumeCampaignForCustomer = (customer) => {
   toast(`已恢复向 ${customer.name} 发送营销邮件`, 'success')
 }
 
-// 活动详情方法
-const openCampaignDetailModal = (campaign) => {
-  selectedCampaign.value = campaign
-  showCampaignDetailModal.value = true
-}
-
 const getChannelStyleByName = (ch) => {
   const styles = {
     '邮件': { background: 'rgba(99, 102, 241, 0.2)', color: 'var(--primary)' },
@@ -1603,11 +1379,6 @@ const getChannelStyleByName = (ch) => {
 const exportCampaignReport = (campaign) => {
   toast(`正在导出"${campaign.name}"报告`, 'success')
 }
-
-const editCampaign = (campaign) => {
-  showCampaignDetailModal.value = false
-  addMarketingRef.value.open(campaign)
-}
 </script>
 
 <style scoped>