Selaa lähdekoodia

客户管理增加AI聊天功能

peicj 19 tuntia sitten
vanhempi
commit
60b10091c4

+ 51 - 0
public/static/images/ai-avatar.svg

@@ -0,0 +1,51 @@
+<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+  <!-- 渐变背景 -->
+  <defs>
+    <linearGradient id="aiGrad" x1="0%" y1="0%" x2="100%" y2="100%">
+      <stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
+      <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
+    </linearGradient>
+    
+    <!-- 发光效果 -->
+    <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
+      <feGaussianBlur stdDeviation="3" result="coloredBlur"/>
+      <feMerge>
+        <feMergeNode in="coloredBlur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+  </defs>
+  
+  <!-- 圆形背景 -->
+  <circle cx="50" cy="50" r="48" fill="url(#aiGrad)"/>
+  
+  <!-- AI 芯片/大脑图案 -->
+  <g filter="url(#glow)">
+    <!-- 中心圆环 -->
+    <circle cx="50" cy="50" r="20" fill="none" stroke="white" stroke-width="2.5" opacity="0.9"/>
+    <circle cx="50" cy="50" r="14" fill="none" stroke="white" stroke-width="2" opacity="0.7"/>
+    
+    <!-- 四个角的连接点 -->
+    <circle cx="50" cy="30" r="3" fill="white"/>
+    <circle cx="70" cy="50" r="3" fill="white"/>
+    <circle cx="50" cy="70" r="3" fill="white"/>
+    <circle cx="30" cy="50" r="3" fill="white"/>
+    
+    <!-- 连接线 -->
+    <line x1="50" y1="33" x2="50" y2="47" stroke="white" stroke-width="1.5" opacity="0.6"/>
+    <line x1="67" y1="50" x2="53" y2="50" stroke="white" stroke-width="1.5" opacity="0.6"/>
+    <line x1="50" y1="67" x2="50" y2="53" stroke="white" stroke-width="1.5" opacity="0.6"/>
+    <line x1="33" y1="50" x2="47" y2="50" stroke="white" stroke-width="1.5" opacity="0.6"/>
+    
+    <!-- 中心 AI 核心 -->
+    <circle cx="50" cy="50" r="6" fill="white" opacity="0.9"/>
+    
+    <!-- 电路纹理 -->
+    <path d="M 50 30 L 50 25 M 50 75 L 50 70 M 30 50 L 25 50 M 75 50 L 70 50" 
+          stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
+  </g>
+  
+  <!-- AI 标识文字 -->
+  <text x="50" y="54" font-family="Arial, sans-serif" font-size="10" font-weight="bold" 
+        fill="white" text-anchor="middle" dominant-baseline="middle" opacity="0.95">AI</text>
+</svg>

+ 41 - 0
public/static/images/customer-avatar.svg

@@ -0,0 +1,41 @@
+<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+  <!-- 渐变背景 -->
+  <defs>
+    <linearGradient id="customerGrad" x1="0%" y1="0%" x2="100%" y2="100%">
+      <stop offset="0%" style="stop-color:#10b981;stop-opacity:1" />
+      <stop offset="100%" style="stop-color:#059669;stop-opacity:1" />
+    </linearGradient>
+    
+    <!-- 发光效果 -->
+    <filter id="customerGlow" x="-50%" y="-50%" width="200%" height="200%">
+      <feGaussianBlur stdDeviation="2" result="coloredBlur"/>
+      <feMerge>
+        <feMergeNode in="coloredBlur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+  </defs>
+  
+  <!-- 圆形背景 -->
+  <circle cx="50" cy="50" r="48" fill="url(#customerGrad)"/>
+  
+  <!-- 专业用户图标 -->
+  <g filter="url(#customerGlow)">
+    <!-- 外环装饰 -->
+    <circle cx="50" cy="50" r="35" fill="none" stroke="white" stroke-width="1.5" opacity="0.3"/>
+    
+    <!-- 头部轮廓 -->
+    <circle cx="50" cy="40" r="16" fill="white" opacity="0.95"/>
+    
+    <!-- 身体轮廓 -->
+    <path d="M 28 82 Q 28 65 50 65 Q 72 65 72 82 L 72 85 Q 72 88 68 88 L 32 88 Q 28 88 28 85 Z" 
+          fill="white" opacity="0.9"/>
+    
+    <!-- 领带/商务元素 -->
+    <path d="M 50 65 L 50 78 M 45 68 L 50 65 L 55 68" 
+          stroke="#10b981" stroke-width="2" fill="none" opacity="0.4" stroke-linecap="round" stroke-linejoin="round"/>
+    
+    <!-- 肩膀线条装饰 -->
+    <path d="M 30 75 Q 50 68 70 75" stroke="#10b981" stroke-width="1.5" fill="none" opacity="0.3" stroke-linecap="round"/>
+  </g>
+</svg>

+ 9 - 1
src/api/crm/customerAnalyze.js

@@ -8,6 +8,14 @@ export function listAnalyze(query) {
     params: query
     params: query
   })
   })
 }
 }
+// 查询客户聊天记录分析列表
+export function listAllAnalyze(query) {
+    return request({
+        url: '/crm/analyze/listAll',
+        method: 'get',
+        params: query
+    })
+}
 
 
 // 查询客户聊天记录分析详细
 // 查询客户聊天记录分析详细
 export function getAnalyze(id) {
 export function getAnalyze(id) {
@@ -50,4 +58,4 @@ export function exportAnalyze(query) {
     method: 'get',
     method: 'get',
     params: query
     params: query
   })
   })
-}
+}

+ 217 - 29
src/views/crm/customer/customerDetail.vue

@@ -256,8 +256,9 @@
                     <div class="intention-section">
                     <div class="intention-section">
                         <div class="intention-header">
                         <div class="intention-header">
                             <span class="intention-label">客户意向度</span>
                             <span class="intention-label">客户意向度</span>
+                            <span v-if="getIntentionDegree() === -1" class="no-data-tip">暂无分析数据</span>
                         </div>
                         </div>
-                        <div class="progress-bar-wrapper">
+                        <div class="progress-bar-wrapper" v-if="getIntentionDegree() !== -1">
                             <div class="progress-bar-modern">
                             <div class="progress-bar-modern">
                                 <div class="progress-fill-modern"
                                 <div class="progress-fill-modern"
                                      :style="{
                                      :style="{
@@ -335,24 +336,28 @@ export default {
         // 客户关注点数据(从最新的沟通记录中获取)
         // 客户关注点数据(从最新的沟通记录中获取)
         customerFocusPoints() {
         customerFocusPoints() {
             if (!this.communicationRecords || this.communicationRecords.length === 0) {
             if (!this.communicationRecords || this.communicationRecords.length === 0) {
-                return this.focusPoints; // 返回默认关注点
+                return ['暂无分析数据'];
             }
             }
             const latestRecord = this.communicationRecords[0];
             const latestRecord = this.communicationRecords[0];
             if (latestRecord && latestRecord.customerFocusJson) {
             if (latestRecord && latestRecord.customerFocusJson) {
                 try {
                 try {
-                    // 如果是字符串,解析为 JSON 数组
+                    // 如果是字符串,尝试解析为 JSON 数组
                     if (typeof latestRecord.customerFocusJson === 'string') {
                     if (typeof latestRecord.customerFocusJson === 'string') {
                         const parsed = JSON.parse(latestRecord.customerFocusJson);
                         const parsed = JSON.parse(latestRecord.customerFocusJson);
-                        return Array.isArray(parsed) ? parsed : this.focusPoints;
+                        // 如果解析后的数组为空,返回默认提示
+                        if (Array.isArray(parsed) && parsed.length > 0) {
+                            return parsed;
+                        }
+                    }
+                    // 如果已经是数组且不为空,直接返回
+                    if (Array.isArray(latestRecord.customerFocusJson) && latestRecord.customerFocusJson.length > 0) {
+                        return latestRecord.customerFocusJson;
                     }
                     }
-                    // 如果已经是数组,直接返回
-                    return Array.isArray(latestRecord.customerFocusJson) ? latestRecord.customerFocusJson : this.focusPoints;
                 } catch (error) {
                 } catch (error) {
                     console.error('解析客户关注点 JSON 失败:', error);
                     console.error('解析客户关注点 JSON 失败:', error);
-                    return this.focusPoints;
                 }
                 }
             }
             }
-            return this.focusPoints; // 默认返回硬编码数据
+            return ['暂无分析数据'];
         }
         }
     },
     },
     created() {
     created() {
@@ -530,29 +535,30 @@ export default {
         // 获取流失风险等级标签
         // 获取流失风险等级标签
         getRiskLevelLabel() {
         getRiskLevelLabel() {
             const level = this.getAttritionLevel();
             const level = this.getAttritionLevel();
-            const labels = ['无风险', '低风险', '中风险', '高风险'];
-            return labels[level] || '无风险';
+            const labels = ['未知', '无风险', '低风险', '中风险', '高风险'];
+            return labels[level] || '未知';
         },
         },
         // 获取流失风险等级徽章样式类
         // 获取流失风险等级徽章样式类
         getRiskLevelBadgeClass() {
         getRiskLevelBadgeClass() {
             const level = this.getAttritionLevel();
             const level = this.getAttritionLevel();
             const badgeClasses = [
             const badgeClasses = [
+                'badge-unknown',
                 'badge-none',
                 'badge-none',
                 'badge-low',
                 'badge-low',
                 'badge-medium',
                 'badge-medium',
                 'badge-high'
                 'badge-high'
             ];
             ];
-            return badgeClasses[level] || 'badge-none';
+            return badgeClasses[level] || 'badge-unknown';
         },
         },
         // 获取客户意向度
         // 获取客户意向度
         getIntentionDegree() {
         getIntentionDegree() {
             if (!this.communicationRecords || this.communicationRecords.length === 0) {
             if (!this.communicationRecords || this.communicationRecords.length === 0) {
-                return 0;
+                return -1;
             }
             }
             const latestRecord = this.communicationRecords[0];
             const latestRecord = this.communicationRecords[0];
             const degree = parseInt(latestRecord.intentionDegree);
             const degree = parseInt(latestRecord.intentionDegree);
-            // 如果是有效数字且在 0-100 之间,返回该值,否则返回默认值
-            return (degree >= 0 && degree <= 100) ? degree : 0;
+            // 如果是有效数字且在 0-100 之间,返回该值,否则返回 -1 表示无数据
+            return (degree >= 0 && degree <= 100) ? degree : -1;
         },
         },
         // 获取单条记录的风险等级数值
         // 获取单条记录的风险等级数值
         getRecordAttritionLevel(record) {
         getRecordAttritionLevel(record) {
@@ -562,14 +568,14 @@ export default {
         // 获取单条记录的风险等级标签
         // 获取单条记录的风险等级标签
         getRecordRiskLevelLabel(record) {
         getRecordRiskLevelLabel(record) {
             const level = this.getRecordAttritionLevel(record);
             const level = this.getRecordAttritionLevel(record);
-            const labels = ['无风险', '低风险', '中风险', '高风险'];
-            return labels[level] || '无风险';
+            const labels = ['未知', '无风险', '低风险', '中风险', '高风险'];
+            return labels[level] || '未知';
         },
         },
         // 获取单条记录的风险等级样式类
         // 获取单条记录的风险等级样式类
         getRecordRiskLevelClass(record) {
         getRecordRiskLevelClass(record) {
             const level = this.getRecordAttritionLevel(record);
             const level = this.getRecordAttritionLevel(record);
-            const classes = ['risk-none', 'risk-low', 'risk-medium', 'risk-high'];
-            return classes[level] || 'risk-none';
+            const classes = ['risk-unknown', 'risk-none', 'risk-low', 'risk-medium', 'risk-high'];
+            return classes[level] || 'risk-unknown';
         },
         },
         // 获取单条记录的客户意向度
         // 获取单条记录的客户意向度
         getIntentionDegreeFromRecord(record) {
         getIntentionDegreeFromRecord(record) {
@@ -580,21 +586,23 @@ export default {
         // 获取流失风险等级样式类
         // 获取流失风险等级样式类
         getRiskLevelClass() {
         getRiskLevelClass() {
             const level = this.getAttritionLevel();
             const level = this.getAttritionLevel();
-            const classes = ['risk-none', 'risk-low', 'risk-medium', 'risk-high'];
-            return classes[level] || 'risk-none';
+            const classes = ['risk-unknown', 'risk-none', 'risk-low', 'risk-medium', 'risk-high'];
+            return classes[level] || 'risk-unknown';
         },
         },
         // 获取流失风险分析
         // 获取流失风险分析
         getRiskLevelAnalysis() {
         getRiskLevelAnalysis() {
+            const level = this.getAttritionLevel();
             if (!this.communicationRecords || this.communicationRecords.length === 0) {
             if (!this.communicationRecords || this.communicationRecords.length === 0) {
-                return 0;
+                return '暂无分析数据';
             }
             }
             const latestRecord = this.communicationRecords[0];
             const latestRecord = this.communicationRecords[0];
-            return latestRecord.attritionLevelPrompt || "未获取到分析结果";
+            return latestRecord.attritionLevelPrompt || "暂无分析数据";
         },
         },
         // 获取流失风险提示
         // 获取流失风险提示
         getRiskLevelTip() {
         getRiskLevelTip() {
             const level = this.getAttritionLevel();
             const level = this.getAttritionLevel();
             const tips = [
             const tips = [
+                '暂无分析',
                 '客户稳定,可以放心。',
                 '客户稳定,可以放心。',
                 '建议定期回访,了解客户最新需求。',
                 '建议定期回访,了解客户最新需求。',
                 '建议安排专项跟进,深入了解客户痛点和需求。',
                 '建议安排专项跟进,深入了解客户痛点和需求。',
@@ -727,6 +735,28 @@ export default {
     color: white;
     color: white;
     font-size: 15px;
     font-size: 15px;
     line-height: 1.7;
     line-height: 1.7;
+    max-height: 150px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar {
+    width: 6px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-track {
+    background: rgba(255, 255, 255, 0.2);
+    border-radius: 3px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-thumb {
+    background: rgba(255, 255, 255, 0.4);
+    border-radius: 3px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-thumb:hover {
+    background: rgba(255, 255, 255, 0.6);
 }
 }
 
 
 .card-highlight .summary-meta span {
 .card-highlight .summary-meta span {
@@ -743,10 +773,24 @@ export default {
     transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
     transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
 }
 }
 
 
+.risk-card.risk-unknown {
+    border-left-color: #6b7280;
+    background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
+}
+
+.risk-card.risk-unknown::before {
+    background: linear-gradient(90deg, #6b7280 0%, #4b5563 100%);
+}
+
 .risk-card::before {
 .risk-card::before {
     background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
     background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
 }
 }
 
 
+.risk-unknown .risk-badge {
+    background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
+    box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3);
+}
+
 .risk-none .risk-badge {
 .risk-none .risk-badge {
     background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
     background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
     box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
     box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
@@ -818,6 +862,28 @@ export default {
     background: rgba(255, 255, 255, 0.7);
     background: rgba(255, 255, 255, 0.7);
     border-radius: 12px;
     border-radius: 12px;
     backdrop-filter: blur(10px);
     backdrop-filter: blur(10px);
+    max-height: 200px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.risk-analysis::-webkit-scrollbar {
+    width: 6px;
+}
+
+.risk-analysis::-webkit-scrollbar-track {
+    background: rgba(255, 255, 255, 0.5);
+    border-radius: 3px;
+}
+
+.risk-analysis::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.risk-analysis::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
 }
 }
 
 
 .risk-text {
 .risk-text {
@@ -998,9 +1064,61 @@ export default {
     border: none;
     border: none;
 }
 }
 
 
+/* 沟通摘要样式 */
+.summary-text.compact {
+    max-height: 120px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    line-height: 1.6;
+    font-size: 14px;
+    color: #475569;
+    padding-right: 4px;
+}
+
+.summary-text.compact::-webkit-scrollbar {
+    width: 6px;
+}
+
+.summary-text.compact::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.summary-text.compact::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.summary-text.compact::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
 /* AI 标签美化样式 */
 /* AI 标签美化样式 */
 .tags-container {
 .tags-container {
     padding: 0;
     padding: 0;
+    max-height: 300px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+}
+
+.tags-container::-webkit-scrollbar {
+    width: 6px;
+}
+
+.tags-container::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.tags-container::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.tags-container::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
 }
 }
 
 
 .tags-grid {
 .tags-grid {
@@ -1011,7 +1129,8 @@ export default {
 }
 }
 
 
 .tag-chip {
 .tag-chip {
-    display: inline-flex;
+    display: grid;
+    grid-template-columns: 35% 65%;
     align-items: center;
     align-items: center;
     gap: 8px;
     gap: 8px;
     padding: 6px 12px;
     padding: 6px 12px;
@@ -1023,6 +1142,7 @@ export default {
     cursor: default;
     cursor: default;
     position: relative;
     position: relative;
     overflow: hidden;
     overflow: hidden;
+    word-break: break-word;
 }
 }
 
 
 .tag-chip::before {
 .tag-chip::before {
@@ -1071,7 +1191,9 @@ export default {
 .tag-name {
 .tag-name {
     font-weight: 600;
     font-weight: 600;
     color: #475569;
     color: #475569;
-    white-space: nowrap;
+    white-space: normal;
+    word-break: break-word;
+    line-height: 1.3;
 }
 }
 
 
 .tag-separator {
 .tag-separator {
@@ -1111,7 +1233,11 @@ export default {
     justify-content: center;
     justify-content: center;
     padding: 16px 0;
     padding: 16px 0;
     border-top: 1px solid #f1f5f9;
     border-top: 1px solid #f1f5f9;
-    margin-top: 8px;
+    margin-top: auto;
+    background: white;
+    position: sticky;
+    bottom: 0;
+    z-index: 10;
 }
 }
 
 
 .btn-expand-tags,
 .btn-expand-tags,
@@ -1417,11 +1543,34 @@ export default {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     gap: 6px;
     gap: 6px;
+    max-height: 400px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+    padding-top: 0;
+}
+
+.profile-grid::-webkit-scrollbar {
+    width: 6px;
+}
+
+.profile-grid::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.profile-grid::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.profile-grid::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
 }
 }
 
 
 .profile-item {
 .profile-item {
     display: grid;
     display: grid;
-    grid-template-columns: 110px 1fr;
+    grid-template-columns: 35% 65%;
     align-items: flex-start;
     align-items: flex-start;
     gap: 8px;
     gap: 8px;
     padding: 6px 12px;
     padding: 6px 12px;
@@ -1429,6 +1578,7 @@ export default {
     transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
     transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
     background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
     background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
     border: 1px solid #e2e8f0;
     border: 1px solid #e2e8f0;
+    word-break: break-word;
 }
 }
 
 
 .profile-item:hover {
 .profile-item:hover {
@@ -1441,6 +1591,10 @@ export default {
 .profile-item-main {
 .profile-item-main {
     background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
     background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
     border-color: #bfdbfe;
     border-color: #bfdbfe;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
 }
 }
 
 
 .profile-item-main:hover {
 .profile-item-main:hover {
@@ -1448,18 +1602,19 @@ export default {
 }
 }
 
 
 .profile-item-full {
 .profile-item-full {
-    grid-template-columns: 110px 1fr;
+    grid-template-columns: 35% 65%;
 }
 }
 
 
 .profile-item .label {
 .profile-item .label {
     font-size: 13px;
     font-size: 13px;
     color: #64748b;
     color: #64748b;
     font-weight: 600;
     font-weight: 600;
-    white-space: nowrap;
+    white-space: normal;
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
     gap: 6px;
     gap: 6px;
-    line-height: 1.3;
+    line-height: 1.4;
+    min-width: 0;
 }
 }
 
 
 .profile-item .label i {
 .profile-item .label i {
@@ -1467,6 +1622,7 @@ export default {
     font-size: 12px;
     font-size: 12px;
     width: 14px;
     width: 14px;
     text-align: center;
     text-align: center;
+    flex-shrink: 0;
 }
 }
 
 
 .profile-item .value {
 .profile-item .value {
@@ -1523,6 +1679,28 @@ export default {
     list-style: none;
     list-style: none;
     padding: 0;
     padding: 0;
     margin: 0;
     margin: 0;
+    max-height: 180px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.focus-list::-webkit-scrollbar {
+    width: 6px;
+}
+
+.focus-list::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.focus-list::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.focus-list::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
 }
 }
 
 
 .focus-item {
 .focus-item {
@@ -1568,6 +1746,16 @@ export default {
     margin-bottom: 12px;
     margin-bottom: 12px;
 }
 }
 
 
+.no-data-tip {
+    font-size: 13px;
+    color: #94a3b8;
+    font-style: italic;
+    padding: 4px 12px;
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border-radius: 6px;
+    border: 1px dashed #cbd5e1;
+}
+
 .intention-label {
 .intention-label {
     font-size: 14px;
     font-size: 14px;
     font-weight: 600;
     font-weight: 600;

+ 13 - 9
src/views/crm/customer/index.vue

@@ -132,10 +132,11 @@
           <el-form-item label="流失风险" prop="attritionLevel">
           <el-form-item label="流失风险" prop="attritionLevel">
                 <el-select style="width:220px" v-model="queryParams.attritionLevel" placeholder="请选择流失风险等级" clearable size="small">
                 <el-select style="width:220px" v-model="queryParams.attritionLevel" placeholder="请选择流失风险等级" clearable size="small">
                     <el-option label="全部" value="" />
                     <el-option label="全部" value="" />
-                    <el-option label="无风险" :value="0" />
-                    <el-option label="低风险" :value="1" />
-                    <el-option label="中风险" :value="2" />
-                    <el-option label="高风险" :value="3" />
+                    <el-option label="未知" :value="0" />
+                    <el-option label="无风险" :value="1" />
+                    <el-option label="低风险" :value="2" />
+                    <el-option label="中风险" :value="3" />
+                    <el-option label="高风险" :value="4" />
                 </el-select>
                 </el-select>
           </el-form-item>
           </el-form-item>
           <el-form-item label="意向度">
           <el-form-item label="意向度">
@@ -255,10 +256,12 @@
           <el-table-column label="标签" align="center" prop="tags" />
           <el-table-column label="标签" align="center" prop="tags" />
           <el-table-column label="流失风险" align="center" prop="attritionLevel">
           <el-table-column label="流失风险" align="center" prop="attritionLevel">
             <template slot-scope="scope">
             <template slot-scope="scope">
-              <el-tag v-if="scope.row.attritionLevel === 0" type="success">无风险</el-tag>
-              <el-tag v-else-if="scope.row.attritionLevel === 1" type="info">低风险</el-tag>
-              <el-tag v-else-if="scope.row.attritionLevel === 2" type="warning">中风险</el-tag>
-              <el-tag v-else-if="scope.row.attritionLevel === 3" type="danger">高风险</el-tag>
+              <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
+              <el-tag v-if="scope.row.attritionLevel === 0" type="info">未知</el-tag>
+              <el-tag v-else-if="scope.row.attritionLevel === 1" type="success">无风险</el-tag>
+              <el-tag v-else-if="scope.row.attritionLevel === 2" type="info">低风险</el-tag>
+              <el-tag v-else-if="scope.row.attritionLevel === 3" type="warning">中风险</el-tag>
+              <el-tag v-else-if="scope.row.attritionLevel === 4" type="danger">高风险</el-tag>
             </template>
             </template>
           </el-table-column>
           </el-table-column>
           <el-table-column label="意向度" align="center" prop="intentionDegree">
           <el-table-column label="意向度" align="center" prop="intentionDegree">
@@ -294,11 +297,12 @@
                 v-hasPermi="['crm:customer:query']"
                 v-hasPermi="['crm:customer:query']"
               >查看</el-button>
               >查看</el-button>
               <el-button
               <el-button
+                v-if="scope.row.attritionLevel !== null && scope.row.attritionLevel !== undefined"
                 size="mini"
                 size="mini"
                 type="text"
                 type="text"
                 @click="toDetailPage(scope.row)"
                 @click="toDetailPage(scope.row)"
                 v-hasPermi="['crm:analyze:list']"
                 v-hasPermi="['crm:analyze:list']"
-              >AI分析</el-button>
+              >AI 分析</el-button>
               <el-button
               <el-button
                 v-if="scope.row.isReceive==1"
                 v-if="scope.row.isReceive==1"
                 size="mini"
                 size="mini"

+ 1545 - 0
src/views/crm/customerAiChat/index.vue

@@ -0,0 +1,1545 @@
+<template>
+    <div class="ai-chat-container">
+        <!-- 聊天主界面 -->
+        <div class="chat-main">
+            <!-- 左侧边栏 - 会话列表 -->
+            <div class="sidebar">
+                <div class="sidebar-header">
+                    <button class="new-chat-btn" @click="createNewChatDirect">
+                        <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
+                            <path d="M8 3.5V12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+                            <path d="M3.5 8H12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+                        </svg>
+                        <span>新建对话</span>
+                    </button>
+                </div>
+
+                <div class="chat-list">
+                    <div
+                        v-for="(chat, index) in chatList"
+                        :key="chat.id"
+                        :class="['chat-item', { active: currentChatId === chat.id }]"
+                        @click="selectChat(chat)"
+                    >
+                        <div class="chat-item-content">
+                            <el-input
+                                v-if="editingChatIndex === index"
+                                v-model="chat.editingTitle"
+                                size="mini"
+                                @blur="confirmEditTitle(index)"
+                                @keydown.native.enter.prevent="confirmEditTitle(index)"
+                                @click.stop
+                                ref="editInput"
+                                class="chat-title-input"
+                            >
+                            </el-input>
+                            <div 
+                                v-else 
+                                class="chat-item-title"
+                                @dblclick.stop="startEditTitle(index, chat)"
+                                title="双击编辑标题"
+                            >{{ chat.title }}</div>
+                            <div class="chat-item-time">{{ formatTime(chat.time) }}</div>
+                        </div>
+                        <div class="chat-item-actions">
+                            <i
+                                class="el-icon-edit chat-item-edit"
+                                @click.stop="startEditTitle(index, chat)"
+                                title="修改标题"
+                            ></i>
+                            <i
+                                v-if="index !== 0"
+                                class="el-icon-top chat-item-pin"
+                                @click.stop="pinChat(index)"
+                                title="置顶会话"
+                            ></i>
+                            <i
+                                v-if="chatList.length > 1"
+                                class="el-icon-close chat-item-close"
+                                @click.stop="closeChat(index)"
+                                title="关闭会话"
+                            ></i>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 中间聊天区域 -->
+            <div class="chat-content">
+                <!-- 左侧客户画像面板 -->
+                <div v-if="linkedCustomer" class="customer-portrait-panel">
+                    <!-- 客户姓名固定顶部 -->
+                    <div class="profile-item profile-item-main profile-item-sticky">
+                        <span class="label"><i class="fas fa-user-circle"></i> 客户姓名:</span>
+                        <span class="value highlight">{{ linkedCustomer.name }}</span>
+                    </div>
+                    <!-- 其他画像数据可滚动 -->
+                    <div class="profile-scroll-content">
+                        <template v-for="(value, key) in customerPortraitData">
+                            <div
+                                v-if="key !== '需求'"
+                                :key="key"
+                                class="profile-item"
+                            >
+                                    <span class="label">
+                                        <i :class="getIconClass(key)"></i> {{ key }}:
+                                    </span>
+                                <span class="value">{{ value }}</span>
+                            </div>
+                        </template>
+                        <!-- 需求单独显示,占满整行 -->
+                        <div
+                            v-if="customerPortraitData['需求']"
+                            key="需求"
+                            class="profile-item profile-item-full"
+                        >
+                                <span class="label">
+                                    <i class="fas fa-bullseye"></i> 需求:
+                                </span>
+                            <span class="value long-text">{{ customerPortraitData['需求'] }}</span>
+                        </div>
+                        <!-- 当画像数据为空时显示提示 -->
+                        <div v-if="Object.keys(customerPortraitData).length === 0"
+                             class="profile-item profile-item-full">
+                            <span class="value empty-tip">暂无画像信息</span>
+                        </div>
+                    </div>
+                </div>
+                <!-- 聊天标题 -->
+                <div v-if="currentChatTitle && messages.length > 0" class="chat-title-header">
+                    <h2 class="chat-title-text">{{ currentChatTitle }}</h2>
+                </div>
+
+                <!-- 消息列表 -->
+                <div class="messages-container" ref="messagesContainer">
+                    <!-- 欢迎界面 -->
+                    <div v-if="messages.length === 0" class="welcome-container">
+                        <div class="welcome-avatar">
+                            <img src="/static/images/ai-avatar.svg" alt="AI" class="welcome-avatar-img"/>
+                        </div>
+                        <div class="welcome-text">今天有什么可以帮到你?</div>
+                    </div>
+
+                    <div v-else class="messages-list">
+                        <div
+                            v-for="(message, index) in messages"
+                            :key="index"
+                            :class="['message-row', message.type === 'user' ? 'message-user' : 'message-ai']"
+                        >
+                            <!-- AI 消息 -->
+                            <div v-if="message.type === 'ai'" class="message-wrapper">
+                                <div class="avatar avatar-ai">
+                                    <img src="/static/images/ai-avatar.svg" alt="AI" @error="handleAvatarError"/>
+                                </div>
+                                <div class="message-body">
+                                    <div class="message-name">AI 助手</div>
+                                    <div class="message-bubble message-bubble-ai">
+                                        <div class="message-content" v-html="formatMessage(message.content)"></div>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <!-- 用户消息 -->
+                            <div v-else class="message-row message-user">
+                                <div class="message-bubble message-bubble-user">
+                                    <div class="message-content" v-html="formatUserMessage(message.content)"></div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <!-- 加载中提示 -->
+                        <div v-if="isLoading" class="message-row message-ai">
+                            <div class="message-wrapper">
+                                <div class="avatar avatar-ai">
+                                    <img src="/static/images/ai-avatar.svg" alt="AI" @error="handleAvatarError"/>
+                                </div>
+                                <div class="message-body">
+                                    <div class="message-name">AI 助手</div>
+                                    <div class="message-bubble message-bubble-ai thinking-bubble">
+                                        <div class="thinking-indicator">
+                                            <span></span>
+                                            <span></span>
+                                            <span></span>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 输入区域 -->
+                <div class="input-area">
+                    <!-- 关联客户按钮 -->
+                    <div class="customer-link-bar">
+                        <el-button
+                            size="small"
+                            type="primary"
+                            plain
+                            @click="linkCustomer"
+                            class="customer-link-btn"
+                        >
+                            <i class="el-icon-user"></i>
+                            关联客户
+                        </el-button>
+                        <span v-if="linkedCustomer" class="linked-customer-info">
+              <i class="el-icon-check"></i>
+              已关联:{{ linkedCustomer.name }}
+              <el-tag size="mini" style="margin-left: 8px;">
+                意向度:{{ linkedCustomer.intentionDegree }}
+              </el-tag>
+              <el-tag
+                  size="mini"
+                  :type="getAttritionLevelTagType(linkedCustomer.attritionLevel)"
+                  style="margin-left: 8px;"
+              >
+                {{ getAttritionLevelText(linkedCustomer.attritionLevel) }}
+              </el-tag>
+              <i class="el-icon-close unlink-icon" @click="unlinkCustomer" title="取消关联"></i>
+            </span>
+                    </div>
+                    <div class="input-container">
+                        <div class="input-box">
+                            <el-input
+                                v-model="inputMessage"
+                                type="textarea"
+                                :rows="3"
+                                placeholder="输入消息... (Shift+Enter 换行,Enter 发送)"
+                                @keydown.native.enter.exact.prevent="sendMessage"
+                                resize="none"
+                                class="message-input"
+                            >
+                            </el-input>
+                            <div class="send-btn-wrapper" @click="sendMessage" title="点击发送">
+                                <i class="el-icon-top"></i>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="input-tip">
+                        AI 生成内容仅供参考,请谨慎判断
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 客户选择对话框 -->
+        <el-dialog
+            v-if="customerDialogVisible"
+            :visible.sync="customerDialogVisible"
+            title="选择客户"
+            width="900px"
+            :close-on-click-modal="false"
+            @close="cancelSelectCustomer"
+        >
+            <div class="customer-dialog-content">
+                <!-- 筛选区域 -->
+                <div class="customer-filter">
+                    <el-form :inline="true" size="small">
+                        <el-form-item label="客户名称">
+                            <el-input
+                                v-model="customerFilter.customerName"
+                                placeholder="请输入客户名称"
+                                clearable
+                                @keyup.enter.native="handleCustomerFilter"
+                            />
+                        </el-form-item>
+                        <el-form-item label="流失风险等级">
+                            <el-select
+                                v-model="customerFilter.attritionLevel"
+                                placeholder="请选择风险等级"
+                                clearable
+                            >
+                                <el-option label="未知" :value="0" />
+                                <el-option label="无风险" :value="1" />
+                                <el-option label="低风险" :value="2" />
+                                <el-option label="中风险" :value="3" />
+                                <el-option label="高风险" :value="4" />
+                            </el-select>
+                        </el-form-item>
+                        <el-form-item>
+                            <el-button type="primary" @click="handleCustomerFilter">
+                                <i class="el-icon-search"></i> 搜索
+                            </el-button>
+                            <el-button @click="resetCustomerFilter">
+                                <i class="el-icon-refresh"></i> 重置
+                            </el-button>
+                        </el-form-item>
+                    </el-form>
+                </div>
+
+                <el-table
+                    :data="customerList"
+                    v-loading="customerLoading"
+                    highlight-current-row
+                    @current-change="handleCurrentChange"
+                    border
+                >
+                    <el-table-column type="radio" width="50" align="center"></el-table-column>
+                    <el-table-column prop="customerId" label="客户 ID" width="120" align="center"></el-table-column>
+                    <el-table-column prop="customerName" label="客户名称" min-width="200"
+                                     align="center"></el-table-column>
+                    <el-table-column label="流失风险等级" width="150" align="center">
+                        <template slot-scope="scope">
+                            <el-tag :type="getAttritionLevelTagType(scope.row.attritionLevel)" size="small">
+                                {{ getAttritionLevelText(scope.row.attritionLevel) }}
+                            </el-tag>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="intentionDegree" label="意向度" width="150" align="center">
+                        <template slot-scope="scope">
+                            <el-tag type="success" size="small" effect="plain">
+                                {{ scope.row.intentionDegree }}
+                            </el-tag>
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="操作" width="100" align="center" fixed="right">
+                        <template slot-scope="scope">
+                            <el-button
+                                type="primary"
+                                size="small"
+                                @click="selectCustomerByClick(scope.row)"
+                            >
+                                选择
+                            </el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+
+                <div class="customer-pagination">
+                    <el-pagination
+                        @current-change="handleCustomerPageChange"
+                        :current-page="customerPagination.pageNum"
+                        :page-size="customerPagination.pageSize"
+                        :total="customerPagination.total"
+                        layout="total, prev, pager, next, jumper"
+                        background
+                        :page-sizes="[5, 10, 20, 50]"
+                    >
+                    </el-pagination>
+                </div>
+            </div>
+
+            <span slot="footer" class="dialog-footer">
+        <el-button @click="cancelSelectCustomer">关 闭</el-button>
+      </span>
+        </el-dialog>
+
+
+    </div>
+</template>
+
+<script>
+import {listAllAnalyze} from "../../../api/crm/customerAnalyze";
+
+export default {
+    name: 'CustomerAiChat',
+    data() {
+        return {
+            // 聊天列表
+            chatList: [],
+            currentChatId: null,
+            // 当前聊天消息
+            messages: [],
+            inputMessage: '',
+            isLoading: false,
+            currentChatTitle: '', // 当前聊天标题
+            linkedCustomer: null, // 已关联的客户
+            customerLinkMap: {}, // 每个会话关联的客户映射 {chatId: customer}
+            hasSentFirstMessage: false, // 标记是否已发送第一条消息
+            manuallyEditedTitles: {}, // 记录手动编辑过标题的会话 ID
+            // 客户选择对话框相关
+            customerDialogVisible: false,
+            customerList: [],
+            customerPagination: {
+                pageNum: 1,
+                pageSize: 5,
+                total: 0
+            },
+            customerLoading: false,
+            selectedCustomerId: null, // 当前选中的客户 ID
+            // 客户筛选条件
+            customerFilter: {
+                customerName: '',
+                attritionLevel: null
+            },
+            // 会话标题编辑相关
+            editingChatIndex: -1,
+            isEditingChat: false
+        }
+    },
+    created() {
+        // 初始化时创建第一个默认会话
+        this.createDefaultChat()
+    },
+    computed: {
+        // 客户画像数据(从最新的沟通记录中获取)
+        customerPortraitData() {
+            if (!this.linkedCustomer || !this.linkedCustomer.customerPortraitJson) {
+                return {};
+            }
+            try {
+                // 如果是字符串,解析为 JSON 对象
+                if (typeof this.linkedCustomer.customerPortraitJson === 'string') {
+                    return JSON.parse(this.linkedCustomer.customerPortraitJson);
+                }
+                // 如果已经是对象,直接返回
+                return this.linkedCustomer.customerPortraitJson;
+            } catch (error) {
+                console.error('解析客户画像 JSON 失败:', error);
+            }
+            return {};
+        }
+    },
+    methods: {
+        // 创建默认会话
+        createDefaultChat() {
+            const newTitle = '新会话 1'
+            const defaultChat = {
+                id: Date.now(),
+                title: newTitle,
+                time: Date.now(),
+                messages: []
+            }
+            this.chatList.push(defaultChat)
+            this.selectChat(defaultChat)
+            this.currentChatTitle = newTitle
+            this.hasSentFirstMessage = false
+        },
+
+        // 直接创建新会话(按序号)
+        createNewChatDirect() {
+            const newTitle = '新会话 ' + (this.chatList.length + 1)
+            const newChat = {
+                id: Date.now(),
+                title: newTitle,
+                time: Date.now(),
+                messages: []
+            }
+            this.chatList.unshift(newChat)
+            this.selectChat(newChat)
+            // 更新显示的标题
+            this.currentChatTitle = newTitle
+
+            // 清空当前消息,显示空状态
+            this.messages = []
+            // 新建对话时重置关联客户
+            this.linkedCustomer = null
+            this.hasSentFirstMessage = false
+
+            // 滚动到顶部以显示欢迎界面
+            this.$nextTick(() => {
+                const container = this.$refs.messagesContainer
+                if (container) {
+                    container.scrollTop = 0
+                }
+            })
+        },
+
+        // 开始编辑标题
+        startEditTitle(index, chat) {
+            this.$set(chat, 'editingTitle', chat.title)
+            this.editingChatIndex = index
+            this.isEditingChat = true
+            this.$nextTick(() => {
+                if (this.$refs.editInput && this.$refs.editInput[index]) {
+                    this.$refs.editInput[index].focus()
+                    this.$refs.editInput[index].select()
+                }
+            })
+        },
+
+        // 确认编辑标题
+        confirmEditTitle(index) {
+            const chat = this.chatList[index]
+            if (!chat) return
+
+            const newTitle = (chat.editingTitle || '').trim()
+            if (!newTitle) {
+                this.$message.warning('标题不能为空')
+                chat.editingTitle = chat.title
+                return
+            }
+
+            chat.title = newTitle
+            if (chat.id === this.currentChatId) {
+                this.currentChatTitle = newTitle
+            }
+
+            // 标记该会话标题已被手动编辑
+            this.manuallyEditedTitles[chat.id] = true
+
+            this.editingChatIndex = -1
+            this.isEditingChat = false
+            delete chat.editingTitle
+        },
+
+        // 选择聊天
+        selectChat(chat) {
+            // 保存当前会话的关联客户
+            if (this.currentChatId) {
+                if (this.linkedCustomer) {
+                    this.customerLinkMap[this.currentChatId] = this.linkedCustomer
+                } else {
+                    delete this.customerLinkMap[this.currentChatId]
+                }
+            }
+
+            this.currentChatId = chat.id
+            this.messages = chat.messages || []
+            this.currentChatTitle = chat.title || ''
+
+            // 恢复目标会话的关联客户
+            this.linkedCustomer = this.customerLinkMap[chat.id] || null
+
+            this.$nextTick(() => {
+                this.scrollToBottom()
+            })
+        },
+
+        // 发送消息
+        sendMessage() {
+            if (!this.inputMessage.trim() || this.isLoading) return
+
+            // 如果没有当前会话,自动创建新对话
+            if (!this.currentChatId || this.chatList.length === 0) {
+                this.createNewChat()
+            }
+
+            const userMessage = {
+                type: 'user',
+                content: this.inputMessage.trim(),
+                time: Date.now()
+            }
+
+            this.messages.push(userMessage)
+            const userInput = this.inputMessage
+            this.inputMessage = ''
+            this.isLoading = true
+
+            // 如果是第一条消息,更新会话标题(仅当标题未被手动编辑过时)
+            if (!this.hasSentFirstMessage) {
+                const currentChat = this.chatList.find(c => c.id === this.currentChatId)
+                if (currentChat) {
+                    // 检查标题是否被手动编辑过
+                    if (!this.manuallyEditedTitles[currentChat.id]) {
+                        const newTitle = userInput.substring(0, 50) + (userInput.length > 50 ? '...' : '')
+                        currentChat.title = newTitle
+                        this.currentChatTitle = newTitle
+                    }
+                    this.hasSentFirstMessage = true
+                }
+            }
+
+            this.$nextTick(() => {
+                this.scrollToBottom()
+            })
+
+            // 模拟 AI 回复(实际项目中替换为 API 调用)
+            setTimeout(() => {
+                this.getAIResponse(userInput)
+            }, 1000)
+        },
+
+        // 获取 AI 回复
+        getAIResponse(userInput) {
+            // TODO: 这里调用实际的 AI 接口
+            const aiMessage = {
+                type: 'ai',
+                content: '您好!我已经收到您的消息:"' + userInput + '"。\n\n这是一个演示回复,实际项目中请调用后端 AI 接口获取真实响应。',
+                time: Date.now()
+            }
+
+            this.messages.push(aiMessage)
+            this.isLoading = false
+
+            // 更新当前聊天的消息列表
+            const currentChat = this.chatList.find(c => c.id === this.currentChatId)
+            if (currentChat) {
+                currentChat.messages = this.messages
+                // 将当前对话移动到最前面
+                const index = this.chatList.indexOf(currentChat)
+                if (index > 0) {
+                    this.chatList.splice(index, 1)
+                    this.chatList.unshift(currentChat)
+                }
+            }
+
+            this.$nextTick(() => {
+                this.scrollToBottom()
+            })
+        },
+
+        // 滚动到底部
+        scrollToBottom() {
+            const container = this.$refs.messagesContainer
+            if (container) {
+                this.$nextTick(() => {
+                    container.scrollTop = container.scrollHeight
+                })
+            }
+        },
+
+        // 格式化时间
+        formatTime(timestamp) {
+            const date = new Date(timestamp)
+            const now = new Date()
+            const diff = now.getTime() - date.getTime()
+
+            // 今天
+            if (diff < 86400000 && date.getDate() === now.getDate()) {
+                const hours = date.getHours().toString().padStart(2, '0')
+                const minutes = date.getMinutes().toString().padStart(2, '0')
+                return `今天 ${hours}:${minutes}`
+            }
+
+            // 昨天
+            const yesterday = new Date(now)
+            yesterday.setDate(yesterday.getDate() - 1)
+            if (date.getDate() === yesterday.getDate() &&
+                date.getMonth() === yesterday.getMonth() &&
+                date.getFullYear() === yesterday.getFullYear()) {
+                return '昨天'
+            }
+
+            // 更早
+            const month = (date.getMonth() + 1).toString().padStart(2, '0')
+            const day = date.getDate().toString().padStart(2, '0')
+            return `${month}-${day}`
+        },
+
+        // 格式化消息内容(支持简单的 markdown)
+        formatMessage(content) {
+            if (!content) return ''
+
+            // 换行转<br>
+            let formatted = content.replace(/\n/g, '<br>')
+
+            // **bold** 转粗体
+            formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
+
+            // *italic* 转斜体
+            formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>')
+
+            return formatted
+        },
+
+        // 格式化用户消息(支持换行)
+        formatUserMessage(content) {
+            if (!content) return ''
+
+            // 换行转<br>
+            return content.replace(/\n/g, '<br>')
+        },
+
+        // 关闭会话
+        closeChat(index) {
+            const chat = this.chatList[index]
+
+            // 如果关闭的是当前选中的会话
+            if (this.currentChatId === chat.id) {
+                // 尝试切换到前一个会话,如果没有则切换到后一个
+                if (index > 0) {
+                    this.selectChat(this.chatList[index - 1])
+                } else if (this.chatList.length > 1) {
+                    this.selectChat(this.chatList[1])
+                } else {
+                    this.currentChatId = null
+                    this.messages = []
+                    this.linkedCustomer = null
+                    this.customerLinkMap = {}
+                }
+            }
+
+            // 删除会话的关联客户映射
+            delete this.customerLinkMap[chat.id]
+
+            // 删除会话
+            this.chatList.splice(index, 1)
+        },
+
+        // 头像加载失败处理
+        handleAvatarError(event) {
+            event.target.style.display = 'none'
+            event.target.parentElement.innerHTML = '<i class="el-icon-user" style="font-size: 20px; color: white;"></i>'
+        },
+
+        // 关联客户
+        linkCustomer() {
+            this.customerDialogVisible = true
+            // 重置到第一页
+            this.customerPagination.pageNum = 1
+            // 重置筛选条件
+            this.resetCustomerFilter()
+            this.getCustomerList()
+        },
+
+        // 获取客户列表
+        getCustomerList() {
+            this.customerLoading = true
+            const params = {
+                pageNum: this.customerPagination.pageNum,
+                pageSize: this.customerPagination.pageSize
+            }
+            // 添加筛选条件
+            if (this.customerFilter.customerName) {
+                params.customerName = this.customerFilter.customerName
+            }
+            if (this.customerFilter.attritionLevel !== null && this.customerFilter.attritionLevel !== undefined) {
+                params.attritionLevel = this.customerFilter.attritionLevel
+            }
+            listAllAnalyze(params).then(response => {
+                this.customerList = response.rows || []
+                this.customerPagination.total = response.total || 0
+                this.customerLoading = false
+            }).catch(() => {
+                this.customerLoading = false
+            })
+        },
+
+        // 处理客户筛选
+        handleCustomerFilter() {
+            this.customerPagination.pageNum = 1
+            this.getCustomerList()
+        },
+
+        // 重置客户筛选
+        resetCustomerFilter() {
+            this.customerFilter.customerName = ''
+            this.customerFilter.attritionLevel = null
+            this.customerPagination.pageNum = 1
+            this.getCustomerList()
+        },
+
+        // 处理客户分页
+        handleCustomerPageChange(page) {
+            this.customerPagination.pageNum = page
+            this.getCustomerList()
+        },
+
+        // 点击选择按钮选择客户
+        selectCustomerByClick(customer) {
+            if (customer) {
+                this.linkedCustomer = {
+                    id: customer.customerId,
+                    name: customer.customerName,
+                    attritionLevel: customer.attritionLevel,
+                    intentionDegree: customer.intentionDegree,
+                    customerPortraitJson: customer.customerPortraitJson
+                }
+                this.customerDialogVisible = false
+                this.$message.success('已成功关联客户-' + customer.customerName)
+            }
+        },
+        // 获取图标类名
+        getIconClass(key) {
+            const iconMap = {
+                '性别': 'fas fa-venus-mars',
+                '年龄': 'fas fa-calendar-alt',
+                '职业': 'fas fa-briefcase',
+                '地区': 'fas fa-map-marker-alt',
+                '来源': 'fas fa-source',
+                '意向度': 'fas fa-heart',
+                '备注': 'fas fa-sticky-note'
+            };
+            return iconMap[key] || 'fas fa-info-circle';
+        },
+
+        // 取消选择客户
+        cancelSelectCustomer() {
+            this.customerDialogVisible = false
+            this.selectedCustomerId = null
+        },
+
+        // 获取流失风险等级文本
+        getAttritionLevelText(level) {
+            const levelMap = {
+                0: '未知',
+                1: '无风险',
+                2: '低风险',
+                3: '中风险',
+                4: '高风险'
+            }
+            return levelMap[level] !== undefined ? levelMap[level] : '未知'
+        },
+
+        // 获取流失风险等级标签类型
+        getAttritionLevelTagType(level) {
+            const typeMap = {
+                0: 'info', // 未知
+                1: 'success', // 无风险
+                2: '', // 低风险
+                3: 'warning', // 中风险
+                4: 'danger' // 高风险
+            }
+            return typeMap[level] !== undefined ? typeMap[level] : 'info'
+        },
+
+        // 处理表格行选择(单选)
+        handleCurrentChange(val) {
+            if (val) {
+                this.selectedCustomerId = val.customerId
+            }
+        },
+
+        // 取消关联客户
+        unlinkCustomer() {
+            this.linkedCustomer = null
+            this.$message.success('已取消关联客户')
+        },
+
+        // 置顶会话
+        pinChat(index) {
+            if (index === 0) return // 第一个不需要置顶
+
+            const chat = this.chatList[index]
+            // 从当前位置移除
+            this.chatList.splice(index, 1)
+            // 添加到第一个位置
+            this.chatList.unshift(chat)
+            this.$message.success('会话已置顶')
+        }
+    },
+}
+</script>
+
+<style lang="scss" scoped>
+.ai-chat-container {
+    height: 100%;
+    width: 100%;
+    display: flex;
+    background: #f9fafb;
+}
+
+.chat-main {
+    display: flex;
+    width: 100%;
+    height: 100%;
+}
+
+/* 左侧边栏 */
+.sidebar {
+    width: 260px;
+    min-width: 260px;
+    background: #f9f9fb;
+    border-right: 1px solid #e5e5ea;
+    display: flex;
+    flex-direction: column;
+}
+
+.sidebar-header {
+    padding: 16px;
+    border-bottom: 1px solid #e5e5ea;
+}
+
+.new-chat-btn {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    padding: 10px 16px;
+    background: white;
+    color: #1c1c1c;
+    border: 1px solid #e5e5ea;
+    border-radius: 8px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: all 0.2s ease;
+
+    &:hover {
+        background: #f5f5f5;
+        border-color: #d4d4d4;
+    }
+}
+
+.chat-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 8px;
+}
+
+.chat-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 10px 12px;
+    margin-bottom: 4px;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    position: relative;
+
+    &:hover {
+        background: #f3f4f6;
+
+        .chat-item-actions {
+            opacity: 1;
+        }
+    }
+
+    &.active {
+        background: #e8f4ff;
+    }
+}
+
+.chat-item-actions {
+    display: flex;
+    align-items: center;
+    gap: 2px;
+    opacity: 0;
+    transition: opacity 0.2s ease;
+    position: absolute;
+    right: 8px;
+    top: 50%;
+    transform: translateY(-50%);
+    z-index: 10;
+}
+
+.chat-item-close {
+    font-size: 14px;
+    color: #9ca3af;
+    cursor: pointer;
+    padding: 6px;
+    border-radius: 6px;
+    transition: all 0.2s ease;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    &:hover {
+        background: #fee2e2;
+        color: #ef4444;
+    }
+}
+
+.chat-item-pin {
+    font-size: 14px;
+    color: #9ca3af;
+    cursor: pointer;
+    padding: 6px;
+    border-radius: 6px;
+    transition: all 0.2s ease;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    &:hover {
+        background: #fef3c7;
+        color: #f59e0b;
+    }
+}
+
+.chat-item-edit {
+    font-size: 14px;
+    color: #9ca3af;
+    cursor: pointer;
+    padding: 6px;
+    border-radius: 6px;
+    transition: all 0.2s ease;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    &:hover {
+        background: #dbeafe;
+        color: #3b82f6;
+    }
+}
+
+.chat-item-icon {
+    width: 0;
+    min-width: 0;
+}
+
+.chat-item-content {
+    flex: 1;
+    overflow: hidden;
+}
+
+.chat-item-title {
+    font-size: 14px;
+    color: #1c1c1c;
+    margin-bottom: 4px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    cursor: pointer;
+
+    &:hover {
+        background: rgba(0, 0, 0, 0.05);
+        border-radius: 4px;
+        padding: 2px 4px;
+        margin: -2px -4px;
+    }
+}
+
+.chat-title-input {
+    ::v-deep .el-input__inner {
+        padding: 4px 8px;
+        font-size: 14px;
+        height: 28px;
+    }
+}
+
+.chat-item-time {
+    font-size: 12px;
+    color: #999;
+}
+
+/* 聊天内容区域 */
+.chat-content {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    background: white;
+    height: calc(100vh - 90px);
+    overflow: hidden;
+    position: relative;
+}
+
+// 客户画像面板
+.customer-portrait-panel {
+    position: absolute;
+    top: 50%;
+    left: 12px;
+    transform: translateY(-50%);
+    width: 320px;
+    background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+    border-radius: 16px;
+    padding: 16px;
+    box-shadow: 0 0 0 1px #d1d5db, 0 8px 32px rgba(0, 0, 0, 0.15);
+    z-index: 100;
+    max-height: calc(100vh - 320px);
+    display: flex;
+    flex-direction: column;
+}
+
+@keyframes slideInLeft {
+    from {
+        opacity: 0;
+        transform: translateX(-30px);
+    }
+    to {
+        opacity: 1;
+        transform: translateX(0);
+    }
+}
+
+.profile-grid {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.profile-scroll-content {
+    flex: 1;
+    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+    min-height: 0;
+    margin-top: 8px;
+    padding-top: 0;
+}
+
+.profile-scroll-content::-webkit-scrollbar {
+    width: 6px;
+}
+
+.profile-scroll-content::-webkit-scrollbar-track {
+    background: transparent;
+}
+
+.profile-scroll-content::-webkit-scrollbar-thumb {
+    background: #bae6fd;
+    border-radius: 3px;
+
+    &:hover {
+        background: #7dd3fc;
+    }
+}
+
+.profile-item-sticky {
+    position: sticky;
+    top: 0;
+    z-index: 1;
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+    box-shadow: 0 2px 8px rgba(186, 230, 253, 0.3);
+}
+
+.profile-item-sticky::after {
+    content: '';
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 2px;
+    background: #d1d5db;
+}
+
+.profile-item {
+    display: grid;
+    grid-template-columns: 85px 1fr;
+    align-items: flex-start;
+    gap: 5px;
+    padding: 5px 8px;
+    border-radius: 10px;
+    background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.95) 100%);
+}
+
+.profile-item-main {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+
+    &:hover {
+        background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+    }
+}
+
+.profile-item-full {
+    grid-template-columns: 85px 1fr;
+}
+
+.label {
+    font-size: 13px;
+    color: #0f172a;
+    font-weight: 600;
+    white-space: normal;
+    word-break: break-all;
+    max-width: 85px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    line-height: 1.4;
+
+    i {
+        color: #0ea5e9;
+        font-size: 14px;
+        width: 14px;
+        text-align: center;
+        flex-shrink: 0;
+    }
+}
+
+.value {
+    font-size: 14px;
+    color: #0369a1;
+    font-weight: 600;
+    word-break: break-word;
+    line-height: 1.5;
+    min-width: 0;
+
+    &.highlight {
+        color: #dc2626;
+        font-weight: 600;
+        font-size: 15px;
+    }
+
+    &.long-text {
+        color: #334155;
+        font-weight: 400;
+    }
+
+    &.empty-tip {
+        color: #94a3b8;
+        font-style: italic;
+        font-size: 14px;
+    }
+}
+
+// 聊天标题头部
+.chat-title-header {
+    position: sticky;
+    top: 0;
+    left: 0;
+    right: 0;
+    background: white;
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
+    z-index: 10;
+    text-align: center;
+}
+
+.chat-title-text {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1c1c1c;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    max-width: 100%;
+    letter-spacing: -0.02em;
+}
+
+.messages-container {
+    flex: 1;
+    overflow-y: auto;
+    padding: 24px;
+    scroll-behavior: smooth;
+    max-width: 768px;
+    margin: 0 auto;
+    width: 100%;
+    position: relative;
+}
+
+// 欢迎界面
+.welcome-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    min-height: 400px;
+    animation: fadeIn 0.5s ease;
+}
+
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.welcome-avatar {
+    width: 120px;
+    height: 120px;
+    margin-bottom: 24px;
+    animation: float 3s ease-in-out infinite;
+}
+
+@keyframes float {
+    0%, 100% {
+        transform: translateY(0);
+    }
+    50% {
+        transform: translateY(-10px);
+    }
+}
+
+.welcome-avatar-img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.welcome-text {
+    font-size: 28px;
+    font-weight: 600;
+    color: #1c1c1c;
+    text-align: center;
+    letter-spacing: 0.5px;
+}
+
+
+.message-row {
+    display: flex;
+    margin-bottom: 24px;
+
+    &.message-user {
+        justify-content: flex-end;
+    }
+
+    &.message-ai {
+        justify-content: flex-start;
+    }
+}
+
+.message-wrapper {
+    display: flex;
+    gap: 12px;
+    max-width: 80%;
+
+    &.message-wrapper-user {
+        flex-direction: row-reverse;
+    }
+}
+
+.avatar {
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    overflow: hidden;
+    flex-shrink: 0;
+    background: #f5f5f5;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+    }
+
+    &.avatar-ai {
+        background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
+    }
+
+    &.avatar-user {
+        background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    }
+}
+
+.message-body {
+    display: flex;
+    flex-direction: column;
+
+    &.message-body-user {
+        align-items: flex-end;
+    }
+}
+
+.message-name {
+    font-size: 13px;
+    color: #999;
+    margin-bottom: 8px;
+
+    &.message-name-user {
+        text-align: right;
+    }
+}
+
+.message-bubble {
+    padding: 12px 16px;
+    border-radius: 12px;
+    font-size: 15px;
+    line-height: 1.6;
+    word-break: break-word;
+
+    &.message-bubble-ai {
+        background: transparent;
+        border: none;
+        padding-left: 0;
+        box-shadow: none;
+    }
+
+    &.message-bubble-user {
+        background: #f5f5f5;
+        color: #1c1c1c;
+        box-shadow: none;
+    }
+}
+
+.thinking-bubble {
+    display: flex;
+    align-items: center;
+    padding: 16px 20px;
+}
+
+.thinking-indicator {
+    display: flex;
+    gap: 4px;
+
+    span {
+        width: 6px;
+        height: 6px;
+        border-radius: 50%;
+        background: #999;
+        animation: bounce 1.4s infinite ease-in-out both;
+
+        &:nth-child(1) {
+            animation-delay: -0.32s;
+        }
+
+        &:nth-child(2) {
+            animation-delay: -0.16s;
+        }
+    }
+}
+
+@keyframes bounce {
+    0%, 80%, 100% {
+        transform: scale(0);
+    }
+    40% {
+        transform: scale(1);
+    }
+}
+
+.input-area {
+    background: white;
+    border-top: 1px solid #e5e5ea;
+    padding: 16px 24px;
+    flex-shrink: 0;
+}
+
+// 关联客户按钮栏
+.customer-link-bar {
+    max-width: 768px;
+    margin: 0 auto 12px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+
+.customer-link-btn {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+}
+
+.linked-customer-info {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 6px 12px;
+    background: #f0f9ff;
+    border: 1px solid #bae6fd;
+    border-radius: 6px;
+    font-size: 13px;
+    color: #0366d6;
+
+    i {
+        font-size: 12px;
+    }
+
+    .unlink-icon {
+        cursor: pointer;
+        color: #999;
+        font-size: 14px;
+
+        &:hover {
+            color: #ef4444;
+        }
+    }
+}
+
+.input-container {
+    max-width: 768px;
+    margin: 0 auto;
+    position: relative;
+}
+
+.input-box {
+    flex: 1;
+    position: relative;
+
+    ::v-deep .el-textarea__inner {
+        border: 1px solid #e5e5ea;
+        border-radius: 12px;
+        padding: 12px 50px 12px 16px;
+        font-size: 15px;
+        resize: none;
+        transition: all 0.2s ease;
+        background: #fafafa;
+
+        &:focus {
+            border-color: #d4d4d4;
+            background: white;
+            box-shadow: none;
+        }
+
+        &:hover:not(:focus) {
+            border-color: #d4d4d4;
+        }
+    }
+}
+
+.send-btn-wrapper {
+    position: absolute;
+    right: 8px;
+    bottom: 8px;
+    width: 32px;
+    height: 32px;
+    border-radius: 8px;
+    background: #1c1c1c;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    color: white;
+    font-size: 16px;
+
+    &:hover {
+        background: #333;
+    }
+
+    &:active {
+        transform: scale(0.95);
+    }
+}
+
+
+.input-tip {
+    max-width: 768px;
+    margin: 8px auto 0;
+    text-align: center;
+    font-size: 12px;
+    color: #999;
+}
+
+// 滚动条样式
+.messages-container::-webkit-scrollbar {
+    width: 6px;
+}
+
+.messages-container::-webkit-scrollbar-track {
+    background: transparent;
+}
+
+.messages-container::-webkit-scrollbar-thumb {
+    background: #d1d5db;
+    border-radius: 3px;
+
+    &:hover {
+        background: #9ca3af;
+    }
+}
+
+.chat-list::-webkit-scrollbar {
+    width: 6px;
+}
+
+.chat-list::-webkit-scrollbar-track {
+    background: transparent;
+}
+
+.chat-list::-webkit-scrollbar-thumb {
+    background: #e5e7eb;
+    border-radius: 3px;
+
+    &:hover {
+        background: #d1d5db;
+    }
+}
+
+// 客户选择对话框样式
+.customer-dialog-content {
+    .customer-filter {
+        margin-bottom: 16px;
+        padding: 16px;
+        background: #f5f7fa;
+        border-radius: 8px;
+        border: 1px solid #e4e7ed;
+
+        ::v-deep .el-form-item {
+            margin-bottom: 0;
+            margin-right: 20px;
+        }
+
+        ::v-deep .el-input__inner {
+            width: 200px;
+        }
+
+        ::v-deep .el-select {
+            width: 150px;
+        }
+    }
+
+    .el-table {
+        margin-bottom: 16px;
+    }
+
+    .customer-pagination {
+        display: flex;
+        justify-content: flex-end;
+        padding: 12px 0;
+    }
+}
+</style>