Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

zyy 2 месяцев назад
Родитель
Сommit
dec8050655

+ 18 - 0
.env.prod-jsbk

@@ -9,6 +9,22 @@ VUE_APP_ICP_URL =https://beian.miit.gov.cn
 # 网站LOG
 VUE_APP_LOG_URL =@/assets/logo/jsbk.jpg
 
+# 存储桶配置
+VUE_APP_OBS_ACCESS_KEY_ID = K2UTJGIN7UTZJR2XMXYG
+# 存储桶配置
+VUE_APP_OBS_SECRET_ACCESS_KEY = sbyeNJLbcYmH6copxeFP9pAoksM4NIT9Zw4x0SRX
+# 存储桶配置
+VUE_APP_OBS_SERVER = https://obs.cn-north-4.myhuaweicloud.com
+# 存储桶配置
+VUE_APP_OBS_BUCKET = jsbk-hw079058881
+# 存储桶配置
+VUE_APP_COS_BUCKET = jsbk-1323137866
+# 存储桶配置
+VUE_APP_COS_REGION = ap-chongqing
+# 线路一地址
+VUE_APP_VIDEO_LINE_1 = http://jsbktcpv.ylrzcloud.com
+# 线路二地址
+VUE_APP_VIDEO_LINE_2 = https://bjzmobs.ylrztop.com
 # 生产环境配置
 ENV = 'production'
 
@@ -23,3 +39,5 @@ VUE_APP_PROJECT_FROM=jsbk
 
 # 路由懒加载
 VUE_CLI_BABEL_TRANSPILE_MODULES = true
+
+VUE_APP_LIVE_WS_URL = wss://websocket.zhdyfztdj.cn/ws

+ 116 - 0
src/api/aiChat/aiChatSession.js

@@ -0,0 +1,116 @@
+import request from '@/utils/request'
+
+// 获取会话列表(分页)
+export function getChatSessionList(query) {
+  return request({
+    url: '/crm/customer/chat/chatSession/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 创建新会话
+export function createChatSession(data) {
+  return request({
+    url: '/crm/customer/chat/chatSession',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新会话标题
+export function updateChatSessionTitle(data) {
+  return request({
+    url: '/crm/customer/chat/chatSession/title',
+    method: 'put',
+    data: data
+  })
+}
+
+// 更新会话关联客户
+export function updateChatSessionCustomer(sessionId, customerId) {
+  return request({
+    url: '/crm/customer/chat/chatSession/customer',
+    method: 'put',
+    data: { sessionId, customerId }
+  })
+}
+
+// 删除会话
+export function deleteChatSession(sessionId) {
+  return request({
+    url: '/crm/customer/chat/chatSession/' + sessionId,
+    method: 'delete'
+  })
+}
+// 置顶/取消置顶会话
+export function pinChatSession(sessionId, isPinned) {
+    return request({
+        url: '/crm/customer/chat/chatSession/pin',
+        method: 'put',
+        data: { sessionId, isPinned }
+    })
+}
+
+
+
+
+
+
+
+
+
+
+
+// 获取消息列表(分页,带时间游标)
+export function getChatMessageList(query) {
+  return request({
+    url: '/crm/customer/chat/chatMsg/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 批量保存消息
+export function batchSaveMessages(data) {
+  return request({
+    url: '/crm/customer/chat/chatMsg/batch',
+    method: 'post',
+    data: data
+  })
+}
+
+// 单条消息保存
+export function saveChatMessage(data) {
+  return request({
+    url: '/crm/customer/chat/chatMsg',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新消息
+export function updateChatMessage(data) {
+  return request({
+    url: '/crm/customer/chat/chatMsg',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除消息
+export function deleteChatMessage(msgId) {
+  return request({
+    url: '/crm/customer/chat/chatMsg/' + msgId,
+    method: 'delete'
+  })
+}
+
+// 获取会话详情
+export function getChatSessionDetail(sessionId) {
+  return request({
+    url: '/crm/customer/chat/chatMsg/' + sessionId,
+    method: 'get'
+  })
+}
+

+ 7 - 0
src/api/company/aiModel.js

@@ -40,3 +40,10 @@ export function copy(id) {
         method: 'get'
     })
 }
+
+export function getCidConfig() {
+    return request({
+        url: '/aicall/account/getCidConfig',
+        method: 'get'
+    })
+}

+ 28 - 0
src/api/company/companyUser.js

@@ -182,6 +182,34 @@ export function getUserProfile() {
   })
 }
 
+//获取销售绑定的用户列表
+export function getSaleBindUserList(query) {
+  return request({
+    url: '/company/user/getFsUserBySaleId',
+    method: 'get',
+    params: query
+  })
+}
+
+//绑定销售会员关系
+export function bindSaleAndFsUser(data) {
+  return request({
+    url: '/company/user/bindSaleAndFsUser',
+    method: 'post',
+    data: data
+  })
+}
+
+//解绑销售会员关系
+export function unbindSaleAndFsUser(data) {
+  return request({
+    url: '/company/user/unbindSaleAndFsUser',
+    method: 'post',
+    data
+  });
+}
+
+
 // 修改用户个人信息
 export function updateUserProfile(data) {
   return request({

+ 27 - 0
src/api/company/voiceClone.js

@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+/**
+ * 上传音频文件并训练声音克隆音色
+ * @param {FormData} formData 包含 file / voice_name / speaker_id / language / model_type
+ */
+export function uploadAndTrain(formData) {
+  return request({
+    url: '/company/voiceClone/uploadAndTrain',
+    method: 'post',
+    data: formData,
+    headers: { 'Content-Type': 'multipart/form-data' }
+  })
+}
+
+/**
+ * TTS 语音合成测试
+ * @param {FormData} formData 包含 speakerId / language / text
+ */
+export function doubaoTtsTest(formData) {
+  return request({
+    url: '/company/voiceClone/doubaoTtsTest',
+    method: 'post',
+    data: formData,
+    headers: { 'Content-Type': 'multipart/form-data' }
+  })
+}

+ 84 - 12
src/views/company/aiModel/account/info.vue

@@ -34,6 +34,7 @@
                     v-model="form.concurrentNum"
                     placeholder="请输入并发数"
                     @input="handleConcurrentNumInput"
+                    :disabled="true"
                 />
             </el-form-item>
 
@@ -50,6 +51,7 @@
                             v-if="field.type === 'input'"
                             v-model="form.accountJson[field.name]"
                             :placeholder="'请输入' + field.label"
+                            :disabled="!!field.disableProp"
                         />
 
                         <!-- 文本域 -->
@@ -59,6 +61,7 @@
                             type="textarea"
                             :rows="field.rows || 3"
                             :placeholder="'请输入' + field.label"
+                             :disabled="!!field.disableProp"
                         />
 
                         <!-- 下拉框 -->
@@ -70,6 +73,7 @@
                             @change="(val) => handleSelectChange(field.name, val)"
                             filterable
                             clearable
+                             :disabled="!!field.disableProp"
                         >
                             <el-option
                                 v-for="option in field.options"
@@ -86,6 +90,7 @@
                             type="textarea"
                             :rows="30"
                             :placeholder="'请输入' + field.label"
+                             :disabled="!!field.disableProp"
                         />
                     </el-form-item>
 
@@ -197,7 +202,7 @@
 </template>
 
 <script>
-import { add, update } from '@/api/company/aiModel'  // 假设有 update 接口
+import { add, update, getCidConfig } from '@/api/company/aiModel'  // 假设有 update 接口
 import { all } from '@/api/company/aiCall'
 
 export default {
@@ -234,28 +239,69 @@ export default {
             // 动态字段配置
             dynamicFields: [],
             // 是否显示客户意向提示词
-            showIntentionTips: false
+            showIntentionTips: false,
+            cidConf:{}
         }
     },
     computed: {
         // 表单验证规则
         rules() {
-            return {
+            const baseRules = {
                 name: [
-                    { required: true, message: '请输入账户名称', trigger: 'blur' }
+                    { required: true, message: '请输入模型名称', trigger: 'blur' }
                 ],
                 providerClassName: [
                     { required: true, message: '请选择实现类', trigger: 'change' }
                 ],
                 concurrentNum: [
-                    { required: true, message: '请输入并发数', trigger: 'blur' },
+                    { required: true, message: '请输入模型并发数', trigger: 'blur' },
                     { pattern: /^\d+$/, message: '请输入数字', trigger: 'blur' },
                     { validator: this.validateConcurrentNum, trigger: 'blur' }
                 ],
                 transferManualDigit: [
                     { pattern: /^[0-9]?$/, message: '请输入单个数字0-9', trigger: 'blur' }
+                ],
+                interruptFlag: [
+                    { required: true, message: '请选择打断开关', trigger: 'change' }
+                ]
+            }
+
+            // 动态字段验证规则
+            this.dynamicFields.forEach(field => {
+                if (field.required) {
+                    const prop = 'accountJson.' + field.name
+                    const isSelect = field.type === 'select'
+                    baseRules[prop] = [
+                        {
+                            required: true,
+                            message: isSelect ? `请选择${field.label}` : `请输入${field.label}`,
+                            trigger: isSelect ? 'change' : 'blur'
+                        }
+                    ]
+                }
+            })
+
+            // Coze OAuth 相关字段验证
+            if (this.form.accountJson.tokenType === 'oauth') {
+                baseRules['accountJson.oauthClientId'] = [
+                    { required: true, message: '请输入Client ID', trigger: 'blur' }
+                ]
+                baseRules['accountJson.oauthPrivateKey'] = [
+                    { required: true, message: '请输入Private Key', trigger: 'blur' }
+                ]
+                baseRules['accountJson.oauthPublicKeyId'] = [
+                    { required: true, message: '请输入Public Key ID', trigger: 'blur' }
+                ]
+            }
+
+            // Coze PAT Token 验证
+            if (this.form.accountJson.tokenType === 'pat') {
+                baseRules['accountJson.patToken'] = [
+                    { required: true, message: '请输入PAT Token', trigger: 'blur' }
                 ]
             }
+
+            return baseRules
         }
     },
     watch: {
@@ -283,9 +329,16 @@ export default {
         }
     },
     created() {
-        this.loadKbCatOptions()
+        this.loadKbCatOptions();
+        this.initCidConfData();
     },
     methods: {
+        initCidConfData(){
+            getCidConfig().then(res=>{
+            console.log(JSON.parse(res.data));
+            this.cidConf = JSON.parse(res.data);
+        }).catch(res=>{})
+        },
         // 初始化表单数据
         initFormData(data) {
             this.form.id = data.id
@@ -297,7 +350,6 @@ export default {
             this.form.interruptIgnoreKeywords = data.interruptIgnoreKeywords || ''
             this.form.transferManualDigit = data.transferManualDigit || ''
             this.form.intentionTips = data.intentionTips || ''
-
             // 解析accountJson
             try {
                 this.form.accountJson = data.accountJson ?
@@ -310,7 +362,7 @@ export default {
 
             // 根据providerClassName更新动态字段
             if (this.form.providerClassName) {
-                this.updateDynamicFields(this.form.providerClassName)
+                this.updateDynamicFields(this.form.providerClassName,true)
             }
         },
 
@@ -349,7 +401,7 @@ export default {
         },
 
         // 更新动态字段
-        updateDynamicFields(providerClassName) {
+        updateDynamicFields(providerClassName,isUpdate) {
             this.dynamicFields = []
             this.showIntentionTips = false
 
@@ -358,9 +410,9 @@ export default {
 
             if (['DeepSeekChat', 'ChatGpt4o', 'JiutianChat'].includes(providerClassName)) {
                 this.dynamicFields = [
-                    { name: 'serverUrl', label: '服务地址', type: 'input', required: true },
-                    { name: 'apiKey', label: 'apiKey', type: 'input', required: true },
-                    { name: 'modelName', label: '模型名称', type: 'input', required: true },
+                    { name: 'serverUrl', label: '服务地址', type: 'input', required: true, disableProp:true  },
+                    { name: 'apiKey', label: 'apiKey', type: 'input', required: true, disableProp:true  },
+                    { name: 'modelName', label: '模型名称', type: 'input', required: true, disableProp:true  },
                     { name: 'llmTips', label: '大模型提示词', type: 'large-textarea', required: true },
                     { name: 'faqContext', label: 'FAQ上下文', type: 'large-textarea', required: true },
                     { name: 'kbCatId', label: '知识库分类', type: 'select', required: false, options: this.kbCatOptions },
@@ -448,6 +500,26 @@ export default {
             this.form.accountJson = {
                 ...newAccountJson,
                 ...this.form.accountJson
+            }   
+
+            //默认跟上配置值
+            if('DeepSeekChat' === providerClassName && !!this.cidConf && !!!isUpdate){
+                this.$set(this.form.accountJson,"serverUrl",this.cidConf.serverAddress);
+                this.$set(this.form.accountJson,"apiKey",this.cidConf.apiKey);
+                this.$set(this.form.accountJson,"modelName",this.cidConf.modelName);
+                this.$set(this.form,"concurrentNum",this.cidConf.concurrency);
+                 this.dynamicFields = [
+                    { name: 'serverUrl', label: '服务地址', type: 'input', required: true, disableProp:true },
+                    { name: 'apiKey', label: 'apiKey', type: 'input', required: true, disableProp:true },
+                    { name: 'modelName', label: '模型名称', type: 'input', required: true, disableProp:true },
+                    { name: 'llmTips', label: '大模型提示词', type: 'large-textarea', required: true },
+                    { name: 'faqContext', label: 'FAQ上下文', type: 'large-textarea', required: true },
+                    { name: 'kbCatId', label: '知识库分类', type: 'select', required: false, options: this.kbCatOptions },
+                    { name: 'transferToAgentTips', label: '转人工提示词', type: 'textarea', rows: 3, required: true },
+                    { name: 'hangupTips', label: '挂机提示', type: 'textarea', rows: 3, required: true },
+                    { name: 'customerNoVoiceTips', label: '客户不说话提示', type: 'textarea', rows: 3, required: true },
+                    { name: 'openingRemarks', label: '开场白', type: 'textarea', rows: 3, required: true }
+                ]
             }
         },
 

+ 7 - 3
src/views/company/aiModel/index.vue

@@ -76,8 +76,8 @@
         <!-- 数据表格 -->
         <el-table v-loading="loading" :data="modelList" border @selection-change="handleSelectionChange">
             <el-table-column type="selection" width="55" align="center" />
-            <el-table-column align="center" label="模型名称" prop="name" width="400" />
-            <el-table-column align="center" label="实现类" prop="providerClassName" width="400" />
+            <el-table-column align="center" label="模型名称" prop="name"  />
+            <el-table-column align="center" label="实现类" prop="providerClassName" />
             <el-table-column
                 align="center"
                 class-name="small-padding fixed-width"
@@ -227,7 +227,11 @@ export default {
         /** 获取所有实现类 */
         getAll() {
             all().then((response) => {
-                this.providerClassNameList = response.data || []
+                //隐藏其他几种大模型
+                if(!!response.data){
+                    response.data =  response.data.filter(a => a.providerClassName == "DeepSeekChat")
+                }
+                this.providerClassNameList = response.data || [];
             })
         },
         /** 新增按钮 */

+ 586 - 0
src/views/company/aiModel/voiceClone/index.vue

@@ -0,0 +1,586 @@
+<template>
+  <div class="app-container">
+    <!-- 注意事项 -->
+    <el-card class="tips-card" shadow="hover">
+      <div class="tips-header">
+        <i class="el-icon-warning-outline tips-icon"></i>
+        <span class="tips-title">语音克隆注意事项</span>
+      </div>
+      <el-row :gutter="32" class="tips-content">
+        <el-col :span="12">
+          <div class="tip-item">
+            <div class="tip-num">1</div>
+            <div class="tip-text">
+              <div class="tip-label">音色限制</div>
+              <div class="tip-desc">
+                <div class="tip-warning-box">
+                  <i class="el-icon-info"></i>
+                  <span>每个音色最多只能上传并训练 <strong>10</strong> 次</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div class="tip-item">
+            <div class="tip-num">2</div>
+            <div class="tip-text">
+              <div class="tip-label">录制要求</div>
+              <div class="tip-desc">
+                预先录制 <strong>20~30秒</strong> 左右的声音,确保环境安静且无噪音
+              </div>
+            </div>
+          </div>
+          <div class="tip-item tip-sub">
+            <div class="tip-sub-label">推荐录制方式:</div>
+            <div class="tip-sub-list">
+              <span class="tip-tag"><i class="el-icon-monitor"></i> 电脑+耳机,使用自带录音机</span>
+              <span class="tip-tag"><i class="el-icon-mobile-phone"></i> 手机录音后传到电脑</span>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <!-- 声音克隆表单 -->
+    <el-card class="box-card" style="margin-bottom: 16px;">
+      <div slot="header"><span>声音克隆配置</span></div>
+      <el-form ref="cloneForm" :model="cloneForm" :rules="cloneRules" label-width="120px">
+        <el-form-item label="音色名称" prop="voice_name">
+          <el-input
+            v-model="cloneForm.voice_name"
+            placeholder="请输入音色名称"
+            style="width: 320px;"
+          />
+        </el-form-item>
+
+        <el-form-item label="声音ID" prop="speaker_id">
+          <el-input
+            v-model="cloneForm.speaker_id"
+            placeholder="请输入声音ID"
+            style="width: 320px;"
+          />
+        </el-form-item>
+
+        <el-form-item label="模型类型" prop="model_type">
+          <el-select
+            v-model="cloneForm.model_type"
+            placeholder="请选择模型类型"
+            style="width: 400px;"
+            @change="onModelTypeChange"
+          >
+            <el-option :value="1" label="声音复刻ICL1.0效果" />
+            <!-- <el-option :value="2" label="DiT标准版效果(音色、不还原用户的风格)" />
+            <el-option :value="3" label="DiT还原版效果(音色、还原用户口音、语速等风格)" /> -->
+            <el-option :value="4" label="声音复刻ICL2.0效果" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="语言" prop="language">
+          <el-select
+            v-model="cloneForm.language"
+            placeholder="请选择语言"
+            style="width: 240px;"
+          >
+            <el-option
+              v-for="item in languageOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="音频文件">
+          <el-upload
+            ref="audioUpload"
+            action="#"
+            :auto-upload="false"
+            :on-change="onAudioFileChange"
+            :on-remove="onAudioFileRemove"
+            :file-list="audioFileList"
+            accept=".wav,.mp3,.ogg,.m4a,.aac"
+            :limit="1"
+            :on-exceed="onFileExceed"
+          >
+            <el-button size="small" type="primary" icon="el-icon-upload">
+              选择音频文件
+            </el-button>
+            <div slot="tip" class="el-upload__tip" style="color: #909399;">
+              支持 wav、mp3、ogg、m4a、aac 格式,推荐 20-30 秒录音
+            </div>
+          </el-upload>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button
+            type="success"
+            icon="el-icon-upload2"
+            :loading="uploadLoading"
+            @click="handleUploadAndTrain"
+          >
+            {{ uploadLoading ? '上传训练中,约15-30秒...' : '上传并训练' }}
+          </el-button>
+          <el-button
+            type="primary"
+            icon="el-icon-headset"
+            plain
+            @click="handleOpenTestPanel"
+          >
+            测试已有声音
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- TTS测试区域(上传训练成功后 或 点击测试按钮后显示) -->
+    <el-card v-if="showTtsArea" class="box-card">
+      <div slot="header" class="tts-card-header">
+        <span>TTS语音合成测试</span>
+        <el-button v-if="isManualTest" type="text" icon="el-icon-close" @click="showTtsArea = false">关闭</el-button>
+      </div>
+      <el-form ref="ttsForm" :model="ttsForm" label-width="120px">
+        <!-- 手动测试时需要输入声音ID -->
+        <el-form-item v-if="isManualTest" label="声音ID" prop="test_speaker_id">
+          <el-input
+            v-model="ttsForm.test_speaker_id"
+            placeholder="请输入要测试的声音ID"
+            style="width: 320px;"
+          />
+        </el-form-item>
+
+        <el-form-item label="测试文本" prop="tts_text">
+          <el-input
+            v-model="ttsForm.tts_text"
+            type="textarea"
+            :rows="5"
+            placeholder="请输入测试语音合成的文本"
+            style="width: 480px;"
+          />
+        </el-form-item>
+
+        <el-form-item label="语言" prop="language_test">
+          <el-select
+            v-model="ttsForm.language_test"
+            placeholder="请选择语言"
+            style="width: 240px;"
+          >
+            <el-option
+              v-for="item in ttsLanguageOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+          <el-button
+            type="primary"
+            icon="el-icon-video-play"
+            style="margin-left: 12px;"
+            :loading="ttsLoading"
+            :disabled="isManualTest && !ttsForm.test_speaker_id"
+            @click="handleTtsTest"
+          >
+            {{ ttsLoading ? '合成中...' : '开始合成' }}
+          </el-button>
+        </el-form-item>
+
+        <el-form-item v-if="audioSrc" label="播放">
+          <audio ref="audioPlayer" controls :src="audioSrc" style="margin-top: 4px;" />
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { uploadAndTrain, doubaoTtsTest } from '@/api/company/voiceClone'
+
+export default {
+  name: 'VoiceClone',
+  data() {
+    return {
+      // 声音克隆表单
+      cloneForm: {
+        voice_name: '',
+        speaker_id: '',
+        model_type: null,
+        language: 0
+      },
+      cloneRules: {
+        voice_name: [{ required: true, message: '请输入音色名称', trigger: 'blur' }],
+        speaker_id: [{ required: true, message: '请输入声音ID', trigger: 'blur' }],
+        model_type: [{ required: true, message: '请选择模型类型', trigger: 'change' }],
+        language: [{ required: true, message: '请选择语言', trigger: 'change' }]
+      },
+
+      // 音频文件列表
+      audioFileList: [],
+      selectedFile: null,
+
+      // 上传训练loading
+      uploadLoading: false,
+
+      // 是否显示TTS测试区域
+      showTtsArea: false,
+
+      // 是否为手动测试模式(区别于上传训练成功后的自动显示)
+      isManualTest: false,
+
+      // TTS测试表单
+      ttsForm: {
+        tts_text: '',
+        language_test: 0,
+        test_speaker_id: '' // 手动测试时的声音ID
+      },
+
+      // TTS testing loading
+      ttsLoading: false,
+
+      // 播放音频src(base64)
+      audioSrc: ''
+    }
+  },
+
+  computed: {
+    // 根据模型类型动态计算语言选项
+    languageOptions() {
+      const modelType = this.cloneForm.model_type
+      const options = [
+        { value: 0, label: '中文' },
+        { value: 1, label: '英文' }
+      ]
+      if (modelType <= 2) {
+        options.push(
+          { value: 2, label: '日语' },
+          { value: 3, label: '西班牙语' },
+          { value: 4, label: '印尼语' },
+          { value: 5, label: '葡萄牙语' }
+        )
+      }
+      if (modelType === 2) {
+        options.push(
+          { value: 6, label: '德语' },
+          { value: 7, label: '法语' }
+        )
+      }
+      return options
+    },
+    // TTS测试语言选项(手动测试时显示全部语言)
+    ttsLanguageOptions() {
+      return [
+        { value: 0, label: '中文' },
+        { value: 1, label: '英文' },
+        { value: 2, label: '日语' },
+        { value: 3, label: '西班牙语' },
+        { value: 4, label: '印尼语' },
+        { value: 5, label: '葡萄牙语' },
+        { value: 6, label: '德语' },
+        { value: 7, label: '法语' }
+      ]
+    }
+  },
+
+  methods: {
+    // 模型类型变更,重置语言为中文
+    onModelTypeChange() {
+      const validValues = this.languageOptions.map(o => o.value)
+      if (!validValues.includes(this.cloneForm.language)) {
+        this.cloneForm.language = 0
+      }
+      if (!validValues.includes(this.ttsForm.language_test)) {
+        this.ttsForm.language_test = 0
+      }
+    },
+
+    // 音频文件选择
+    onAudioFileChange(file) {
+      this.selectedFile = file.raw
+      this.audioFileList = [file]
+    },
+
+    // 音频文件移除
+    onAudioFileRemove() {
+      this.selectedFile = null
+      this.audioFileList = []
+    },
+
+    // 超出文件数量限制
+    onFileExceed() {
+      this.$message.warning('每次只能上传一个音频文件,请先移除已选文件')
+    },
+
+    // 打开手动测试面板
+    handleOpenTestPanel() {
+      this.isManualTest = true
+      this.showTtsArea = true
+      this.ttsForm.test_speaker_id = ''
+      this.ttsForm.tts_text = ''
+      this.ttsForm.language_test = 0
+      this.audioSrc = ''
+    },
+
+    // 上传并训练
+    handleUploadAndTrain() {
+      this.$refs.cloneForm.validate(valid => {
+        if (!valid) return
+        if (!this.selectedFile) {
+          this.$message.error('请选择音频文件')
+          return
+        }
+        this.uploadLoading = true
+
+        const formData = new FormData()
+        formData.append('file', this.selectedFile)
+        formData.append('voice_name', this.cloneForm.voice_name)
+        formData.append('speaker_id', this.cloneForm.speaker_id)
+        formData.append('language', this.cloneForm.language)
+        formData.append('model_type', this.cloneForm.model_type)
+
+        uploadAndTrain(formData).then(res => {
+          if (res.code === 200) {
+            this.$message.success(res.msg || '上传训练成功!')
+            this.isManualTest = false // 上传训练成功后的测试模式
+            this.showTtsArea = true
+          } else {
+            this.$message.error(res.msg || '上传训练失败,请重试')
+          }
+        }).catch(() => {
+          this.$message.error('上传训练失败,请重试')
+        }).finally(() => {
+          this.uploadLoading = false
+        })
+      })
+    },
+
+    // TTS测试
+    handleTtsTest() {
+      if (this.ttsLoading) {
+        this.$message.warning('请等待合成完成!')
+        return
+      }
+      if (!this.ttsForm.tts_text || this.ttsForm.tts_text.trim() === '') {
+        this.$message.error('请输入测试文本')
+        return
+      }
+
+      // 获取声音ID:手动测试用输入框的值,自动测试用表单的值
+      const speakerId = this.isManualTest ? this.ttsForm.test_speaker_id : this.cloneForm.speaker_id
+      if (!speakerId || speakerId.trim() === '') {
+        this.$message.error('请输入声音ID')
+        return
+      }
+
+      this.ttsLoading = true
+
+      const formData = new FormData()
+      formData.append('speakerId', speakerId)
+      formData.append('language', this.ttsForm.language_test)
+      formData.append('text', this.ttsForm.tts_text)
+
+      doubaoTtsTest(formData).then(res => {
+        if (res.code === 200) {
+          this.$message.success('TTS合成成功!')
+          // res.data 是 JSON 字符串 {"code":3000,"data":"...base64..."}
+          try {
+            const audioJson = JSON.parse(res.data)
+            if (audioJson.code === 3000 && audioJson.data) {
+              this.audioSrc = 'data:audio/mp3;base64,' + audioJson.data
+              this.$nextTick(() => {
+                this.$refs.audioPlayer && this.$refs.audioPlayer.play()
+              })
+            }
+          } catch (e) {
+            this.$message.warning('音频解析失败')
+          }
+        } else {
+          this.$message.error(res.msg || 'TTS测试失败,请重试')
+        }
+      }).catch(() => {
+        this.$message.error('TTS测试失败,请重试')
+      }).finally(() => {
+        this.ttsLoading = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.box-card {
+  margin-bottom: 16px;
+}
+
+/* TTS测试卡片头部 */
+.tts-card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+/* 注意事项卡片样式 */
+.tips-card {
+  margin-bottom: 20px;
+  border-left: 4px solid #409EFF;
+}
+
+.tips-card /deep/ .el-card__body {
+  padding: 16px 20px;
+}
+
+.tips-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+  padding-bottom: 12px;
+  border-bottom: 1px dashed #EBEEF5;
+}
+
+.tips-icon {
+  font-size: 22px;
+  color: #409EFF;
+  margin-right: 10px;
+}
+
+.tips-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.tips-content {
+  margin: 0;
+}
+
+.tip-item {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 12px;
+}
+
+.tip-item:last-child {
+  margin-bottom: 0;
+}
+
+.tip-num {
+  width: 24px;
+  height: 24px;
+  background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
+  border-radius: 50%;
+  color: #fff;
+  font-size: 13px;
+  font-weight: bold;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 12px;
+  flex-shrink: 0;
+}
+
+.tip-text {
+  flex: 1;
+}
+
+.tip-label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 4px;
+}
+
+.tip-desc {
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.6;
+}
+
+.tip-desc strong {
+  color: #409EFF;
+}
+
+.link-btn {
+  display: inline-flex;
+  align-items: center;
+  padding: 2px 8px;
+  background: #ecf5ff;
+  border-radius: 4px;
+  color: #409EFF;
+  font-size: 12px;
+  margin: 0 4px;
+  transition: all 0.3s;
+}
+
+.link-btn:hover {
+  background: #409EFF;
+  color: #fff;
+}
+
+.link-btn i {
+  margin-left: 2px;
+}
+
+.tip-warning {
+  display: inline-block;
+  margin-left: 4px;
+  padding: 1px 6px;
+  background: #fef0f0;
+  border-radius: 3px;
+  color: #f56c6c;
+  font-size: 12px;
+}
+
+.tip-warning-box {
+  display: inline-flex;
+  align-items: center;
+  padding: 8px 14px;
+  background: linear-gradient(135deg, #fff6f6 0%, #fef0f0 100%);
+  border: 1px solid #fbc4c4;
+  border-radius: 6px;
+  color: #f56c6c;
+  font-size: 13px;
+}
+
+.tip-warning-box i {
+  margin-right: 8px;
+  font-size: 16px;
+}
+
+.tip-warning-box strong {
+  color: #f56c6c;
+  font-size: 16px;
+  margin: 0 2px;
+}
+
+.tip-sub {
+  margin-top: 12px;
+  padding-left: 36px;
+}
+
+.tip-sub-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 8px;
+}
+
+.tip-sub-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.tip-tag {
+  text-align: center;
+  line-height: 20px;
+  display: inline-flex;
+  align-items: center;
+  padding: 4px 10px;
+  background: #f4f4f5;
+  border-radius: 4px;
+  font-size: 12px;
+  color: #606266;
+}
+
+.tip-tag i {
+  margin-right: 4px;
+  color: #909399;
+}
+</style>

+ 235 - 1
src/views/company/companyUser/index.vue

@@ -248,6 +248,14 @@
                 v-hasPermi="['qw:user:bind']"
               >查或换绑企微</el-button>
 
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-user"
+                @click="handleBindMember(scope.row)"
+                v-hasPermi="['company:user:bindUser']"
+              >绑定员工用户</el-button>
+
               <el-button
                 size="mini"
                 type="text"
@@ -706,6 +714,98 @@
       </div>
     </el-dialog>
 
+    <!-- 绑定会员对话框 -->
+    <el-dialog
+      :title="member.title"
+      :visible.sync="member.open"
+      width="800px"
+      append-to-body
+      class="bind-member-dialog"
+    >
+      <!-- 筛选表单 - 优化布局,修复按钮换行问题 -->
+      <el-form
+        :model="memberQueryParams"
+        :inline="true"
+        @submit.native.prevent
+        class="filter-form"
+      >
+        <div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;">
+          <el-form-item label="昵称" style="margin-bottom: 0; flex: 1 1 auto; min-width: 160px;">
+            <el-input
+              v-model="memberQueryParams.nickName"
+              placeholder="请输入昵称"
+              clearable
+              @keyup.enter.native="handleMemberQuery"
+              prefix-icon="el-icon-user"
+              style="width: 100%;"
+            />
+          </el-form-item>
+
+          <el-form-item label="手机号" style="margin-bottom: 0; flex: 1 1 auto; min-width: 160px;">
+            <el-input
+              v-model="memberQueryParams.phone"
+              placeholder="请输入手机号"
+              clearable
+              @keyup.enter.native="handleMemberQuery"
+              prefix-icon="el-icon-phone"
+              style="width: 100%;"
+            />
+          </el-form-item>
+
+          <el-form-item style="margin-bottom: 0; flex: 0 0 auto;">
+            <el-button type="primary" icon="el-icon-search" @click="handleMemberQuery" size="mini">搜索</el-button>
+            <el-button icon="el-icon-refresh" @click="resetMemberQuery" size="mini">重置</el-button>
+          </el-form-item>
+        </div>
+      </el-form>
+
+      <!-- 表格区域 -->
+      <div class="table-container" style="margin-top: 16px;">
+        <el-table
+          :data="memberList"
+          height="260"
+          highlight-current-row
+          @current-change="selectMember"
+          v-loading="loading"
+          border
+          stripe
+          size="small"
+        >
+          <el-table-column property="userId" label="ID" width="70" align="center"></el-table-column>
+          <el-table-column property="nickName" label="昵称" min-width="130" show-overflow-tooltip></el-table-column>
+          <el-table-column property="phone" label="手机号" width="130" align="center"></el-table-column>
+          <el-table-column property="status" label="绑定状态" width="100" align="center">
+            <template slot-scope="scope">
+              <el-tag
+                :type="scope.row.status === 1 ? 'success' : 'info'"
+                size="small"
+                effect="light"
+              >
+                {{ scope.row.status === 1 ? '已绑定' : '未绑定' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <!-- 分页组件 -->
+        <!-- 修改分页组件,添加判断条件 -->
+        <pagination
+          v-show="memberTotal > 0"
+          :total="memberTotal"
+          :page.sync="memberQueryParams.pageNum"
+          :limit.sync="memberQueryParams.pageSize"
+          @pagination="handlePagination"
+          style="margin-top: 10px; text-align: center;"
+        />
+      </div>
+
+      <!-- 底部按钮 -->
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancelBindMember" size="small">取 消</el-button>
+        <el-button type="primary" @click="confirmBindMember" size="small">确 定</el-button>
+      </div>
+    </el-dialog>
+
     <ai-sip-call-user ref="aiSipCallUser" v-show="false" @refreshParentData="getList" />
 
   </div>
@@ -741,7 +841,7 @@ import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
 import  selectUser  from "@/views/company/components/selectQwUser.vue";
 import { getConfigByKey } from "@/api/company/companyConfig";
 import axios from "axios";
-import {addCodeUrl} from "../../../api/company/companyUser";
+import {addCodeUrl, getSaleBindUserList} from "../../../api/company/companyUser";
 import selectDoctor from "@/views/qw/user/selectDoctor.vue";
 import {bindCidServer,unbindCidServer} from "@/api/company/companyAiWorkflowServer";
 import AiSipCallUser from "../../aiSipCall/aiSipCallUser.vue";
@@ -750,7 +850,23 @@ export default {
   components: {selectDoctor, Treeselect ,selectUser,AiSipCallUser},
   data() {
     return {
+      selectedMember: null,
+      currentCompanyId: null,
       bindCidServerLoading:false,
+      memberList: [],
+      member: {
+        open: false,
+        title: "绑定会员"
+      },
+      memberTotal: 0,
+      // 添加筛选参数
+      memberQueryParams: {
+        nickName: null,
+        phone: null,
+        pageNum: 1,
+        pageSize: 10
+      },
+
       doctor: {
         open: false,
         title: '绑定医生'
@@ -958,6 +1074,7 @@ export default {
       boundUsersList: [],
       bindUserLoading: false,
     };
+
   },
   watch: {
     // 根据名称筛选部门树
@@ -1013,6 +1130,123 @@ export default {
       }
     },
 
+
+    /**
+     * 绑定会员按钮操作
+     */
+    handleBindMember(row) {
+      this.currentCompanyId = row.userId;
+      // 重置筛选参数
+      this.memberQueryParams = {
+        nickName: null,
+        phone: null,
+        pageNum: 1,    // 重置为第一页
+        pageSize: 10   // 保持每页数量
+      };
+      this.selectedMember = null;
+      this.member.open = true;
+      this.getMemberList();
+    },
+
+
+    /**
+     * 获取会员列表
+     */
+    /**
+     * 获取会员列表
+     */
+    getMemberList() {
+      // 调用指定的接口获取会员列表
+      getSaleBindUserList({
+        pageNum: this.memberQueryParams.pageNum,
+        pageSize: this.memberQueryParams.pageSize,
+        nickName: this.memberQueryParams.nickName,
+        phone: this.memberQueryParams.phone
+      })
+        .then(response => {
+          this.memberList = response.rows || [];
+          this.memberTotal = response.total || 0;
+        })
+        .catch(error => {
+          console.error('获取会员列表失败:', error);
+          this.$message.error('获取会员列表失败');
+        });
+    },
+    /**
+     * 会员搜索按钮操作
+     */
+    handleMemberQuery() {
+      this.memberQueryParams.pageNum = 1;
+      this.getMemberList();
+    },
+
+    /**
+     * 会员重置按钮操作
+     */
+    resetMemberQuery() {
+      this.memberQueryParams = {
+        nickName: null,
+        phone: null,
+        pageNum: 1,
+        pageSize: 10
+      };
+      this.getMemberList();
+    },
+
+
+    /**
+     * 选择会员
+     */
+    selectMember(member) {
+      if (member) {
+        this.selectedMember = member;
+      }
+    },
+
+
+    /**
+     * 确认绑定会员
+     */
+    confirmBindMember() {
+      if (!this.selectedMember) {
+        this.$message.warning('请选择要绑定的会员');
+        return;
+      }
+      // 这里需要根据实际接口调整参数
+      const bindData = {
+        companyUserId: this.currentCompanyId,
+        userId: this.selectedMember.userId // 使用实际的用户ID字段
+      };
+
+      // 发起绑定请求
+      bindSaleAndFsUser(bindData).then(response => {
+        if (response.code === 200) {
+          this.$message.success('绑定会员成功');
+          this.cancelBindMember();
+          this.getList(); // 刷新当前页面列表
+        } else {
+          this.$message.error(response.msg || '绑定失败');
+        }
+      })
+        .catch(error => {
+          console.error('绑定会员失败:', error);
+          this.$message.error('绑定会员失败');
+        });
+    },
+    /**
+     * 取消绑定会员
+     */
+    cancelBindMember() {
+      this.member.open = false;
+      this.memberList = [];
+      this.selectedMember = null;
+      this.currentCompanyId = null;
+      this.memberQueryParams = {
+        nickName: null,
+        phone: null
+      };
+    },
+
     // 添加处理 selectUser 关闭的方法
     handleSelectUserClose() {
       this.user.open = false

+ 519 - 1
src/views/company/companyUser/profile/index.vue

@@ -36,6 +36,62 @@
                 <div class="pull-right">{{ user.createTime }}</div>
               </li>
             </ul>
+            <!-- 操作区域 - 优化布局 -->
+            <div class="action-section">
+              <!-- 绑定会员按钮 -->
+              <el-button
+                v-if="!user.fsUserId"
+                type="primary"
+                icon="el-icon-user"
+                @click="handleBindMember"
+                class="action-btn"
+                :loading="bindLoading"
+              >绑定会员</el-button>
+
+              <!-- 已绑定会员列表 -->
+              <div v-if="user.fsUserIdArray && user.fsUserIdArray.length > 0" class="bound-members">
+                <div class="bound-header">
+                  <el-tag type="success" effect="dark" size="small">已绑定会员</el-tag>
+                  <span class="bound-count">{{ user.fsUserIdArray.length }}个</span>
+                </div>
+                <div class="member-list">
+                  <div
+                    v-for="(fsUserId, index) in user.fsUserIdArray"
+                    :key="index"
+                    class="member-item"
+                  >
+                  <span class="member-id">
+                    <i class="el-icon-user-solid"></i>
+                    {{ fsUserId }}
+                  </span>
+                    <el-button
+                      size="mini"
+                      type="danger"
+                      icon="el-icon-delete"
+                      @click="handleUnbindSpecificMember(fsUserId)"
+                      circle
+                      title="解绑"
+                    ></el-button>
+                  </div>
+                </div>
+              </div>
+
+              <!-- 微信服务号绑定 -->
+              <div v-if="needWxTemplateMsg" class="wechat-section">
+                <div v-if="user.wechatBindStatus" class="wechat-bound">
+                  <el-tag type="success" effect="dark" size="small">
+                    <i class="el-icon-wechat"></i> 已绑定服务号通知
+                  </el-tag>
+                </div>
+                <el-button
+                  v-else
+                  type="primary"
+                  @click="openWechatBindDialog"
+                  icon="el-icon-wechat"
+                  class="wechat-btn"
+                >订阅服务号通知</el-button>
+              </div>
+            </div>
             <div class="text-center" style="margin-top: 20px;" v-if="needWxTemplateMsg">
               <div v-if="user.wechatBindStatus">
                 <el-tag type="success">已绑定服务号通知</el-tag>
@@ -86,7 +142,7 @@
 import userAvatar from "./userAvatar";
 import userInfo from "./userInfo";
 import resetPwd from "./resetPwd";
-import { getUserProfile } from "@/api/company/companyUser";
+import { getUserProfile,getSaleBindUserList, bindSaleAndFsUser, unbindSaleAndFsUser } from "@/api/company/companyUser";
 import { getWechatBindQrcode, checkWechatBindStatus } from "@/api/wechat";
 export default {
   name: "Profile",
@@ -101,6 +157,27 @@ export default {
       wechatQrcode: "",
       wechatBindTimer: null,
       needWxTemplateMsg: false,
+
+      bindLoading: false,
+      confirmLoading: false,
+      // 绑定会员相关数据
+      member: {
+        open: false,
+        title: "绑定会员"
+      },
+      memberList: [],
+      selectedMember: null,
+      currentCompanyId: null,
+      memberTotal: 0,
+      loading: false,
+      // 添加筛选参数
+      memberQueryParams: {
+        nickName: null,
+        phone: null,
+        userId: null,
+        pageNum: 1,
+        pageSize: 10
+      },
     };
   },
   created() {
@@ -110,6 +187,80 @@ export default {
     this.getUser();
   },
   methods: {
+
+    handleBindMember() {
+      this.currentCompanyId = this.user.userId;
+      this.resetMemberQuery();
+      this.selectedMember = null;
+      this.member.open = true;
+    },
+
+    resetMemberQuery() {
+      this.memberQueryParams = {
+        nickName: null,
+        phone: null,
+        userId: null,
+        pageNum: 1,
+        pageSize: 10
+      };
+      this.getMemberList();
+    },
+
+
+    getMemberList() {
+      this.loading = true;
+      getSaleBindUserList({
+        pageNum: this.memberQueryParams.pageNum,
+        pageSize: this.memberQueryParams.pageSize,
+        nickName: this.memberQueryParams.nickName,
+        phone: this.memberQueryParams.phone,
+        userId: this.memberQueryParams.userId
+      })
+        .then(response => {
+          this.memberList = response.rows || [];
+          this.memberTotal = response.total || 0;
+        })
+        .catch(error => {
+          console.error('获取会员列表失败:', error);
+          this.$message.error('获取会员列表失败');
+        })
+        .finally(() => {
+          this.loading = false;
+        });
+    },
+
+    handleUnbindSpecificMember(fsUserId) {
+      this.$confirm(`确定要解除与会员 ${fsUserId} 的绑定吗?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      })
+        .then(() => {
+          const unbindData = {
+            companyUserId: this.user.userId,
+            userId: fsUserId
+          };
+
+          unbindSaleAndFsUser(unbindData)
+            .then(response => {
+              if (response.code === 200) {
+                this.$message.success("解绑成功");
+                this.getUser();
+              } else {
+                this.$message.error(response.msg || "解绑失败");
+              }
+            })
+            .catch(error => {
+              console.error("解绑失败:", error);
+              this.$message.error("解绑失败");
+            });
+        })
+        .catch(() => {
+          this.$message.info("已取消解绑");
+        });
+    },
+
+
     getUser() {
       getUserProfile().then(response => {
         this.user = response.data;
@@ -146,3 +297,370 @@ export default {
   }
 };
 </script>
+<style scoped>
+/* 个人信息卡片样式 */
+.profile-card {
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.profile-card :deep(.el-card__header) {
+  padding: 15px 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-bottom: none;
+}
+
+.card-header {
+  color: white;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+}
+
+.card-header i {
+  margin-right: 8px;
+  font-size: 16px;
+}
+
+/* 头像区域 */
+.avatar-section {
+  text-align: center;
+  padding: 24px 20px 16px;
+  background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
+}
+
+.user-name {
+  margin: 12px 0 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+
+/* 信息列表 */
+.info-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 20px;
+  border-bottom: 1px solid #f0f0f0;
+  transition: background-color 0.3s;
+}
+
+.info-item:hover {
+  background-color: #f5f7fa;
+}
+
+.info-item:last-child {
+  border-bottom: none;
+}
+
+.info-label {
+  color: #606266;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+}
+
+.info-label svg {
+  margin-right: 8px;
+  width: 16px;
+  height: 16px;
+  color: #909399;
+}
+
+.info-value {
+  color: #303133;
+  font-weight: 500;
+  font-size: 14px;
+}
+
+/* 操作区域 */
+.action-section {
+  padding: 20px;
+  background-color: #fafbfc;
+  border-top: 1px solid #e4e7ed;
+}
+
+.action-btn {
+  width: 100%;
+  margin-bottom: 16px;
+}
+
+/* 已绑定会员列表 */
+.bound-members {
+  margin-top: 16px;
+}
+
+.bound-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.bound-count {
+  font-size: 12px;
+  color: #909399;
+  background-color: #f0f2f5;
+  padding: 2px 8px;
+  border-radius: 10px;
+}
+
+.member-list {
+  max-height: 200px;
+  overflow-y: auto;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  background-color: white;
+}
+
+.member-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 12px;
+  border-bottom: 1px solid #f0f0f0;
+  transition: background-color 0.3s;
+}
+
+.member-item:hover {
+  background-color: #f5f7fa;
+}
+
+.member-item:last-child {
+  border-bottom: none;
+}
+
+.member-id {
+  font-size: 13px;
+  color: #606266;
+  display: flex;
+  align-items: center;
+}
+
+.member-id i {
+  margin-right: 6px;
+  color: #409EFF;
+}
+
+/* 微信服务号区域 */
+.wechat-section {
+  margin-top: 16px;
+}
+
+.wechat-bound {
+  text-align: center;
+}
+
+.wechat-btn {
+  width: 100%;
+}
+
+/* 右侧卡片样式 */
+.content-card {
+  border-radius: 8px;
+}
+
+.content-card :deep(.el-card__header) {
+  padding: 15px 20px;
+  background-color: #fafbfc;
+  border-bottom: 1px solid #e4e7ed;
+}
+
+/* 绑定会员对话框样式 - 无背景色 */
+.bind-member-dialog :deep(.el-dialog__body) {
+  padding: 20px 24px;
+}
+
+/* 筛选表单 - 无背景色,优化排版 */
+.filter-container {
+  margin-bottom: 16px;
+  padding: 0 4px;
+}
+
+.filter-row {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+}
+
+.filter-buttons {
+  text-align: right;
+  white-space: nowrap;
+}
+
+.filter-buttons .el-button {
+  margin-left: 8px;
+}
+
+.filter-buttons .el-button:first-child {
+  margin-left: 0;
+}
+
+/* 表格区域 */
+.table-section {
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  overflow: hidden;
+}
+
+.member-table :deep(th) {
+  background-color: #f2f6fc;
+  color: #303133;
+  font-weight: 600;
+  font-size: 13px;
+}
+
+.status-tag {
+  min-width: 70px;
+}
+
+.status-tag i {
+  margin-right: 4px;
+}
+
+.nickname-cell, .phone-cell {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.nickname-cell i, .phone-cell i {
+  color: #909399;
+  font-size: 12px;
+}
+
+.no-data {
+  color: #c0c4cc;
+}
+
+/* 分页样式 - 支持每页条数选择 */
+.pagination-wrapper {
+  padding: 16px;
+  background-color: #ffffff;
+  border-top: 1px solid #e4e7ed;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.custom-pagination :deep(.el-pagination) {
+  padding: 0;
+}
+
+.custom-pagination :deep(.el-pagination__sizes) {
+  margin-right: 16px;
+}
+
+.empty-data {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 0;
+  color: #909399;
+  background-color: #ffffff;
+}
+
+.empty-data i {
+  font-size: 40px;
+  margin-bottom: 8px;
+  color: #dcdfe6;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-top: 10px;
+}
+
+.selected-info {
+  font-size: 13px;
+  color: #606266;
+  background-color: #f0f9eb;
+  padding: 6px 12px;
+  border-radius: 4px;
+  border: 1px solid #e1f3d8;
+}
+
+/* 微信对话框样式 */
+.wechat-dialog :deep(.el-dialog__body) {
+  padding: 30px 20px;
+}
+
+.qrcode-container {
+  text-align: center;
+}
+
+.qrcode-img {
+  width: 260px;
+  height: 260px;
+  border: 1px solid #e4e7ed;
+  padding: 10px;
+  border-radius: 8px;
+  background-color: white;
+}
+
+.qrcode-loading {
+  width: 260px;
+  height: 260px;
+  margin: 0 auto;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background-color: #f5f7fa;
+  border-radius: 8px;
+  color: #909399;
+}
+
+.qrcode-loading i {
+  font-size: 32px;
+  margin-bottom: 10px;
+}
+
+.qrcode-tip {
+  margin-top: 16px;
+  color: #606266;
+  font-size: 14px;
+}
+
+.qrcode-tip i {
+  color: #07C160;
+  margin-right: 6px;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .filter-container .el-col {
+    width: 100%;
+    margin-bottom: 8px;
+  }
+
+  .filter-buttons {
+    text-align: left;
+    margin-top: 8px;
+  }
+
+  .filter-buttons .el-button {
+    margin-left: 0;
+    margin-right: 8px;
+  }
+
+  .dialog-footer {
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .selected-info {
+    width: 100%;
+    text-align: center;
+  }
+}
+</style>

+ 15 - 15
src/views/company/companyVoiceRobotic/index.vue

@@ -775,9 +775,9 @@ import {
   companyUserList,
   wxList,
   taskRun,
-  getTypes,
+  // getTypes,
   getSmsTempList,
-  getCIDGroupList,
+  // getCIDGroupList,
   getExecRecords,
   getCurrentCompanyId
 } from "@/api/company/companyVoiceRobotic";
@@ -802,7 +802,7 @@ export default {
       currentCompanyId:null,
       // 遮罩层
       loading: true,
-      CIDGroupList:[],
+      // CIDGroupList:[],
       // 选中数组
       ids: [],
       weekList: [
@@ -963,10 +963,10 @@ export default {
     }).catch(res=>{
       console.log(res);
     })
-    getTypes().then(e => {
-      this.robotList = e.robot;
-      this.dialogList = e.dialog;
-    })
+    //getTypes().then(e => {
+      //this.robotList = e.robot;
+      //this.dialogList = e.dialog;
+    //})
     // listAll().then(e => {
     //   this.wxDialogList = e.data;
     // })
@@ -986,14 +986,14 @@ export default {
     this.getList();
     this.getSmsTempDropList();
 
-    getCIDGroupList().then(res=>{
-      console.log("----------------------")
-      console.log(res);
-      this.CIDGroupList = res.data;
-    }).catch(res=>{
-      console.log("catch_____+++++++")
-      console.log(res);
-    });
+    // getCIDGroupList().then(res=>{
+    //   console.log("----------------------")
+    //   console.log(res);
+    //   this.CIDGroupList = res.data;
+    // }).catch(res=>{
+    //   console.log("catch_____+++++++")
+    //   console.log(res);
+    // });
   },
   watch: {
     // 监听添加类型的切换,清空选择器数据

+ 1 - 1
src/views/course/userCoursePeriod/courseStatistics.vue

@@ -232,7 +232,7 @@
             </el-col>
             <el-col :span="6">
               <div class="stat-item">
-                <div class="stat-label">领红包人数</div>
+                <div class="stat-label">领红包/积分人数</div>
                 <div class="stat-value">{{ detailDialog.data.redPacketUserCount || 0 }}</div>
               </div>
             </el-col>

+ 370 - 255
src/views/crm/customer/customerDetail.vue

@@ -45,22 +45,21 @@
                         </h3>
                     </div>
                     <div class="tags-container">
-                        <div v-if="allAiTags.length > 0" class="tags-grid">
+                        <div v-if="allAiTags.length > 0" class="tags-list">
                             <div
                                 v-for="(item, index) in visibleTags"
                                 :key="item.id"
-                                class="tag-chip"
+                                class="tag-item"
                                 :class="{ 'tag-highlight': index < 3 }"
                             >
-                                <i class="fas fa-tag tag-icon"></i>
-                                <span class="tag-name">{{ item.propertyName }}</span>
+                                <span class="tag-key">{{ item.propertyName }}</span>
                                 <span class="tag-separator">:</span>
                                 <span class="tag-value">{{ item.propertyValue }}</span>
                             </div>
                         </div>
                         <div v-else class="empty-tags">
                             <i class="fas fa-inbox"></i>
-                            <span>暂无AI标签</span>
+                            <span>暂无 AI 标签</span>
                         </div>
 
                         <!-- 加载更多按钮 -->
@@ -116,39 +115,39 @@
                     <div class="records-table-wrapper">
                         <table class="records-table">
                             <thead>
-                                <tr>
-                                    <th><i class="fas fa-user"></i> 客户名称</th>
-                                    <th><i class="fas fa-chart-line"></i> 流失等级</th>
-                                    <th><i class="fas fa-heart"></i> 客户意向度</th>
-                                    <th><i class="far fa-clock"></i> 创建时间</th>
-                                    <th><i class="fas fa-cog"></i> 操作</th>
-                                </tr>
+                            <tr>
+                                <th><i class="fas fa-user"></i> 客户名称</th>
+                                <th><i class="fas fa-chart-line"></i> 流失等级</th>
+                                <th><i class="fas fa-heart"></i> 客户意向度</th>
+                                <th><i class="far fa-clock"></i> 创建时间</th>
+                                <th><i class="fas fa-cog"></i> 操作</th>
+                            </tr>
                             </thead>
                             <tbody>
-                                <tr v-for="record in communicationRecords" :key="record.id" class="record-row">
-                                    <td class="record-cell">{{ customerData.customerName }}</td>
-                                    <td class="record-cell">
+                            <tr v-for="record in communicationRecords" :key="record.id" class="record-row">
+                                <td class="record-cell">{{ customerData.customerName }}</td>
+                                <td class="record-cell">
                                         <span class="risk-level-tag" :class="getRecordRiskLevelClass(record)">
                                             {{ getRecordRiskLevelLabel(record) }}
                                         </span>
-                                    </td>
-                                    <td class="record-cell">
+                                </td>
+                                <td class="record-cell">
                                         <span class="intention-degree">
-                                            {{ getIntentionDegreeFromRecord(record) }}%
+                                            {{ getIntentionDegreeFromRecord(record) }}
                                         </span>
-                                    </td>
-                                    <td class="record-cell">{{ record.createTime }}</td>
-                                    <td class="record-cell">
-                                        <button @click="viewChat(record)" class="btn-view-chat">
-                                            <i class="fas fa-comments"></i> 聊天详情
-                                        </button>
-                                    </td>
-                                </tr>
-                                <tr v-if="!communicationRecords.length">
-                                    <td colspan="5" class="empty-tip">
-                                        <i class="fas fa-inbox"></i> 暂无沟通记录
-                                    </td>
-                                </tr>
+                                </td>
+                                <td class="record-cell">{{ record.createTime }}</td>
+                                <td class="record-cell">
+                                    <button @click="viewChat(record)" class="btn-view-chat">
+                                        <i class="fas fa-comments"></i> 聊天详情
+                                    </button>
+                                </td>
+                            </tr>
+                            <tr v-if="!communicationRecords.length">
+                                <td colspan="5" class="empty-tip">
+                                    <i class="fas fa-inbox"></i> 暂无沟通记录
+                                </td>
+                            </tr>
                             </tbody>
                         </table>
 
@@ -158,7 +157,7 @@
                                 @current-change="handleCommunicationRecordsPageChange"
                                 @size-change="handleCommunicationRecordsSizeChange"
                                 :current-page="communicationRecordsPageNum"
-                                :page-sizes="[3, 10, 20, 50]"
+                                :page-sizes="[4, 10, 20, 50]"
                                 :page-size="communicationRecordsPageSize"
                                 layout="total, sizes, prev, pager, next, jumper"
                                 :total="communicationRecordsTotal"
@@ -172,7 +171,9 @@
                         <div class="chat-dialog-header">
                             <div class="chat-title">
                                 <i class="fas fa-comments"></i>
-                                <span>{{ (currentChatRecord && currentChatRecord.customerName) || (customerData && customerData.customerName) }} - 历史聊天记录</span>
+                                <span>{{
+                                        (currentChatRecord && currentChatRecord.customerName) || (customerData && customerData.customerName)
+                                    }} - 历史聊天记录</span>
                             </div>
                             <button @click="closeChatDialog" class="btn-close">
                                 ×
@@ -190,7 +191,8 @@
                                     <!-- AI 消息:头像在左,名称在聊天内容上方靠左 -->
                                     <div v-if="msg.type === 'ai'" class="message-wrapper message-wrapper-left">
                                         <div class="message-avatar message-avatar-ai">
-                                            <img src="/static/images/ai-avatar.svg" alt="AI" @error="handleAvatarError($event, 'ai')" />
+                                            <img src="/static/images/ai-avatar.svg" alt="AI"
+                                                 @error="handleAvatarError($event, 'ai')"/>
                                         </div>
                                         <div class="message-content">
                                             <div class="message-name message-name-ai">AI</div>
@@ -208,13 +210,16 @@
                                             </div>
                                         </div>
                                         <div class="message-avatar message-avatar-customer">
-                                            <img src="/static/images/customer-avatar.svg" alt="客户" @error="handleAvatarError($event, 'customer')" />
+                                            <img src="/static/images/customer-avatar.svg" alt="客户"
+                                                 @error="handleAvatarError($event, 'customer')"/>
                                         </div>
                                     </div>
                                 </div>
 
                                 <!-- 空数据提示 -->
-                                <div v-if="!parseChatMessages(currentChatRecord && currentChatRecord.aiChatRecord).length" class="empty-chat-tip">
+                                <div
+                                    v-if="!parseChatMessages(currentChatRecord && currentChatRecord.aiChatRecord).length"
+                                    class="empty-chat-tip">
                                     <i class="fas fa-inbox"></i> 暂无聊天内容
                                 </div>
                             </div>
@@ -256,19 +261,25 @@
                     <div class="intention-section">
                         <div class="intention-header">
                             <span class="intention-label">客户意向度</span>
-                            <span v-if="getIntentionDegree() === -1" class="no-data-tip">暂无分析数据</span>
-                        </div>
-                        <div class="progress-bar-wrapper" v-if="getIntentionDegree() !== -1">
-                            <div class="progress-bar-modern">
-                                <div class="progress-fill-modern"
-                                     :style="{
-                                         width: getIntentionDegree() + '%',
-                                         background: getProgressGradient(getIntentionDegree())
-                                     }"
-                                     :class="getIntentionClass(getIntentionDegree())">
-                                    <span class="progress-text">{{ getIntentionDegree() }}%</span>
+                            <el-tooltip placement="top" effect="light">
+                                <i class="el-icon-info intention-info-icon"></i>
+                                <div slot="content" class="intention-tooltip">
+                                    <div><strong>A 级</strong> - 最高意向度</div>
+                                    <div><strong>B 级</strong> - 高意向度</div>
+                                    <div><strong>C 级</strong> - 中等意向度</div>
+                                    <div><strong>D 级</strong> - 较低意向度</div>
+                                    <div><strong>E 级</strong> - 低意向度</div>
+                                    <div><strong>F 级</strong> - 最低意向度</div>
                                 </div>
-                            </div>
+                            </el-tooltip>
+                        </div>
+                        <div class="intention-watermark"
+                             v-if="getIntentionDegree()"
+                             :class="getIntentionColorClass(getIntentionDegree())">
+                            {{ getIntentionDegree() }}
+                        </div>
+                        <div class="no-intention-tip" v-else>
+                            暂无评级
                         </div>
                     </div>
                 </div>
@@ -289,13 +300,13 @@ export default {
             customerData: null, // 从列表页传递过来的完整客户数据
             aiTags: [],// 需要显示的 AI 标签
             allAiTags: [], // 存储所有 AI 标签
-            tagsPageSize: 3,//默认展开标签的数量
+            tagsPageSize: 5,//默认展开标签的数量
             isExpanded: false, // 是否已展开显示全部标签
             // 聊天记录分页相关
             communicationRecords: [],
             communicationRecordsTotal: 0,
             communicationRecordsPageNum: 1,
-            communicationRecordsPageSize: 3,
+            communicationRecordsPageSize: 4,
             // 聊天弹窗相关
             chatDialogVisible: false, // 聊天弹窗是否显示
             currentChatRecord: null, // 当前查看的聊天记录
@@ -380,7 +391,7 @@ export default {
     methods: {
         loadCustomerTags() {
             listByCustomerId(this.customerUserId).then((response) => {
-                if(response.code === 200){
+                if (response.code === 200) {
                     this.allAiTags = response.data || [];
                     // 强制 Vue 更新视图
                     this.$forceUpdate();
@@ -398,7 +409,7 @@ export default {
                 customerId: this.customerUserId
             };
             listAnalyze(params).then((response) => {
-                if(response.code === 200){
+                if (response.code === 200) {
                     this.communicationRecords = response.rows || [];
                     this.communicationRecordsTotal = response.total || 0;
                 } else {
@@ -443,7 +454,7 @@ export default {
                         }));
                     }
                     // 如果是对象,转换为数组
-                    return [{ content: parsed.content, type: parsed.type || 'ai' }];
+                    return [{content: parsed.content, type: parsed.type || 'ai'}];
                 } catch (e) {
                     // 解析失败,返回空数组
                     console.error('解析聊天记录失败:', e);
@@ -470,25 +481,7 @@ export default {
                 img.parentElement.innerHTML = '<i class="fas fa-user" style="font-size: 24px; color: white;"></i>';
             }
         },
-        // 获取意向度样式类
-        getIntentionClass(percent) {
-            if (percent >= 80) return 'excellent';
-            if (percent >= 60) return 'good';
-            if (percent >= 40) return 'normal';
-            return 'poor';
-        },
-        // 获取进度条渐变色
-        getProgressGradient(percent) {
-            if (percent >= 80) {
-                return 'linear-gradient(90deg, #10b981 0%, #059669 100%)';
-            } else if (percent >= 60) {
-                return 'linear-gradient(90deg, #3b82f6 0%, #2563eb 100%)';
-            } else if (percent >= 40) {
-                return 'linear-gradient(90deg, #f59e0b 0%, #d97706 100%)';
-            } else {
-                return 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)';
-            }
-        },
+
         // 分页改变事件
         handleCommunicationRecordsPageChange(pageNum) {
             this.communicationRecordsPageNum = pageNum;
@@ -553,12 +546,24 @@ export default {
         // 获取客户意向度
         getIntentionDegree() {
             if (!this.communicationRecords || this.communicationRecords.length === 0) {
-                return -1;
+                return '';
             }
             const latestRecord = this.communicationRecords[0];
-            const degree = parseInt(latestRecord.intentionDegree);
-            // 如果是有效数字且在 0-100 之间,返回该值,否则返回 -1 表示无数据
-            return (degree >= 0 && degree <= 100) ? degree : -1;
+            return latestRecord.intentionDegree || '';
+        },
+        // 根据意向度等级获取颜色样式类
+        getIntentionColorClass(grade) {
+            if (!grade) return '';
+            const gradeUpper = grade.toUpperCase();
+            const colorMap = {
+                'A': 'intention-grade-a',
+                'B': 'intention-grade-b',
+                'C': 'intention-grade-c',
+                'D': 'intention-grade-d',
+                'E': 'intention-grade-e',
+                'F': 'intention-grade-f'
+            };
+            return colorMap[gradeUpper] || '';
         },
         // 获取单条记录的风险等级数值
         getRecordAttritionLevel(record) {
@@ -580,8 +585,7 @@ export default {
         // 获取单条记录的客户意向度
         getIntentionDegreeFromRecord(record) {
             if (!record) return 0;
-            const degree = parseInt(record.intentionDegree);
-            return (degree >= 0 && degree <= 100) ? degree : 0;
+            return record.intentionDegree;
         },
         // 获取流失风险等级样式类
         getRiskLevelClass() {
@@ -618,13 +622,31 @@ export default {
     box-sizing: border-box;
 }
 
+.left-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.middle-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.right-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
 .customer-container {
     max-width: 1600px;
     margin: 0 auto;
     font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
     background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
-    padding: 24px;
-    min-height: 100vh;
+    padding: 8px;
+    min-height: calc(100vh - 50px);
 }
 
 @keyframes pulse {
@@ -639,8 +661,9 @@ export default {
 .main-grid-three-columns {
     display: grid;
     grid-template-columns: 380px 1fr 340px;
-    gap: 28px;
+    gap: 12px;
     animation: fadeIn 0.6s ease-in-out;
+    align-items: stretch;
 }
 
 @keyframes fadeIn {
@@ -669,10 +692,10 @@ export default {
 
 .card {
     background: white;
-    border-radius: 20px;
-    padding: 24px;
-    margin-bottom: 28px;
-    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
+    border-radius: 12px;
+    padding: 12px;
+    margin-bottom: 12px;
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
     transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
     border: 1px solid rgba(226, 232, 240, 0.5);
     position: relative;
@@ -718,13 +741,17 @@ export default {
     right: -50%;
     width: 200%;
     height: 200%;
-    background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
+    background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
     animation: shimmer 3s infinite;
 }
 
 @keyframes shimmer {
-    0%, 100% { transform: translate(0, 0); }
-    50% { transform: translate(-30%, -30%); }
+    0%, 100% {
+        transform: translate(0, 0);
+    }
+    50% {
+        transform: translate(-30%, -30%);
+    }
 }
 
 .card-highlight .card-header h3 {
@@ -733,9 +760,9 @@ export default {
 
 .card-highlight .summary-text {
     color: white;
-    font-size: 15px;
-    line-height: 1.7;
-    max-height: 150px;
+    font-size: 17px;
+    line-height: 1.6;
+    max-height: 120px;
     overflow-y: auto;
     overflow-x: hidden;
     padding-right: 4px;
@@ -762,10 +789,12 @@ export default {
 .card-highlight .summary-meta span {
     color: rgba(255, 255, 255, 0.9);
 }
+
 /* 表格卡片 */
 .card-table {
     background: white;
 }
+
 /* 风险卡片 */
 .risk-card {
     border-left: 4px solid #10b981;
@@ -823,7 +852,7 @@ export default {
     gap: 6px;
     padding: 6px 14px;
     border-radius: 8px;
-    font-size: 13px;
+    font-size: 16px;
     font-weight: 700;
     color: white;
     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
@@ -857,12 +886,12 @@ export default {
 
 /* 风险分析内容 */
 .risk-analysis {
-    margin-top: 16px;
-    padding: 16px;
+    margin-top: 10px;
+    padding: 10px;
     background: rgba(255, 255, 255, 0.7);
-    border-radius: 12px;
+    border-radius: 10px;
     backdrop-filter: blur(10px);
-    max-height: 200px;
+    max-height: 180px;
     overflow-y: auto;
     overflow-x: hidden;
     padding-right: 4px;
@@ -887,28 +916,28 @@ export default {
 }
 
 .risk-text {
-    font-size: 14px;
-    line-height: 1.8;
+    font-size: 16px;
+    line-height: 1.7;
     color: #475569;
     margin: 0;
 }
 
 .risk-tip {
-    margin-top: 12px;
-    padding: 12px;
+    margin-top: 8px;
+    padding: 8px;
     background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.9) 100%);
     border-left: 3px solid #f59e0b;
-    border-radius: 8px;
-    font-size: 13px;
+    border-radius: 6px;
+    font-size: 15px;
     color: #92400e;
     display: flex;
     align-items: flex-start;
-    gap: 8px;
+    gap: 6px;
 }
 
 .risk-tip i {
-    font-size: 14px;
-    margin-top: 2px;
+    font-size: 12px;
+    margin-top: 1px;
     color: #f59e0b;
 }
 
@@ -924,17 +953,17 @@ export default {
     justify-content: space-between;
     align-items: center;
     border-bottom: 1px solid #eef2ff;
-    padding-bottom: 12px;
-    margin-bottom: 16px;
+    padding-bottom: 6px;
+    margin-bottom: 10px;
     flex-wrap: wrap;
 }
 
 .card-header h3 {
-    font-size: 17px;
+    font-size: 19px;
     font-weight: 700;
     display: flex;
     align-items: center;
-    gap: 10px;
+    gap: 8px;
     color: #0f172a;
     letter-spacing: -0.02em;
 }
@@ -971,7 +1000,7 @@ export default {
 .records-table {
     width: 100%;
     border-collapse: collapse;
-    font-size: 14px;
+    font-size: 15px;
 }
 
 .records-table thead {
@@ -983,7 +1012,7 @@ export default {
     padding: 12px 16px;
     text-align: center !important;
     font-weight: 600;
-    font-size: 13px;
+    font-size: 14px;
     border-bottom: 2px solid #e2e8f0;
     color: #64748b;
 }
@@ -1010,7 +1039,7 @@ export default {
 }
 
 .record-cell {
-    font-size: 14px;
+    font-size: 15px;
     color: #334155;
     text-align: center !important;
 }
@@ -1020,7 +1049,7 @@ export default {
     display: inline-block;
     padding: 4px 12px;
     border-radius: 6px;
-    font-size: 12px;
+    font-size: 13px;
     font-weight: 500;
     border: 1px solid;
 }
@@ -1030,7 +1059,7 @@ export default {
     display: inline-block;
     padding: 4px 12px;
     border-radius: 6px;
-    font-size: 13px;
+    font-size: 14px;
     font-weight: 600;
     color: #6366f1;
     background: rgba(99, 102, 241, 0.08);
@@ -1043,7 +1072,7 @@ export default {
     border: 1px solid #667eea;
     padding: 6px 12px;
     border-radius: 6px;
-    font-size: 13px;
+    font-size: 14px;
     cursor: pointer;
     display: inline-flex;
     align-items: center;
@@ -1057,7 +1086,7 @@ export default {
 
 .empty-tip {
     color: #94a3b8;
-    font-size: 13px;
+    font-size: 16px;
     text-align: center;
     padding: 40px 20px;
     background: transparent;
@@ -1066,11 +1095,11 @@ export default {
 
 /* 沟通摘要样式 */
 .summary-text.compact {
-    max-height: 120px;
+    max-height: 100px;
     overflow-y: auto;
     overflow-x: hidden;
     line-height: 1.6;
-    font-size: 14px;
+    font-size: 16px;
     color: #475569;
     padding-right: 4px;
 }
@@ -1096,7 +1125,7 @@ export default {
 /* AI 标签美化样式 */
 .tags-container {
     padding: 0;
-    max-height: 300px;
+    min-height: calc(16px * 5 + 6px * 4 + 12px * 2);
     overflow-y: auto;
     overflow-x: hidden;
     display: flex;
@@ -1121,50 +1150,49 @@ export default {
     background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
 }
 
-.tags-grid {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+/* 标签列表 - 每行一个 */
+.tags-list {
+    display: flex;
+    flex-direction: column;
     gap: 6px;
-    margin-bottom: 12px;
+    margin-bottom: 6px;
 }
 
-.tag-chip {
-    display: grid;
-    grid-template-columns: 35% 65%;
+.tag-item {
+    display: flex;
     align-items: center;
-    gap: 8px;
-    padding: 6px 12px;
+    gap: 6px;
+    padding: 6px 10px;
     background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
     border: 1px solid #e2e8f0;
-    border-radius: 8px;
-    font-size: 13px;
+    border-radius: 6px;
+    font-size: 15px;
     transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
     cursor: default;
     position: relative;
     overflow: hidden;
-    word-break: break-word;
 }
 
-.tag-chip::before {
+.tag-item::before {
     content: '';
     position: absolute;
     top: 0;
     left: 0;
-    width: 3px;
+    width: 4px;
     height: 100%;
     background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
     transition: width 0.3s ease;
 }
 
-.tag-chip:hover {
+.tag-item:hover {
     background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
     border-color: #cbd5e1;
-    transform: translateX(4px);
-    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
+    transform: translateX(6px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
 }
 
-.tag-chip:hover::before {
-    width: 4px;
+.tag-item:hover::before {
+    width: 5px;
     background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
 }
 
@@ -1177,28 +1205,19 @@ export default {
     background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
 }
 
-.tag-icon {
-    color: #64748b;
-    font-size: 12px;
-    transition: all 0.3s ease;
-}
-
-.tag-chip:hover .tag-icon {
-    color: #3b82f6;
-    transform: scale(1.05);
-}
-
-.tag-name {
+.tag-key {
     font-weight: 600;
     color: #475569;
-    white-space: normal;
-    word-break: break-word;
-    line-height: 1.3;
+    white-space: nowrap;
+    flex-shrink: 0;
+    font-size: 15px;
 }
 
 .tag-separator {
     color: #94a3b8;
     font-weight: 300;
+    flex-shrink: 0;
+    font-size: 15px;
 }
 
 .tag-value {
@@ -1207,6 +1226,7 @@ export default {
     word-break: break-word;
     flex: 1;
     min-width: 0;
+    font-size: 15px;
 }
 
 .empty-tags {
@@ -1216,7 +1236,7 @@ export default {
     justify-content: center;
     padding: 40px 20px;
     color: #94a3b8;
-    font-size: 14px;
+    font-size: 17px;
     background: linear-gradient(135deg, rgba(248, 250, 252, 0.5) 0%, rgba(241, 245, 249, 0.5) 100%);
     border-radius: 12px;
     border: 2px dashed #e2e8f0;
@@ -1247,7 +1267,7 @@ export default {
     border: 1px solid #667eea;
     padding: 8px 16px;
     border-radius: 6px;
-    font-size: 13px;
+    font-size: 16px;
     cursor: pointer;
     display: inline-flex;
     align-items: center;
@@ -1414,6 +1434,7 @@ export default {
     color: #667eea;
     font-weight: 500;
 }
+
 .message-avatar {
     width: 32px;
     height: 32px;
@@ -1522,6 +1543,7 @@ export default {
     border-left: 6px solid #d9fdd3;
     border-right: none;
 }
+
 .empty-chat-tip {
     display: flex;
     flex-direction: column;
@@ -1542,8 +1564,8 @@ export default {
 .profile-grid {
     display: flex;
     flex-direction: column;
-    gap: 6px;
-    max-height: 400px;
+    gap: 4px;
+    max-height: 350px;
     overflow-y: auto;
     overflow-x: hidden;
     padding-right: 4px;
@@ -1572,9 +1594,9 @@ export default {
     display: grid;
     grid-template-columns: 35% 65%;
     align-items: flex-start;
-    gap: 8px;
-    padding: 6px 12px;
-    border-radius: 8px;
+    gap: 6px;
+    padding: 4px 10px;
+    border-radius: 6px;
     transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
     background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
     border: 1px solid #e2e8f0;
@@ -1606,13 +1628,13 @@ export default {
 }
 
 .profile-item .label {
-    font-size: 13px;
+    font-size: 15px;
     color: #64748b;
     font-weight: 600;
     white-space: normal;
     display: flex;
     align-items: center;
-    gap: 6px;
+    gap: 4px;
     line-height: 1.4;
     min-width: 0;
 }
@@ -1626,7 +1648,7 @@ export default {
 }
 
 .profile-item .value {
-    font-size: 14px;
+    font-size: 16px;
     color: #0f172a;
     font-weight: 500;
     word-break: break-word;
@@ -1636,7 +1658,7 @@ export default {
 
 .profile-item .value.highlight {
     color: #0369a1;
-    font-size: 14px;
+    font-size: 16px;
     font-weight: 600;
 }
 
@@ -1657,29 +1679,29 @@ export default {
 /* 客户关注点 & 意向度样式 */
 .focus-points {
     padding: 0;
-    margin-bottom: 20px;
+    margin-bottom: 10px;
 }
 
 .focus-title {
-    font-size: 14px;
+    font-size: 17px;
     color: #64748b;
     font-weight: 600;
-    margin-bottom: 12px;
+    margin-bottom: 8px;
     display: flex;
     align-items: center;
-    gap: 8px;
+    gap: 6px;
 }
 
 .focus-title i {
     color: #3b82f6;
-    font-size: 16px;
+    font-size: 14px;
 }
 
 .focus-list {
     list-style: none;
     padding: 0;
     margin: 0;
-    max-height: 180px;
+    max-height: 200px;
     overflow-y: auto;
     overflow-x: hidden;
     padding-right: 4px;
@@ -1706,14 +1728,14 @@ export default {
 .focus-item {
     display: flex;
     align-items: flex-start;
-    gap: 8px;
-    padding: 8px 12px;
+    gap: 6px;
+    padding: 6px 10px;
     margin-bottom: 6px;
     background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
-    border-radius: 8px;
+    border-radius: 6px;
     border: 1px solid #e2e8f0;
     transition: all 0.3s ease;
-    font-size: 14px;
+    font-size: 15px;
     color: #334155;
     line-height: 1.5;
 }
@@ -1727,131 +1749,224 @@ export default {
 
 .focus-item i {
     color: #3b82f6;
-    font-size: 12px;
-    margin-top: 2px;
+    font-size: 10px;
+    margin-top: 1px;
     flex-shrink: 0;
 }
 
-/* 意向度样式 */
+/* 意向度样式 - 水印风格 */
 .intention-section {
-    margin-top: 20px;
-    padding-top: 16px;
+    margin-top: 10px;
+    padding-top: 8px;
     border-top: 1px solid #e2e8f0;
 }
 
 .intention-header {
+    margin-bottom: 6px;
     display: flex;
-    justify-content: space-between;
     align-items: center;
-    margin-bottom: 12px;
+    gap: 6px;
 }
 
-.no-data-tip {
-    font-size: 13px;
+.intention-info-icon {
     color: #94a3b8;
-    font-style: italic;
-    padding: 4px 12px;
-    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
-    border-radius: 6px;
-    border: 1px dashed #cbd5e1;
+    font-size: 16px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+}
+
+.intention-info-icon:hover {
+    color: #3b82f6;
+}
+
+/* 意向度提示框样式 */
+.intention-tooltip {
+    line-height: 1.8;
+    font-size: 13px;
+}
+
+.intention-tooltip div {
+    padding: 2px 0;
+}
+
+.intention-tooltip strong {
+    color: #1e293b;
+    font-weight: 600;
 }
 
 .intention-label {
-    font-size: 14px;
+    font-size: 16px;
     font-weight: 600;
     color: #64748b;
     display: flex;
     align-items: center;
-    gap: 6px;
+    gap: 4px;
 }
 
 .intention-label::before {
     content: '';
-    width: 4px;
-    height: 16px;
+    width: 3px;
+    height: 14px;
     background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
     border-radius: 2px;
 }
 
-.progress-bar-wrapper {
+/* 水印风格意向度显示 - 按等级着色 */
+.intention-watermark {
+    font-size: 59px;
+    font-weight: 800;
+    text-align: center;
+    padding: 18px 12px;
+    border-radius: 10px;
+    border: 2px solid;
     position: relative;
+    overflow: hidden;
+    letter-spacing: 6px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+    transition: all 0.3s ease;
 }
 
-.progress-bar-modern {
-    width: 100%;
-    height: 24px;
-    background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
-    border-radius: 12px;
-    overflow: hidden;
-    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06);
-    border: 1px solid #e2e8f0;
-    position: relative;
+/* A 级 - 金色/绿色,最高级别 */
+.intention-grade-a {
+    background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #fef3c7 100%);
+    border-color: #f59e0b;
+    color: #92400e;
+    text-shadow: 0 2px 4px rgba(146, 64, 14, 0.2);
 }
 
-.progress-bar-modern::before {
-    content: '';
+.intention-grade-a::after {
+    content: 'A';
     position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background: repeating-linear-gradient(
-        45deg,
-        transparent,
-        transparent 10px,
-        rgba(255, 255, 255, 0.03) 10px,
-        rgba(255, 255, 255, 0.03) 20px
-    );
-    z-index: 1;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(245, 158, 11, 0.08);
+    z-index: 0;
     pointer-events: none;
 }
 
-.progress-fill-modern {
-    height: 100%;
-    border-radius: 12px;
-    display: flex;
-    align-items: center;
-    justify-content: flex-end;
-    padding-right: 12px;
-    transition: width 1s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s ease;
-    position: relative;
-    overflow: hidden;
-    min-width: 60px;
+/* B 级 - 蓝色 */
+.intention-grade-b {
+    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 50%, #dbeafe 100%);
+    border-color: #3b82f6;
+    color: #1e40af;
+    text-shadow: 0 2px 4px rgba(30, 64, 175, 0.2);
 }
 
-.progress-fill-modern::before {
-    content: '';
+.intention-grade-b::after {
+    content: 'B';
     position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background: linear-gradient(
-        90deg,
-        rgba(255, 255, 255, 0.2) 0%,
-        rgba(255, 255, 255, 0) 50%,
-        rgba(255, 255, 255, 0.2) 100%
-    );
-    animation: shimmer 2s infinite;
-    z-index: 1;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(59, 130, 246, 0.08);
+    z-index: 0;
+    pointer-events: none;
 }
 
-@keyframes shimmer {
-    0% {
-        transform: translateX(-100%);
-    }
-    100% {
-        transform: translateX(100%);
-    }
+/* C 级 - 紫色 */
+.intention-grade-c {
+    background: linear-gradient(135deg, #e9d5ff 0%, #d8b4fe 50%, #e9d5ff 100%);
+    border-color: #a855f7;
+    color: #6b21a8;
+    text-shadow: 0 2px 4px rgba(107, 33, 168, 0.2);
 }
 
-.progress-text {
-    font-size: 13px;
-    font-weight: 700;
-    color: white;
-    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
-    z-index: 2;
-    font-variant-numeric: tabular-nums;
+.intention-grade-c::after {
+    content: 'C';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(168, 85, 247, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* D 级 - 橙色 */
+.intention-grade-d {
+    background: linear-gradient(135deg, #fed7aa 0%, #fdba74 50%, #fed7aa 100%);
+    border-color: #f97316;
+    color: #9a3412;
+    text-shadow: 0 2px 4px rgba(154, 52, 18, 0.2);
+}
+
+.intention-grade-d::after {
+    content: 'D';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(249, 115, 22, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* E 级 - 粉红色 */
+.intention-grade-e {
+    background: linear-gradient(135deg, #fbcfe8 0%, #f9a8d4 50%, #fbcfe8 100%);
+    border-color: #ec4899;
+    color: #9d174d;
+    text-shadow: 0 2px 4px rgba(157, 23, 77, 0.2);
+}
+
+.intention-grade-e::after {
+    content: 'E';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(236, 72, 153, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* F 级 - 红色,最低级别 */
+.intention-grade-f {
+    background: linear-gradient(135deg, #fecaca 0%, #fca5a5 50%, #fecaca 100%);
+    border-color: #ef4444;
+    color: #991b1b;
+    text-shadow: 0 2px 4px rgba(153, 27, 27, 0.2);
+}
+
+.intention-grade-f::after {
+    content: 'F';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(239, 68, 68, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+.intention-watermark span {
+    position: relative;
+    z-index: 1;
+}
+
+/* 暂无评级提示 */
+.no-intention-tip {
+    text-align: center;
+    padding: 18px 12px;
+    font-size: 16px;
+    color: #94a3b8;
+    font-style: italic;
+    background: linear-gradient(135deg, rgba(248, 250, 252, 0.5) 0%, rgba(241, 245, 249, 0.5) 100%);
+    border-radius: 10px;
+    border: 2px dashed #e2e8f0;
 }
 
 .card-header h3 i {

+ 12 - 7
src/views/crm/customer/index.vue

@@ -140,12 +140,16 @@
                 </el-select>
           </el-form-item>
           <el-form-item label="意向度">
-              <div class="time-range">
-                    <el-input-number v-model="queryParams.intentionDegreeGt" placeholder="最小值" size="small"
-                                     style="width: 130px" :min="0" :max="100"/>
-                    <span class="range-separator">-</span>
-                    <el-input-number v-model="queryParams.intentionDegreelt" placeholder="最大值" size="small"
-                                     style="width: 130px" :min="0" :max="100"/>
+              <div >
+                    <el-select style="width:220px" v-model="queryParams.intentionDegree" placeholder="请选择意向度" clearable size="small">
+                      <el-option label="A" value="A" />
+                      <el-option label="B" value="B" />
+                      <el-option label="C" value="C" />
+                      <el-option label="D" value="D" />
+                      <el-option label="E" value="E" />
+                      <el-option label="F" value="F" />
+                      
+                    </el-select>
                 </div>
           </el-form-item>
           <el-form-item>
@@ -266,7 +270,7 @@
           </el-table-column>
           <el-table-column label="意向度" align="center" prop="intentionDegree">
             <template slot-scope="scope">
-              {{ scope.row.intentionDegree != null ? scope.row.intentionDegree + '%' : '' }}
+              {{ scope.row.intentionDegree != null ? scope.row.intentionDegree  : '' }}
             </template>
           </el-table-column>
           <el-table-column label="备注" align="center" prop="remark" />
@@ -812,6 +816,7 @@ export default {
       this.queryParams.attritionLevel = null;
       this.queryParams.intentionDegreeGt = null;
       this.queryParams.intentionDegreelt = null;
+      this.queryParams.intentionDegree = null;
       this.handleQuery();
     },
     // 多选框选中数据

Разница между файлами не показана из-за своего большого размера
+ 1153 - 70
src/views/crm/customerAiChat/index.vue


+ 30 - 30
src/views/qw/contactWay/index.vue

@@ -194,7 +194,7 @@
         </el-form-item>
         <el-form-item label="客服类型" prop="userType" >
           <el-radio-group v-model="form.userType">
-             <el-radio :key="1":label="1" >全天在线</el-radio>
+<!--             <el-radio :key="1":label="1" >全天在线</el-radio>-->
              <el-radio :key="2":label="2" >自动上下线</el-radio>
              </el-radio-group>
         </el-form-item>
@@ -387,35 +387,35 @@
           <el-input v-model="form.description" type="textarea" placeholder="请输入描述" />
         </el-form-item>
 
-        <el-form-item label="开启欢迎语屏蔽"   prop="isCloseWelcome">
-          <el-switch
-            v-model="form.isCloseWelcome"
-            :active-value="1"
-            :inactive-value="0"
-            >
-          </el-switch>
-        </el-form-item>
-        <el-form-item label="欢迎语屏蔽" prop="closeWelcomeWord" v-if="form.isCloseWelcome==1">
-          <el-tag
-            v-for="tag in closeWelcomeWord"
-            closable
-            :disable-transitions="false"
-            @close="handleCloseWord(tag)">
-            {{tag}}
-          </el-tag>
-          <el-input
-            style="width:110px"
-            class="input-new-tag"
-            v-if="inputVisible"
-            v-model="inputValue"
-            ref="saveTagInput"
-            size="small"
-            @keyup.enter.native="handleInputConfirm"
-            @blur="handleInputConfirm"
-          >
-          </el-input>
-          <el-button v-else class="button-new-tag" size="small" style="width: 110px" @click="showInput">新增屏蔽关键词</el-button>
-        </el-form-item>
+<!--        <el-form-item label="开启欢迎语屏蔽"   prop="isCloseWelcome">-->
+<!--          <el-switch-->
+<!--            v-model="form.isCloseWelcome"-->
+<!--            :active-value="1"-->
+<!--            :inactive-value="0"-->
+<!--            >-->
+<!--          </el-switch>-->
+<!--        </el-form-item>-->
+<!--        <el-form-item label="欢迎语屏蔽" prop="closeWelcomeWord" v-if="form.isCloseWelcome==1">-->
+<!--          <el-tag-->
+<!--            v-for="tag in closeWelcomeWord"-->
+<!--            closable-->
+<!--            :disable-transitions="false"-->
+<!--            @close="handleCloseWord(tag)">-->
+<!--            {{tag}}-->
+<!--          </el-tag>-->
+<!--          <el-input-->
+<!--            style="width:110px"-->
+<!--            class="input-new-tag"-->
+<!--            v-if="inputVisible"-->
+<!--            v-model="inputValue"-->
+<!--            ref="saveTagInput"-->
+<!--            size="small"-->
+<!--            @keyup.enter.native="handleInputConfirm"-->
+<!--            @blur="handleInputConfirm"-->
+<!--          >-->
+<!--          </el-input>-->
+<!--          <el-button v-else class="button-new-tag" size="small" style="width: 110px" @click="showInput">新增屏蔽关键词</el-button>-->
+<!--        </el-form-item>-->
 
         <el-form-item label="添加标签"  prop="isTag">
           <el-switch

Некоторые файлы не были показаны из-за большого количества измененных файлов