lmx 5 日 前
コミット
783906f99e
39 ファイル変更8693 行追加122 行削除
  1. 15 0
      src/api/company/companyUser.js
  2. 63 0
      src/api/company/externalApi.js
  3. 240 0
      src/api/company/knowledge.js
  4. 90 0
      src/api/company/tagBinding.js
  5. 97 0
      src/api/company/workflowLobster.js
  6. 9 0
      src/api/fastGpt/fastGptChatSession.js
  7. 18 0
      src/api/qw/customerProperty.js
  8. 12 0
      src/api/qw/qwAnalyze.js
  9. 6 0
      src/api/qw/user.js
  10. 31 3
      src/router/index.js
  11. 4 4
      src/store/index.js
  12. 4 0
      src/utils/auth.js
  13. 8 2
      src/utils/liveWS.js
  14. 2 0
      src/utils/request.js
  15. 313 4
      src/views/company/companyUser/index.vue
  16. 242 0
      src/views/company/knowledge/audit.vue
  17. 556 0
      src/views/company/knowledge/base.vue
  18. 346 0
      src/views/company/knowledge/dataIndex.vue
  19. 310 0
      src/views/company/tag/binding.vue
  20. 419 0
      src/views/company/workflowExternalApi/index.vue
  21. 275 0
      src/views/company/workflowLobster/WorkflowSolutionEditor.vue
  22. 472 0
      src/views/company/workflowLobster/index.vue
  23. 1896 0
      src/views/company/workflowLobster/visual.vue
  24. 154 46
      src/views/crm/customer/customerDetail.vue
  25. 149 2
      src/views/crm/customer/index.vue
  26. 30 2
      src/views/fastGpt/fastGptChatSession/index.vue
  27. 9 3
      src/views/live/liveConfig/index.vue
  28. 9 3
      src/views/live/liveConsole/LiveConsole.vue
  29. 12 19
      src/views/login.vue
  30. 20 1
      src/views/member/list.vue
  31. 24 1
      src/views/member/mylist.vue
  32. 2305 0
      src/views/qw/externalContact/customerDetail.vue
  33. 130 4
      src/views/qw/externalContact/deptIndex.vue
  34. 245 13
      src/views/qw/externalContact/index.vue
  35. 141 2
      src/views/qw/externalContact/myExternalContact.vue
  36. 11 2
      src/views/qw/externalContactTransfer/index.vue
  37. 21 6
      src/views/qw/friendWelcome/indexNew.vue
  38. 3 3
      src/views/qw/sopTemp/index.vue
  39. 2 2
      src/views/qw/user/index.vue

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

@@ -375,5 +375,20 @@ export function myQwList(query) {
     url: '/company/user/myQwList',
     method: 'get',
     params: query
+    })
+}
+// 查询销售行为分析数据
+export function selectCompanyUserInfo(userId) {
+  return request({
+    url: '/company/user/selectCompanyUserInfo/' + userId,
+    method: 'get'
+  })
+}
+
+// 重新分析销售行为数据
+export function analyseCompanyUserInfo(userId) {
+  return request({
+    url: '/company/user/analyseCompanyUserInfo/' + userId,
+    method: 'get'
   })
 }

+ 63 - 0
src/api/company/externalApi.js

@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 分页查询外部接口配置
+export function pageExternalApi(query) {
+  return request({
+    url: '/companyWorkflow/externalApi/page',
+    method: 'get',
+    params: query
+  })
+}
+
+// 详情
+export function getExternalApi(id) {
+  return request({
+    url: '/companyWorkflow/externalApi/' + id,
+    method: 'get'
+  })
+}
+
+// 新增/编辑
+export function saveOrUpdateExternalApi(data) {
+  return request({
+    url: '/companyWorkflow/externalApi/saveOrUpdate',
+    method: 'post',
+    data
+  })
+}
+
+// 启停
+export function changeExternalApiStatus(id, status) {
+  return request({
+    url: '/companyWorkflow/externalApi/' + id + '/status',
+    method: 'post',
+    params: { status }
+  })
+}
+
+// 删除
+export function deleteExternalApi(id) {
+  return request({
+    url: '/companyWorkflow/externalApi/' + id,
+    method: 'delete'
+  })
+}
+
+// 测试接口
+export function testExternalApi(id, data) {
+  return request({
+    url: '/companyWorkflow/externalApi/' + id + '/test',
+    method: 'post',
+    data
+  })
+}
+
+// 调用日志分页
+export function pageExternalApiLogs(query) {
+  return request({
+    url: '/companyWorkflow/externalApi/logs/page',
+    method: 'get',
+    params: query
+  })
+}
+

+ 240 - 0
src/api/company/knowledge.js

@@ -0,0 +1,240 @@
+import request from '@/utils/request'
+
+// 知识库API
+export const knowledgeApi = {
+  // 获取知识库列表
+  getList: (params) => {
+    return request({
+      url: '/knowledge/base/list',
+      method: 'get',
+      params
+    })
+  },
+
+  // 搜索知识
+  search: (keyword, industryType) => {
+    return request({
+      url: '/knowledge/base/search',
+      method: 'get',
+      params: { keyword, industryType }
+    })
+  },
+
+  // 根据ID获取
+  getById: (id) => {
+    return request({
+      url: `/knowledge/base/${id}`,
+      method: 'get'
+    })
+  },
+
+  // 创建知识
+  create: (data) => {
+    return request({
+      url: '/knowledge/base',
+      method: 'post',
+      data
+    })
+  },
+
+  // 更新知识
+  update: (id, data) => {
+    return request({
+      url: `/knowledge/base/${id}`,
+      method: 'put',
+      data
+    })
+  },
+
+  // 删除知识
+  delete: (id) => {
+    return request({
+      url: `/knowledge/base/${id}`,
+      method: 'delete'
+    })
+  },
+
+  // 审核知识
+  audit: (id, auditStatus, comment) => {
+    return request({
+      url: `/knowledge/base/${id}/audit`,
+      method: 'post',
+      params: { auditStatus, comment }
+    })
+  },
+
+  // 同步到FastGPT
+  syncToFastGpt: (id) => {
+    return request({
+      url: `/knowledge/base/${id}/sync-fastgpt`,
+      method: 'post'
+    })
+  },
+
+  // 从聊天记录提取知识
+  extractFromChat: (chatRecordId, question, answer) => {
+    return request({
+      url: '/knowledge/base/extract-from-chat',
+      method: 'post',
+      params: { chatRecordId, question, answer }
+    })
+  },
+
+  // 双知识库校验
+  dualValidation: (query, fastgptResult) => {
+    return request({
+      url: '/knowledge/base/dual-validation',
+      method: 'post',
+      params: { query, fastgptResult }
+    })
+  },
+
+  addNewBase: (data) => {
+    return request({
+      url: '/ai/knowledge/base/add',
+      method: 'post',
+      data
+    })
+  },
+
+  updateBase: (id, data) => {
+    return request({
+      url: '/ai/knowledge/base',
+      method: 'put',
+      data
+    })
+  },
+
+  getknowledgeList: (params) => {
+    return request({
+      url: '/ai/knowledge/base/list',
+      method: 'get',
+      params
+    })
+  },
+
+  // 删除知识库
+  deleteBae: (id) => {
+    return request({
+      url: `/ai/knowledge/base/${id}`,
+      method: 'delete'
+    })
+  },
+
+  // 获取知识库数据列表
+  getDataList: (params) => {
+    return request({
+      url: '/knowledge/list',
+      method: 'get',
+      params
+    })
+  },
+
+  // 新增QA
+  addQa: (data) => {
+    return request({
+      url: '/knowledge/qa/add',
+      method: 'post',
+      data
+    })
+  },
+
+  // 删除知识库数据
+  deleteData: (id) => {
+    return request({
+      url: `/knowledge/${id}`,
+      method: 'delete'
+    })
+  },
+
+  // CSV文件上传导入问答对
+  uploadCsv: (file, baseId, collectionName, collectionId, onProgress) => {
+    const formData = new FormData()
+    formData.append('file', file)
+    formData.append('baseId', baseId)
+    if (collectionName) {
+      formData.append('collectionName', collectionName)
+    }
+    if (collectionId) {
+      formData.append('collectionId', collectionId)
+    }
+    return request({
+      url: '/knowledge/upload',
+      method: 'post',
+      data: formData,
+      headers: { 'Content-Type': 'multipart/form-data' },
+      onUploadProgress: onProgress
+    })
+  }
+}
+
+// 知识审核API
+export const knowledgeAuditApi = {
+  // 获取待审核列表
+  getPendingList: () => {
+    return request({
+      url: '/knowledge/audit/pending',
+      method: 'get'
+    })
+  },
+
+  // 获取已审核列表
+  getAuditedList: () => {
+    return request({
+      url: '/knowledge/audit/audited',
+      method: 'get'
+    })
+  },
+
+  // 获取优化建议列表
+  getSuggestionList: (params) => {
+    return request({
+      url: '/knowledge/suggestion/list',
+      method: 'get',
+      params
+    })
+  },
+
+  // 通过审核
+  approve: (id, comment) => {
+    return request({
+      url: `/knowledge/audit/${id}/approve`,
+      method: 'post',
+      params: { comment }
+    })
+  },
+
+  // 驳回审核
+  reject: (id, comment) => {
+    return request({
+      url: `/knowledge/audit/${id}/reject`,
+      method: 'post',
+      params: { comment }
+    })
+  },
+
+  // 批量审核
+  batchAudit: (data) => {
+    return request({
+      url: '/knowledge/audit/batch',
+      method: 'post',
+      data
+    })
+  },
+
+  // 应用优化建议
+  applySuggestion: (id) => {
+    return request({
+      url: `/knowledge/suggestion/${id}/apply`,
+      method: 'post'
+    })
+  },
+
+  // 忽略优化建议
+  ignoreSuggestion: (id) => {
+    return request({
+      url: `/knowledge/suggestion/${id}/ignore`,
+      method: 'post'
+    })
+  }
+}

+ 90 - 0
src/api/company/tagBinding.js

@@ -0,0 +1,90 @@
+import request from '@/utils/request'
+
+// 标签模板绑定API
+export const tagBindingApi = {
+  // 获取绑定列表
+  getList: (params) => {
+    return request({
+      url: '/workflow/tag-binding/list',
+      method: 'get',
+      params
+    })
+  },
+    getListByStatus: (params) => {
+    return request({
+      url: '/workflow/tag-binding/listByStatus',
+      method: 'get',
+      params
+    })
+  },
+
+  // 根据ID获取
+  getById: (id) => {
+    return request({
+      url: `/workflow/tag-binding/${id}`,
+      method: 'get'
+    })
+  },
+
+  // 创建绑定
+  create: (data) => {
+    return request({
+      url: '/workflow/tag-binding',
+      method: 'post',
+      data
+    })
+  },
+
+  // 更新绑定
+  update: (id, data) => {
+    return request({
+      url: `/workflow/tag-binding/${id}`,
+      method: 'put',
+      data
+    })
+  },
+
+  // 删除绑定
+  delete: (id) => {
+    return request({
+      url: `/workflow/tag-binding/${id}`,
+      method: 'delete'
+    })
+  },
+
+  // 批量绑定
+  batchBind: (templateId, tagCodes) => {
+    return request({
+      url: `/workflow/tag-binding/batch-bind/${templateId}`,
+      method: 'post',
+      data: tagCodes
+    })
+  },
+
+  // 根据用户标签匹配模板
+  matchTemplate: (userTags) => {
+    return request({
+      url: '/workflow/tag-binding/match-template',
+      method: 'post',
+      data: userTags
+    })
+  },
+
+  // 测试标签匹配
+  testMatch: (id, testTags) => {
+    return request({
+      url: `/workflow/tag-binding/${id}/test-match`,
+      method: 'post',
+      data: testTags
+    })
+  },
+
+  // 批量添加龙虾标签给客户
+  batchBindLobsterTag: (data) => {
+    return request({
+      url: '/workflow/tag-binding/batch-bind-lobster-tag',
+      method: 'post',
+      data
+    })
+  }
+}

+ 97 - 0
src/api/company/workflowLobster.js

@@ -0,0 +1,97 @@
+import request from '@/utils/request'
+
+export function getActiveExternalApis() {
+  return request({
+    url: '/companyWorkflow/externalApi/activeList',
+    method: 'get'
+  })
+}
+
+export function getActiveExternalApisLegacy() {
+  return request({
+    url: '/api/admin/external-api/active-list',
+    method: 'get'
+  })
+}
+
+export function listWorkflowTemplate(query) {
+  return request({
+    url: '/workflow/template/list',
+    method: 'get',
+    params: query
+  })
+}
+export function listWorkflowTemplateByStatus(query) {
+  return request({
+    url: '/workflow/template/listTemplate',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getWorkflowTemplateDetail(id) {
+  return request({
+    url: '/workflow/template/' + id,
+    method: 'get'
+  })
+}
+
+export function updateWorkflowTemplate(id, data) {
+  return request({
+    url: '/workflow/template/' + id,
+    method: 'put',
+    data
+  })
+}
+
+export function updateWorkflowTemplateStatus(id, status) {
+  return request({
+    url: '/workflow/template/' + id + '/' + status,
+    method: 'put'
+  })
+}
+
+export function saveWorkflowCanvas(id, data) {
+  return request({
+    url: '/workflow/canvas/' + id,
+    method: 'put',
+    data
+  })
+}
+
+export function deleteWorkflowTemplate(id) {
+  return request({
+    url: '/workflow/template/' + id,
+    method: 'delete'
+  })
+}
+
+export function aiGenerateWorkflow(data) {
+  return request({
+    url: '/workflow/ai-generator/generate',
+    method: 'post',
+    data
+  })
+}
+
+export function getGenerateResultDetail(recordId) {
+  return request({
+    url: '/workflow/ai-generator/result/' + recordId + '/detail',
+    method: 'get'
+  })
+}
+
+export function confirmGenerateResult(recordId) {
+  return request({
+    url: '/workflow/ai-generator/confirm/' + recordId,
+    method: 'post'
+  })
+}
+
+export function confirmGenerateResultEdited(recordId, data) {
+  return request({
+    url: '/workflow/ai-generator/confirm/' + recordId + '/edited',
+    method: 'post',
+    data
+  })
+}

+ 9 - 0
src/api/fastGpt/fastGptChatSession.js

@@ -35,6 +35,15 @@ export function updateFastGptChatSession(data) {
   })
 }
 
+//关闭客户ai回复
+export function updateFastGptChatSessionNew(data) {
+  return request({
+    url: '/fastGpt/fastGptChatSession/notReply',
+    method: 'get',
+    params: data
+  })
+}
+
 // 删除对话关系
 export function delFastGptChatSession(sessionId) {
   return request({

+ 18 - 0
src/api/qw/customerProperty.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+export function listByCustomerId(params) {
+  return request({
+    url: '/qw/customerProperty/list',
+    method: 'get',
+    params: params
+  })
+}
+// 调用ai分析标签
+export function analyzeAiTagByTrade(data) {
+  return request({
+    url: '/qw/customerProperty/analyzeAiTagByTrade',
+    method: 'post',
+    data: data
+  })
+}
+

+ 12 - 0
src/api/qw/qwAnalyze.js

@@ -0,0 +1,12 @@
+import request from '@/utils/request'
+
+// 查询客户聊天记录分析列表
+export function listAnalyze(query) {
+  return request({
+    url: '/qw/analyze/list',
+    method: 'get',
+    params: query
+  })
+}
+
+

+ 6 - 0
src/api/qw/user.js

@@ -98,6 +98,12 @@ export function addQwUser(id) {
     method: 'post',
   })
 }
+export function addQwSyncUser(id) {
+  return request({
+    url: '/qw/user/syncUser/' + id,
+    method: 'post',
+  })
+}
 export function addQwUserName(id) {
   return request({
     url: '/qw/user/syncName/' + id,

+ 31 - 3
src/router/index.js

@@ -93,6 +93,20 @@ export const constantRoutes = [
       }
     ]
   },
+  {
+    path: '/knowledge/data',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: 'index',
+        component: (resolve) => require(['@/views/company/knowledge/dataIndex'], resolve),
+        name: 'KnowledgeDataIndex',
+        meta: { title: '知识库数据管理', activeMenu: '/company/knowledge/base' }
+      }
+    ]
+  },
+
   {
     path: '/fastGpt/fastGptDataset/Collection',
     component: Layout,
@@ -271,7 +285,7 @@ export const constantRoutes = [
       isIndependentPage: true // 标记为“独立页”
     }
   },
-  
+
   {
   path: '/company/aiWorkflow',
   component: Layout,
@@ -364,7 +378,7 @@ export const constantRoutes = [
             }
         ]
     },
-    
+
 
     {
         path: '/crm/customer/detail/:customerId',
@@ -396,6 +410,7 @@ export const constantRoutes = [
         ]
     },
 
+
   {
     path: '/wxSop/sopUserLogsWx',
     component: Layout,
@@ -442,7 +457,20 @@ export const constantRoutes = [
         meta: { title: '我的音色任务', activeMenu: '/companyWx/companyVoiceRobotic' }
       }
     ]
-  }
+  },
+    {
+        path: '/workflow/visual/:id',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: '',
+                component: () => import('@/views/company/workflowLobster/visual.vue'),
+                name: 'WorkflowLobsterVisual',
+                meta: { title: '工作流可视化编辑', activeMenu: '/company/workflowLobster' }
+            }
+        ]
+    }
 
 ]
 

+ 4 - 4
src/store/index.js

@@ -34,15 +34,15 @@ const store = new Vuex.Store({
     }
   },
   actions: {
-    // 修改 action 以正确传递参数
-    initLiveWs({ commit, state }, { liveWsUrl, liveId, userId }) {
+    // 修改 action 以正确传递参数(含多租户 tenantCode)
+    initLiveWs({ commit, state }, { liveWsUrl, liveId, userId, tenantCode }) {
       // 如果已经存在对应 liveId 的连接,先关闭它
       if (state.liveWs[liveId]) {
         state.liveWs[liveId].close();
       }
 
-      // 创建新的 WebSocket 连接
-      const ws = new LiveWS(liveWsUrl, liveId, userId);
+      // 创建新的 WebSocket 连接(传入 tenantCode 进行多租户隔离)
+      const ws = new LiveWS(liveWsUrl, liveId, userId, tenantCode || undefined);
 
       // 提交到 mutation
       commit('setLiveWs', { ws, liveId });

+ 4 - 0
src/utils/auth.js

@@ -14,3 +14,7 @@ export function setToken(token) {
 export function removeToken() {
   return Cookies.remove(TokenKey)
 }
+
+export function getTenantCode() {
+  return Cookies.get("tenantCode")
+}

+ 8 - 2
src/utils/liveWS.js

@@ -5,19 +5,25 @@ export class LiveWS {
    * @param {string} url - WebSocket 服务器地址
    * @param {number} liveId - 直播间ID
    * @param {number} userId - 用户ID
+   * @param {string} tenantCode - 租户编码(多租户隔离)
    * @param {number} checkInterval - 检查连接状态的时间间隔,单位毫秒
    * @param {number} reconnectDelay - 连接断开后重连的延迟,单位毫秒
    */
-  constructor(url, liveId, userId, checkInterval = 5000, reconnectDelay = 3000) {
+  constructor(url, liveId, userId, tenantCode, checkInterval = 5000, reconnectDelay = 3000) {
     let timestamp = new Date().getTime()
     let userType = 1
     let signature = CryptoJS.HmacSHA256(
       CryptoJS.enc.Utf8.parse('' + liveId + userId + userType + timestamp),
       CryptoJS.enc.Utf8.parse(timestamp)).toString(CryptoJS.enc.Hex)
-    this.url = url + `?liveId=${liveId}&userId=${userId}&userType=${userType}&timestamp=${timestamp}&signature=${signature}`;
+    let query = `liveId=${liveId}&userId=${userId}&userType=${userType}&timestamp=${timestamp}&signature=${signature}`;
+    if (tenantCode && tenantCode !== 'undefined' && tenantCode !== 'null') {
+      query += `&tenantCode=${tenantCode}`;
+    }
+    this.url = url + '?' + query;
     console.log(this.url)
     this.liveId = liveId;
     this.userId = userId;
+    this.tenantCode = tenantCode;
     this.checkInterval = checkInterval;
     this.reconnectDelay = reconnectDelay;
     this.ws = null;

+ 2 - 0
src/utils/request.js

@@ -3,6 +3,7 @@ import { Notification, MessageBox, Message } from 'element-ui'
 import store from '@/store'
 import { getToken } from '@/utils/auth'
 import errorCode from '@/utils/errorCode'
+import {getTenantCode} from "./auth";
 
 axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
 // 创建axios实例
@@ -24,6 +25,7 @@ service.interceptors.request.use(config => {
   if (getToken() && !isToken) {
     config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
     config.headers['X-Frontend-Type'] = 'company'
+    config.headers['tenant-code'] = getTenantCode()
   }
   // get请求映射params参数
   if (config.method === 'get' && config.params) {

+ 313 - 4
src/views/company/companyUser/index.vue

@@ -67,6 +67,16 @@
              v-hasPermi="['qw:user:sync']"
            >同步企微员工和部门</el-button>
           </el-col>
+
+          <el-col :span="1.5">
+            <el-button
+              type="primary"
+              plain
+              size="mini"
+              @click="synOpenUser=true"
+              v-hasPermi="['qw:user:sync']"
+            >同步企微员工正确账户</el-button>
+          </el-col>
           <el-col :span="1.5">
             <el-button
               type="primary"
@@ -201,6 +211,31 @@
                 :type="scope.row.isAllowedAllRegister === 1 ? 'success' : 'info'">{{scope.row.isAllowedAllRegister === 1 ? '是' : '否' }}</el-tag>
             </template>
           </el-table-column>
+          <el-table-column label="销售行为分析等级" align="center" prop="analysisGrade" width="120px">
+            <template slot-scope="scope">
+              <el-tag
+                v-if="scope.row.analyseData.analysisGrade"
+                :type="getGradeType(scope.row.analyseData.analysisGrade)"
+                size="small">
+                {{ scope.row.analyseData.analysisGrade }}级
+              </el-tag>
+              <span v-else style="color: #909399;">未分析</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="销售行为分析数据" align="center" prop="analyseData" :show-overflow-tooltip="true" min-width="200">
+            <template slot-scope="scope">
+              <div v-if="scope.row.analyseData" style="text-align: left; font-size: 12px; line-height: 1.6;">
+                <div v-if="typeof scope.row.analyseData === 'string'" style="white-space: pre-wrap;">
+                  {{ formatTableAnalyseData(scope.row.analyseData) }}
+                </div>
+                <div v-else-if="scope.row.analyseData.overallInfo">
+                  {{ scope.row.analyseData.overallInfo | truncateText(50) }}
+                </div>
+                <span v-else style="color: #909399;">暂无详细数据</span>
+              </div>
+              <span v-else style="color: #909399;">暂无数据</span>
+            </template>
+          </el-table-column>
           <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
             <template slot-scope="scope">
               <el-button
@@ -270,6 +305,9 @@
                 :loading="bindCidServerLoading"
                 @click="handleBindCidServer(scope.row)"
               >绑定cid服务</el-button>
+
+              <el-button size="mini" type="text" icon="el-icon-data-analysis" @click="handleOpenSalesAnalysis(scope.row)">销售行为分析</el-button>
+
               <el-button size="mini" type="text" icon="el-icon-edit" @click="checkBindSipCallUser(scope.row)" v-if="scope.row.aiSipCallUserId==null">绑定sip角色</el-button>
               <el-button size="mini" type="text" icon="el-icon-search" @click="checkChangeSipCallUser(scope.row)" v-if="scope.row.aiSipCallUserId">修改sip角色</el-button>
 
@@ -348,6 +386,25 @@
         <el-button @click="synOpen=false">取 消</el-button>
       </div>
     </el-dialog>
+    <el-dialog title="选择企微主体" :visible.sync="synOpenUser" width="800px" append-to-body>
+      <el-form   label-width="80px">
+        <el-form-item label="企微公司" prop="corpId">
+          <el-select v-model="synform.corpId" placeholder="企微公司"  >
+            <el-option
+              v-for="dict in myQwCompanyList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="dict.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="synUserSubmitForm">确 定</el-button>
+        <el-button @click="synOpenUser=false">取 消</el-button>
+      </div>
+    </el-dialog>
+
 
     <el-dialog title="选择企微主体" :visible.sync="synNameOpen" width="800px" append-to-body>
       <el-form   label-width="80px">
@@ -703,7 +760,100 @@
 
 
     <ai-sip-call-user ref="aiSipCallUser" v-show="false" @refreshParentData="getList" />
-    
+    <!-- 销售行为分析抽屉 -->
+    <el-drawer
+      title="销售行为分析"
+      :visible.sync="drawerVisible"
+      direction="rtl"
+      size="60%"
+      :before-close="handleDrawerClose"
+    >
+      <template #title>
+        <div style="display: flex; align-items: center; justify-content: space-between; width: 100%; padding-right: 20px;">
+          <span style="font-size: 18px; font-weight: bold;">销售行为分析</span>
+          <el-button
+            type="primary"
+            size="small"
+            icon="el-icon-refresh"
+            :loading="refreshLoading"
+            @click="handleRefreshAnalysis"
+          >重新分析</el-button>
+        </div>
+      </template>
+      <div class="drawer-content" v-loading="drawerLoading">
+        <div v-if="analyseData" style="padding: 20px;">
+          <!-- 基本信息 -->
+          <el-card class="box-card" style="margin-bottom: 20px;">
+            <div slot="header" class="clearfix">
+              <span>员工基本信息</span>
+            </div>
+            <el-descriptions :column="2" border>
+              <el-descriptions-item label="员工姓名">{{ drawerData.nickName }}</el-descriptions-item>
+              <el-descriptions-item label="所属部门">{{ drawerData.deptName }}</el-descriptions-item>
+              <el-descriptions-item label="手机号码">{{ drawerData.phonenumber }}</el-descriptions-item>
+              <el-descriptions-item label="分析等级">
+                <el-tag
+                  :type="getGradeType(analyseData.analysisGrade)"
+                  size="medium">
+                  {{ analyseData.analysisGrade }}级
+                </el-tag>
+              </el-descriptions-item>
+            </el-descriptions>
+          </el-card>
+
+          <!-- 整体情况 -->
+          <el-card class="box-card" style="margin-bottom: 20px;" v-if="analyseData.overallInfo">
+            <div slot="header" class="clearfix">
+              <span><i class="el-icon-info"></i> 整体情况</span>
+            </div>
+            <div class="info-content">
+              {{ analyseData.overallInfo }}
+            </div>
+          </el-card>
+
+          <!-- 数据详情 -->
+          <el-card class="box-card" style="margin-bottom: 20px;" v-if="analyseData.dataInfo">
+            <div slot="header" class="clearfix">
+              <span><i class="el-icon-data-line"></i> 数据详情</span>
+            </div>
+            <div class="info-content">
+              {{ formatDataInfo(analyseData.dataInfo) }}
+            </div>
+          </el-card>
+
+          <!-- 服务链路分析 -->
+          <el-card class="box-card" style="margin-bottom: 20px;" v-if="analyseData.serviceInfo">
+            <div slot="header" class="clearfix">
+              <span><i class="el-icon-connection"></i> 服务链路分析</span>
+            </div>
+            <div class="info-content">
+              {{ analyseData.serviceInfo }}
+            </div>
+          </el-card>
+
+          <!-- 改进建议 -->
+          <el-card class="box-card" style="margin-bottom: 20px;" v-if="analyseData.suggestInfo">
+            <div slot="header" class="clearfix">
+              <span><i class="el-icon-s-promotion"></i> 改进建议</span>
+            </div>
+            <div class="suggestion-content">
+              {{ formatSuggestion(analyseData.suggestInfo) }}
+            </div>
+          </el-card>
+
+          <!-- 原始数据(可折叠) -->
+          <el-collapse v-model="activeCollapse">
+            <el-collapse-item title="查看原始JSON数据" name="1">
+              <pre class="json-viewer">{{ JSON.stringify(analyseData, null, 2) }}</pre>
+            </el-collapse-item>
+          </el-collapse>
+        </div>
+        <div v-else-if="!drawerLoading" style="text-align: center; padding: 50px; color: #909399;">
+          <i class="el-icon-document" style="font-size: 48px; margin-bottom: 10px;"></i>
+          <p>暂无销售行为分析数据</p>
+        </div>
+      </div>
+    </el-drawer>
   </div>
 </template>
 
@@ -725,7 +875,9 @@ import {
   isAllowedAllRegister, unBindDoctorId, bindDoctorId,updateBatchUserRoles,
   queryFsUserByPhone,
   batchBindCompanyUserId,
-  getBoundUsers
+  getBoundUsers,
+  selectCompanyUserInfo,
+  analyseCompanyUserInfo
 } from "@/api/company/companyUser";
 import { getToken } from "@/utils/auth";
 import { treeselect } from "@/api/company/companyDept";
@@ -733,7 +885,7 @@ import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import {bindQwUser, getQwUserList, addQwUser, getQwUser, getQwUserByIds,addQwUserName} from '@/api/qw/user';
 import { syncDept } from '@/api/qw/qwDept';
-import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+import {getMyQwUserList, getMyQwCompanyList, addQwSyncUser} from "@/api/qw/user";
 import  selectUser  from "@/views/company/components/selectQwUser.vue";
 import { getConfigByKey } from "@/api/company/companyConfig";
 import axios from "axios";
@@ -774,6 +926,7 @@ export default {
       isNeedRegisterMember: [],
       synform:{corpId:null},
       synOpen:false,
+      synOpenUser:false,
       synNameform:{corpId:null},
       synNameOpen:false,
       // 非单个禁用
@@ -950,6 +1103,13 @@ export default {
       selectedFsUserIds: [],
       boundUsersList: [],
       bindUserLoading: false,
+      // 销售行为分析抽屉相关
+      drawerVisible: false,
+      drawerLoading: false,
+      drawerData: {},
+      analyseData: null,
+      activeCollapse: [],
+      refreshLoading: false,
     };
   },
   watch: {
@@ -981,6 +1141,26 @@ export default {
     });
   },
   methods: {
+    /** 格式化建议信息 */
+    formatSuggestion(suggestInfo) {
+      if (!suggestInfo) return '';
+      // 将数字序号的内容分行显示
+      return suggestInfo.replace(/(\d+\.\s)/g, '\n$1').trim();
+    },
+    /** 格式化表格中的分析数据(简要展示) */
+    formatTableAnalyseData(analyseDataStr) {
+      if (!analyseDataStr) return '';
+      try {
+        const data = typeof analyseDataStr === 'string' ? JSON.parse(analyseDataStr) : analyseDataStr;
+        // 优先展示整体情况,截取前50个字符
+        if (data.overallInfo) {
+          return data.overallInfo.length > 50 ? data.overallInfo.substring(0, 50) + '...' : data.overallInfo;
+        }
+        return '已分析';
+      } catch (e) {
+        return '数据格式错误';
+      }
+    },
     onDomainBlur() {
       if (this.form.domain != null) {
         let value = this.form.domain.trim();
@@ -1099,6 +1279,21 @@ export default {
       qwList(this.addDateRange(this.queryParams, this.dateRange)).then(
         (response) => {
           this.userList = response.rows;
+          // 解析每个用户的 analyseData 字段
+          if (this.userList && this.userList.length > 0) {
+            this.userList.forEach(user => {
+              if (user.analyseData) {
+                try {
+                  user.analyseData = typeof user.analyseData === 'string'
+                    ? JSON.parse(user.analyseData)
+                    : user.analyseData;
+                } catch (e) {
+                  console.error('解析用户 analyseData 失败:', e);
+                  user.analyseData = null;
+                }
+              }
+            });
+          }
           this.total = response.total;
           this.loading = false;
           console.log(" this.userList ", this.userList)
@@ -1397,7 +1592,22 @@ export default {
         //loadingRock.close();
       });
     },
+    synUserSubmitForm() {
+      this.synOpen = false;
+      this.loading = true;
+
 
+      addQwSyncUser(this.synform.corpId).then(response => {
+        //this.msgSuccess("同步成功");
+        this.msgSuccess("正在同步中...");
+        this.getList();
+        this.synOpenUser = false;
+      }).finally(() => {
+        this.loading = false;
+        this.synOpenUser = false;
+        //loadingRock.close();
+      });
+    },
     synNameSubmitForm() {
       this.synNameOpen = false;
       this.loading = true;
@@ -1838,7 +2048,106 @@ export default {
     },
     checkChangeSipCallUser(row){
       this.$refs.aiSipCallUser.handleUpdateById(row.aiSipCallUserId);
-    }
+    },
+    /** 打开销售行为分析抽屉 */
+    handleOpenSalesAnalysis(row) {
+      this.drawerVisible = true;
+      this.drawerLoading = true;
+      this.drawerData = row;
+      this.analyseData = null;
+      this.activeCollapse = [];
+      // 调用接口获取销售行为分析数据
+      this.loadAnalysisData();
+    },
+    /** 重新分析销售行为数据 */
+    handleRefreshAnalysis() {
+      if (!this.drawerData.userId) {
+        this.$message.warning('用户信息不存在');
+        return;
+      }
+
+      this.$confirm('确认要重新分析该员工的销售行为数据吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.refreshLoading = true;
+
+        analyseCompanyUserInfo(this.drawerData.userId).then(response => {
+          if (response.code === 200) {
+            this.$message.success('分析成功');
+            // 重新获取最新数据
+            this.loadAnalysisData();
+          } else {
+            this.$message.error(response.msg || '分析失败');
+          }
+          this.refreshLoading = false;
+        }).catch(error => {
+          console.error('重新分析失败:', error);
+          this.$message.error('分析失败,请稍后重试');
+          this.refreshLoading = false;
+        });
+      }).catch(() => {
+        // 用户取消操作
+      });
+    },
+    /** 加载分析数据 */
+    loadAnalysisData() {
+      this.drawerLoading = true;
+      this.analyseData = null;
+
+      selectCompanyUserInfo(this.drawerData.userId).then(response => {
+        if (response.code === 200 && response.data) {
+          // 解析 analyseData 字段中的 JSON 信息
+          if (response.data.analyseData) {
+            try {
+              // 如果 analyseData 是字符串,则解析为 JSON 对象
+              this.analyseData = typeof response.data.analyseData === 'string'
+                ? JSON.parse(response.data.analyseData)
+                : response.data.analyseData;
+            } catch (e) {
+              console.error('解析 analyseData 失败:', e);
+              this.$message.error('数据解析失败');
+              this.analyseData = null;
+            }
+          } else {
+            this.$message.warning('返回数据中缺少 analyseData 字段');
+          }
+        } else {
+          this.$message.warning(response.msg || '暂无销售行为分析数据');
+        }
+        this.drawerLoading = false;
+      }).catch(error => {
+        console.error('获取销售行为分析数据失败:', error);
+        this.$message.error('获取数据失败');
+        this.drawerLoading = false;
+      });
+    },
+    /** 关闭抽屉 */
+    handleDrawerClose() {
+      this.drawerVisible = false;
+      this.drawerData = {};
+      this.analyseData = null;
+      this.activeCollapse = [];
+    },
+    /** 根据等级获取标签类型 */
+    getGradeType(grade) {
+      const gradeMap = {
+        'A': 'success',
+        'B': 'success',
+        'C': '',
+        'D': 'warning',
+        'E': 'danger'
+      };
+      return gradeMap[grade] || 'info';
+    },
+    /** 格式化数据详情 */
+    formatDataInfo(dataInfo) {
+      if (!dataInfo) return '';
+      // 将分号分隔的内容转换为换行显示
+      return dataInfo.split(/;\s*/).map(item => item.trim()).filter(item => item).join('\n');
+    },
+
   },
 }
 </script>

+ 242 - 0
src/views/company/knowledge/audit.vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="knowledge-audit">
+    <el-card>
+      <div slot="header" class="card-header">
+        <span>知识审核与优化</span>
+        <el-button type="primary" @click="handleBatchAudit" :disabled="!selectedRows.length">
+          <i class="el-icon-check"></i>批量审核
+        </el-button>
+      </div>
+
+      <el-alert
+        title="审核说明"
+        description="对客户聊天记录和AI生成的知识进行审核,确认后自动更新到知识库和工作流中,持续优化话术精准度。"
+        type="info"
+        show-icon
+        :closable="false"
+        style="margin-bottom: 20px"
+      />
+
+      <el-tabs v-model="activeTab" type="border-card">
+        <el-tab-pane label="待审核知识" name="pending">
+          <el-table :data="pendingData" v-loading="loading" border @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column prop="id" label="ID" width="80" />
+            <el-table-column prop="sourceType" label="来源类型" width="120">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.sourceType === 'chat' ? 'primary' : 'warning'">
+                  {{ row.sourceType === 'chat' ? '聊天记录' : 'AI生成' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="content" label="内容摘要" min-width="250" show-overflow-tooltip />
+            <el-table-column prop="suggestion" label="AI优化建议" min-width="200" show-overflow-tooltip />
+            <el-table-column prop="createTime" label="创建时间" width="180" />
+            <el-table-column label="操作" width="200" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button type="primary" size="mini" @click="handleView(row)">查看详情</el-button>
+                <el-button type="success" size="mini" @click="handleApprove(row)">通过</el-button>
+                <el-button type="danger" size="mini" @click="handleReject(row)">驳回</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+
+        <el-tab-pane label="已审核记录" name="audited">
+          <el-table :data="auditedData" v-loading="loading" border>
+            <el-table-column type="index" label="序号" width="60" align="center" />
+            <el-table-column prop="id" label="ID" width="80" />
+            <el-table-column prop="sourceType" label="来源类型" width="120">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.sourceType === 'chat' ? 'primary' : 'warning'">
+                  {{ row.sourceType === 'chat' ? '聊天记录' : 'AI生成' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="content" label="内容摘要" min-width="250" show-overflow-tooltip />
+            <el-table-column prop="auditResult" label="审核结果" width="100">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.auditResult === 'approve' ? 'success' : 'danger'">
+                  {{ row.auditResult === 'approve' ? '已通过' : '已驳回' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="auditor" label="审核人" width="120" />
+            <el-table-column prop="auditTime" label="审核时间" width="180" />
+          </el-table>
+        </el-tab-pane>
+
+        <el-tab-pane label="优化建议" name="suggestions">
+          <el-table :data="suggestionData" v-loading="loading" border>
+            <el-table-column type="index" label="序号" width="60" align="center" />
+            <el-table-column prop="type" label="优化类型" width="120">
+              <template slot-scope="{ row }">
+                <el-tag :type="getSuggestionType(row.type)">
+                  {{ getSuggestionText(row.type) }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="description" label="问题描述" min-width="250" />
+            <el-table-column prop="suggestion" label="优化建议" min-width="250" />
+            <el-table-column prop="impact" label="影响范围" width="120" />
+            <el-table-column label="操作" width="150" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button type="primary" size="mini" @click="handleApply(row)">应用</el-button>
+                <el-button type="info" size="mini" @click="handleIgnore(row)">忽略</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+
+    <!-- 查看详情对话框 -->
+    <el-dialog :visible.sync="detailDialogVisible" title="审核详情" width="700px">
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="来源类型">
+          <el-tag :type="currentItem.sourceType === 'chat' ? 'primary' : 'warning'">
+            {{ currentItem.sourceType === 'chat' ? '聊天记录' : 'AI生成' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="内容">{{ currentItem.content }}</el-descriptions-item>
+        <el-descriptions-item label="AI优化建议">{{ currentItem.suggestion }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ currentItem.createTime }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { knowledgeAuditApi } from '@/api/company/knowledge'
+
+export default {
+  name: 'KnowledgeAudit',
+  data() {
+    return {
+      loading: false,
+      activeTab: 'pending',
+      pendingData: [],
+      auditedData: [],
+      suggestionData: [],
+      selectedRows: [],
+      detailDialogVisible: false,
+      currentItem: {}
+    }
+  },
+  created() {
+    this.getPendingList()
+    this.getAuditedList()
+    this.getSuggestionList()
+  },
+  methods: {
+    async getPendingList() {
+      try {
+        const res = await knowledgeAuditApi.getPendingList()
+        this.pendingData = res.data || []
+      } catch (error) {
+        this.$message.error('获取待审核列表失败')
+      }
+    },
+    async getAuditedList() {
+      try {
+        const res = await knowledgeAuditApi.getAuditedList()
+        this.auditedData = res.data || []
+      } catch (error) {
+        this.$message.error('获取已审核列表失败')
+      }
+    },
+    async getSuggestionList() {
+      try {
+        const res = await knowledgeAuditApi.getSuggestionList({})
+        this.suggestionData = res.data || []
+      } catch (error) {
+        this.$message.error('获取优化建议失败')
+      }
+    },
+    handleSelectionChange(rows) {
+      this.selectedRows = rows
+    },
+    handleView(row) {
+      this.currentItem = row
+      this.detailDialogVisible = true
+    },
+    async handleApprove(row) {
+      try {
+        await knowledgeAuditApi.approve(row.id)
+        this.$message.success('审核通过')
+        this.getPendingList()
+      } catch (error) {
+        this.$message.error('审核失败')
+      }
+    },
+    async handleReject(row) {
+      try {
+        await knowledgeAuditApi.reject(row.id)
+        this.$message.success('已驳回')
+        this.getPendingList()
+      } catch (error) {
+        this.$message.error('操作失败')
+      }
+    },
+    async handleBatchAudit() {
+      try {
+        await this.$confirm('确定要批量通过选中的知识吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        const ids = this.selectedRows.map(row => row.id)
+        await knowledgeAuditApi.batchAudit({ ids, auditResult: 'approve' })
+        this.$message.success('批量审核成功')
+        this.getPendingList()
+      } catch (error) {
+        if (error !== 'cancel') {
+          this.$message.error('批量审核失败')
+        }
+      }
+    },
+    async handleApply(row) {
+      try {
+        await knowledgeAuditApi.applySuggestion(row.id)
+        this.$message.success('应用成功')
+        this.getSuggestionList()
+      } catch (error) {
+        this.$message.error('应用失败')
+      }
+    },
+    async handleIgnore(row) {
+      try {
+        await knowledgeAuditApi.ignoreSuggestion(row.id)
+        this.$message.success('已忽略')
+        this.getSuggestionList()
+      } catch (error) {
+        this.$message.error('操作失败')
+      }
+    },
+    getSuggestionText(type) {
+      const map = {
+        accuracy: '精准度',
+        completeness: '完整性',
+        clarity: '清晰度'
+      }
+      return map[type] || type
+    },
+    getSuggestionType(type) {
+      const map = {
+        accuracy: 'danger',
+        completeness: 'warning',
+        clarity: 'info'
+      }
+      return map[type] || 'info'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 556 - 0
src/views/company/knowledge/base.vue

@@ -0,0 +1,556 @@
+<template>
+  <div class="knowledge-base">
+    <el-card>
+      <div slot="header" class="card-header">
+        <span>独立知识库管理</span>
+        <div>
+          <el-button type="success" @click="handleSync" :disabled="!selectedRows.length">
+            <i class="el-icon-refresh"></i>同步到FastGPT
+          </el-button>
+<!--          <el-button type="primary" @click="handleAdd">-->
+<!--            <i class="el-icon-plus"></i>新增知识-->
+<!--          </el-button>-->
+          <el-button type="primary" @click="handleNew">
+            <i class="el-icon-plus"></i>新建
+          </el-button>
+        </div>
+      </div>
+
+      <el-alert
+        title="独立知识库说明"
+        description="独立知识库专门存储客户提出的问题及对应解答、AI与客户的聊天精华内容。通过与FastGPT双库协同校验,确保话术精准度。"
+        type="info"
+        show-icon
+        :closable="false"
+        style="margin-bottom: 20px"
+      />
+
+      <el-form :inline="true" :model="searchForm" class="search-form">
+<!--        <el-form-item label="关键词">-->
+<!--          <el-input v-model="searchForm.keyword" placeholder="请输入关键词" clearable />-->
+<!--        </el-form-item>-->
+
+        <el-form-item label="知识库名称">
+          <el-input v-model="searchForm.name" placeholder="请输入知识库名称" clearable/>
+        </el-form-item>
+
+        <el-form-item label="行业类型">
+          <el-select v-model="searchForm.industryType" placeholder="请选择" clearable style="width: 150px">
+            <el-option label="旅游" value="travel" />
+            <el-option label="教育" value="education" />
+            <el-option label="医疗" value="medical" />
+            <el-option label="装修" value="decoration" />
+            <el-option label="医美" value="medical_beauty" />
+            <el-option label="零售" value="retail" />
+            <el-option label="通用" value="general" />
+          </el-select>
+        </el-form-item>
+<!--        <el-form-item label="审核状态">-->
+<!--          <el-select v-model="searchForm.auditStatus" placeholder="请选择" clearable style="width: 150px">-->
+<!--            <el-option label="待审核" :value="0" />-->
+<!--            <el-option label="已通过" :value="1" />-->
+<!--            <el-option label="已驳回" :value="2" />-->
+<!--          </el-select>-->
+<!--        </el-form-item>-->
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">
+            <i class="el-icon-search"></i>查询
+          </el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table :data="tableData" v-loading="loading" border @selection-change="handleSelectionChange">
+        <!-- 知识库名称 -->
+        <el-table-column prop="name" label="知识库名称"  min-width="150" />
+        <!-- 描述 -->
+        <el-table-column prop="description" label="描述"  min-width="200" />
+        <!-- 行业类型 -->
+        <el-table-column prop="industryType" label="行业类型" width="100">
+          <template slot-scope="{ row }">
+            {{ getIndustryText(row.industryType) }}
+          </template>
+        </el-table-column>
+
+        <!-- 创建时间 -->
+        <el-table-column prop="createTime" label="创建时间" width="180" />
+
+        <!-- 操作 -->
+        <el-table-column label="操作" width="220" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="primary" size="mini" @click="handleView(row)">查看</el-button>
+            <el-button type="primary" size="mini" @click="handleEdit(row)">编辑</el-button>
+            <el-button type="danger" size="mini" @click="handleBaseDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination">
+        <el-pagination
+          :current-page="searchForm.pageNum"
+          :page-size="searchForm.pageSize"
+          :total="total"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="700px">
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+        <el-form-item label="知识标题" prop="title">
+          <el-input v-model="form.title" placeholder="请输入知识标题" />
+        </el-form-item>
+        <el-form-item label="问题" prop="question">
+          <el-input v-model="form.question" type="textarea" :rows="4" placeholder="请输入问题" />
+        </el-form-item>
+        <el-form-item label="答案" prop="answer">
+          <el-input v-model="form.answer" type="textarea" :rows="8" placeholder="请输入答案/解答" />
+        </el-form-item>
+        <el-form-item label="行业类型" prop="industryType">
+          <el-select v-model="form.industryType" placeholder="请选择行业类型" style="width: 100%">
+            <el-option label="旅游" value="travel" />
+            <el-option label="教育" value="education" />
+            <el-option label="医疗" value="medical" />
+            <el-option label="装修" value="decoration" />
+            <el-option label="医美" value="medical_beauty" />
+            <el-option label="零售" value="retail" />
+            <el-option label="通用" value="general" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 新增知识库-->
+    <el-dialog :title="dialogTitle" :visible.sync="newBaseVisible" width="700px">
+      <el-form :model="newBase" :rules="newBaseRules" ref="newBaseRef" label-width="100px">
+        <el-form-item label="知识库名" prop="name">
+          <el-input v-model="newBase.name" placeholder="请输入知识库名" />
+        </el-form-item>
+<!--        <el-form-item label="知识库头像" prop="avatar">-->
+<!--          <ImageUpload v-model="newBase.avatar"  type="image" :limit="1" :width="100" :height="100"  disabled/>-->
+<!--        </el-form-item>-->
+        <el-form-item label="介绍" prop="description">
+          <el-input type="textarea" :rows="3" v-model="newBase.description" placeholder="请输入介绍" />
+        </el-form-item>
+        <el-form-item label="行业类型" prop="industryType">
+          <el-select v-model="newBase.industryType" placeholder="请选择行业类型" style="width: 100%">
+            <el-option label="旅游" value="travel" />
+            <el-option label="教育" value="education" />
+            <el-option label="医疗" value="medical" />
+            <el-option label="装修" value="decoration" />
+            <el-option label="医美" value="medical_beauty" />
+            <el-option label="零售" value="retail" />
+            <el-option label="通用" value="general" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="newBaseVisible = false">取消</el-button>
+        <el-button type="primary" @click="handlenewBase">确定</el-button>
+      </div>
+    </el-dialog>
+
+
+    <!-- 查看详情对话框 -->
+    <el-dialog title="知识详情" :visible.sync="viewDialogVisible" width="700px">
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="知识标题">{{ currentItem.title }}</el-descriptions-item>
+        <el-descriptions-item label="问题">{{ currentItem.question }}</el-descriptions-item>
+        <el-descriptions-item label="答案">{{ currentItem.answer }}</el-descriptions-item>
+        <el-descriptions-item label="行业类型">{{ getIndustryText(currentItem.industryType) }}</el-descriptions-item>
+        <el-descriptions-item label="来源">{{ getSourceText(currentItem.source) }}</el-descriptions-item>
+        <el-descriptions-item label="审核状态">{{ getAuditStatusText(currentItem.auditStatus) }}</el-descriptions-item>
+        <el-descriptions-item label="使用次数">{{ currentItem.useCount }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ currentItem.createTime }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+
+    <!-- 审核对话框 -->
+    <el-dialog title="审核知识" :visible.sync="auditDialogVisible" width="500px">
+      <el-form :model="auditForm" label-width="100px">
+        <el-form-item label="审核结果">
+          <el-radio-group v-model="auditForm.auditStatus">
+            <el-radio :label="1">通过</el-radio>
+            <el-radio :label="2">驳回</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="审核意见">
+          <el-input v-model="auditForm.comment" type="textarea" :rows="4" placeholder="请输入审核意见" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="auditDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleAuditSubmit">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { knowledgeApi } from '@/api/company/knowledge'
+
+const getDefaultSearchForm = () => ({
+  keyword: '',
+  industryType: '',
+  auditStatus: null,
+  pageNum: 1,
+  pageSize: 10,
+  name: ''
+})
+
+export default {
+  name: 'KnowledgeBase',
+  data() {
+    return {
+      loading: false,
+      tableData: [],
+      total: 0,
+      selectedRows: [],
+      searchForm: getDefaultSearchForm(),
+      dialogVisible: false,
+      dialogTitle: '',
+      form: {
+        id: null,
+        title: '',
+        question: '',
+        answer: '',
+        industryType: 'general'
+      },
+      newBase: {
+        id: null,
+        name: '',
+        avatar: null,
+        description: '',
+        industryType: 'general'
+      },
+      newBaseVisible: false,
+      rules: {
+        title: [{ required: true, message: '请输入知识标题', trigger: 'blur' }],
+        question: [{ required: true, message: '请输入问题', trigger: 'blur' }],
+        answer: [{ required: true, message: '请输入答案', trigger: 'blur' }]
+      },
+      newBaseRules: {
+        name: [{ required: true, message: '请输入知识库名', trigger: 'blur' }],
+        // avatar: [{ required: true, message: '请上传知识库头像', trigger: 'blur' }],
+        description: [{ required: false, message: '请输入知识库介绍', trigger: 'blur' }]
+      },
+      viewDialogVisible: false,
+      currentItem: {},
+      auditDialogVisible: false,
+      auditForm: {
+        id: null,
+        auditStatus: 1,
+        comment: ''
+      }
+    }
+  },
+  created() {
+    this.getknowledgeList()
+  },
+  methods: {
+    // async getList() {
+    //   this.loading = true
+    //   try {
+    //     const res = await knowledgeApi.getList({
+    //       keyword: this.searchForm.keyword,
+    //       industryType: this.searchForm.industryType,
+    //       auditStatus: this.searchForm.auditStatus
+    //     })
+    //     this.tableData = res.data || []
+    //     this.page.total = this.tableData.length
+    //   } catch (error) {
+    //     this.$message.error('获取列表失败')
+    //   } finally {
+    //     this.loading = false
+    //   }
+    // },
+
+    async getknowledgeList() {
+      this.loading = true
+      try {
+        const res = await knowledgeApi.getknowledgeList(this.searchForm)
+        this.tableData = res.rows || []
+        this.total = res.total || 0
+      } catch (error) {
+        this.tableData = []
+        this.total = 0
+        this.$message.error('获取列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+
+    handleSearch() {
+      this.searchForm.pageNum = 1
+      this.getknowledgeList()
+    },
+    handleReset() {
+      this.searchForm = getDefaultSearchForm()
+      this.getknowledgeList()
+    },
+    handleSizeChange(size) {
+      this.searchForm.pageSize = size
+      this.searchForm.pageNum = 1
+      this.getknowledgeList()
+    },
+    handleCurrentChange(current) {
+      this.searchForm.pageNum = current
+      this.getknowledgeList()
+    },
+    handleSelectionChange(rows) {
+      this.selectedRows = rows
+    },
+    handleAdd() {
+      this.dialogTitle = '新增知识'
+      this.form = {
+        id: null,
+        title: '',
+        question: '',
+        answer: '',
+        industryType: 'general'
+      }
+      this.dialogVisible = true
+      this.$nextTick(() => {
+        this.$refs.formRef && this.$refs.formRef.resetFields()
+      })
+    },
+
+    handleNew() {
+      this.dialogTitle = '创建知识库'
+      this.newBase = {
+        id: null,
+        name: '',
+        avatar: null,
+        description: '',
+        industryType: 'general'
+      };
+      this.resetForm("newBaseRef");
+      this.newBaseVisible = true
+      this.$nextTick(() => {
+        this.$refs.newBaseRef && this.$refs.newBaseRef.resetFields()
+      })
+    },
+
+    handleEdit(row) {
+      this.dialogTitle = '编辑知识库';
+
+      // 先赋值
+      this.newBase = {
+        id: row.id,
+        name: row.name,
+        description: row.description,
+        industryType: row.industryType
+      };
+
+      // 重点:必须等 DOM 渲染完再打开弹窗
+      this.$nextTick(() => {
+        this.newBaseVisible = true;
+      });
+    },
+    handleView(row) {
+      this.$router.push({
+        path: '/knowledge/data/index',
+        query: {
+          baseId: row.id,
+          cId: row.collectionId,
+          cName: row.collectionName
+        }
+      })
+    },
+    handleAudit(row) {
+      this.auditForm = {
+        id: row.id,
+        auditStatus: 1,
+        comment: ''
+      }
+      this.auditDialogVisible = true
+    },
+    async handleAuditSubmit() {
+      try {
+        await knowledgeApi.audit(this.auditForm.id, this.auditForm.auditStatus, this.auditForm.comment)
+        this.$message.success('审核成功')
+        this.auditDialogVisible = false
+        this.getknowledgeList()
+      } catch (error) {
+        this.$message.error('审核失败')
+      }
+    },
+    async handleSubmit() {
+      try {
+        await this.$refs.formRef.validate()
+        if (this.form.id) {
+          await knowledgeApi.update(this.form.id, this.form)
+          this.$message.success('修改成功')
+        } else {
+          await knowledgeApi.create(this.form)
+          this.$message.success('新增成功')
+        }
+        this.dialogVisible = false
+        this.getknowledgeList()
+      } catch (error) {
+        if (error !== false) {
+          this.$message.error('操作失败')
+        }
+      }
+    },
+
+    async handlenewBase() {
+      try {
+        // 1. 校验表单
+        await this.$refs.newBaseRef.validate()
+
+        // 2. 判断是新增还是修改
+        if (this.newBase.id) {
+          // 修改 → 用 newBase.id
+          await knowledgeApi.updateBase(this.newBase.id, this.newBase)
+          this.$message.success('修改成功')
+        } else {
+          // 新增
+          await knowledgeApi.addNewBase(this.newBase)
+          this.$message.success('新增成功')
+        }
+
+        // 3. 关闭正确的弹窗
+        this.newBaseVisible = false
+
+        // 4. 刷新正确的列表
+        this.getknowledgeList()
+
+      } catch (error) {
+        // 只有真正报错才提示
+        if (error !== false) {
+          this.$message.error('操作失败')
+        }
+      }
+    },
+
+    async handleDelete(row) {
+      try {
+        await this.$confirm('确定要删除该知识吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        await knowledgeApi.delete(row.id)
+        this.$message.success('删除成功')
+        this.getknowledgeList()
+      } catch (error) {
+        if (error !== 'cancel') {
+          this.$message.error('删除失败')
+        }
+      }
+    },
+
+    async handleBaseDelete(row) {
+      try {
+        await this.$confirm('确定要删除该知识库吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+
+        // 调用删除接口
+        await knowledgeApi.deleteBae(row.id)
+
+        // ✅ 成功提示
+        this.$message.success('删除成功')
+
+        // ✅ 刷新列表(正确方法)
+        this.getknowledgeList()
+
+      } catch (error) {
+        // 取消不提示错误
+        if (error !== 'cancel') {
+          this.$message.error('删除失败')
+        }
+      }
+    },
+
+    async handleSync() {
+      try {
+        await this.$confirm('确定要同步选中的知识到FastGPT吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        for (const row of this.selectedRows) {
+          await knowledgeApi.syncToFastGpt(row.id)
+        }
+        this.$message.success('同步成功')
+        this.getknowledgeList()
+      } catch (error) {
+        if (error !== 'cancel') {
+          this.$message.error('同步失败')
+        }
+      }
+    },
+    getIndustryText(type) {
+      const map = {
+        travel: '旅游',
+        education: '教育',
+        medical: '医疗',
+        decoration: '装修',
+        medical_beauty: '医美',
+        retail: '零售',
+        general: '通用'
+      }
+      return map[type] || type
+    },
+    getSourceText(source) {
+      const map = {
+        manual: '手动录入',
+        chat: '聊天记录',
+        ai: 'AI生成'
+      }
+      return map[source] || source
+    },
+    getSourceType(source) {
+      const map = {
+        manual: 'info',
+        chat: 'primary',
+        ai: 'warning'
+      }
+      return map[source] || 'info'
+    },
+    getAuditStatusText(status) {
+      const map = {
+        0: '待审核',
+        1: '已通过',
+        2: '已驳回'
+      }
+      return map[status] || '未知'
+    },
+    getAuditStatusType(status) {
+      const map = {
+        0: 'warning',
+        1: 'success',
+        2: 'danger'
+      }
+      return map[status] || 'info'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.search-form {
+  margin-bottom: 20px;
+}
+.pagination {
+  margin-top: 20px;
+  text-align: right;
+}
+</style>

+ 346 - 0
src/views/company/knowledge/dataIndex.vue

@@ -0,0 +1,346 @@
+<template>
+  <div class="app-container knowledge-data-page">
+    <el-card shadow="never">
+      <div slot="header" class="clearfix">
+        <span>知识库数据管理</span>
+      </div>
+
+      <el-alert
+        title="支持上传 CSV 文件批量导入问答对和手动新增 QA 数据"
+        type="info"
+        :closable="false"
+        show-icon
+        style="margin-bottom: 16px"
+      />
+
+      <el-row :gutter="10" class="mb8">
+        <el-col :span="1.5">
+          <el-upload
+            ref="uploadRef"
+            :action="upload.url"
+            :headers="upload.headers"
+            :data="upload.data"
+            :show-file-list="false"
+            :before-upload="handleBeforeUpload"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :on-progress="handleUploadProgress"
+            :disabled="upload.isUploading || !baseId"
+            accept=".csv"
+          >
+            <el-button type="primary" icon="el-icon-upload2" :loading="upload.isUploading">
+              {{ upload.isUploading ? '上传中...' : '上传CSV' }}
+            </el-button>
+          </el-upload>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button type="success" icon="el-icon-plus" @click="handleOpenQaDialog">新增QA</el-button>
+        </el-col>
+      </el-row>
+
+      <el-table v-loading="loading" :data="tableData" border>
+        <el-table-column label="问题" min-width="220" show-overflow-tooltip prop="question"></el-table-column>
+
+        <el-table-column label="答案" min-width="300" show-overflow-tooltip prop="answer"></el-table-column>
+
+        <el-table-column label="来源" prop="source" width="120" align="center">
+          <template slot-scope="scope">
+            <el-tag :type="getSourceTagType(scope.row.source)" size="mini">
+              {{ getSourceText(scope.row.source) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="创建时间" prop="createTime" width="180" align="center" />
+
+        <el-table-column label="操作" width="140" align="center" fixed="right">
+          <template slot-scope="scope">
+            <el-button type="text" @click="handleEdit(scope.row)">修改</el-button>
+            <el-button type="text" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        :page.sync="queryParams.pageNum"
+        :limit.sync="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <el-dialog :title="qaDialogTitle" :visible.sync="qaDialogVisible" width="680px" append-to-body>
+      <el-form ref="qaFormRef" :model="qaForm" :rules="qaRules" label-width="80px">
+        <el-form-item label="问题" prop="question">
+          <el-input v-model="qaForm.question" type="textarea" :rows="4" placeholder="请输入问题" maxlength="500" show-word-limit />
+        </el-form-item>
+        <el-form-item label="答案" prop="answer">
+          <el-input v-model="qaForm.answer" type="textarea" :rows="8" placeholder="请输入答案" maxlength="3000" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="qaDialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="submitQa">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getToken, getTenantCode } from '@/utils/auth'
+import { knowledgeApi } from '@/api/company/knowledge'
+
+export default {
+  name: 'KnowledgeDataIndex',
+  data() {
+    return {
+      loading: false,
+      submitLoading: false,
+      qaDialogVisible: false,
+      qaDialogTitle: '新增QA',
+      baseId: '',
+      cId: '',
+      cName: '',
+      tableData: [],
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        baseId: undefined
+      },
+      upload: {
+        isUploading: false,
+        url: process.env.VUE_APP_BASE_API + '/knowledge/upload',
+        headers: {
+          Authorization: 'Bearer ' + getToken(),
+          'tenant-code': getTenantCode()
+        },
+        data: {
+          baseId: undefined,
+          collectionName: undefined,
+          collectionId: undefined
+        }
+      },
+      qaForm: {
+        id: undefined,
+        question: '',
+        answer: ''
+      },
+      qaRules: {
+        question: [
+          { required: true, message: '请输入问题', trigger: 'blur' }
+        ],
+        answer: [
+          { required: true, message: '请输入答案', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  created() {
+    this.initBaseId()
+  },
+  methods: {
+    initBaseId() {
+      this.baseId = this.$route.query.baseId
+      this.cId = this.$route.query.cId
+      this.cName = this.$route.query.cName
+      if (!this.baseId) {
+        this.$message.error('缺少知识库ID参数')
+        return
+      }
+      this.queryParams.baseId = this.baseId
+      this.upload.data.baseId = this.baseId
+      this.upload.data.collectionName = this.cName
+      this.upload.data.collectionId = this.cId
+      this.getList()
+    },
+    async getList() {
+      if (!this.baseId) {
+        return
+      }
+      this.loading = true
+      try {
+        const res = await knowledgeApi.getList(this.queryParams)
+        this.tableData = this.getRows(res)
+        this.total = this.getTotal(res, this.tableData)
+      } catch (error) {
+        this.tableData = []
+        this.total = 0
+        this.$message.error('获取数据列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    getRows(res) {
+      if (Array.isArray(res.rows)) {
+        return res.rows
+      }
+      if (Array.isArray(res.data)) {
+        return res.data
+      }
+      if (Array.isArray(res)) {
+        return res
+      }
+      return []
+    },
+    getTotal(res, rows) {
+      if (typeof res.total === 'number') {
+        return res.total
+      }
+      return rows.length
+    },
+    getTypeText(type) {
+      if (type === 'qa') {
+        return 'QA'
+      }
+      if (type === 'file') {
+        return '文件'
+      }
+      return type || '-'
+    },
+    getName(row) {
+      return row.name || row.fileName || row.question || '-'
+    },
+    getContent(row) {
+      if (row.type === 'qa') {
+        return row.content || row.answer || '-'
+      }
+      return '-'
+    },
+    getSourceText(source) {
+      const sourceMap = {
+        manual: '手动录入',
+        chat: '聊天记录',
+        ai: 'AI生成'
+      }
+      return sourceMap[source] || source || '-'
+    },
+    getSourceTagType(source) {
+      const tagTypeMap = {
+        manual: 'warning',
+        chat: 'success',
+        ai: 'info'
+      }
+      return tagTypeMap[source] || ''
+    },
+    handleBeforeUpload() {
+      if (!this.baseId) {
+        this.$message.error('缺少知识库ID,无法上传')
+        return false
+      }
+      return true
+    },
+    handleUploadProgress() {
+      this.upload.isUploading = true
+    },
+    handleUploadSuccess(response) {
+      this.upload.isUploading = false
+      if (response && (response.code === 200 || response.success)) {
+        const data = response.data || {}
+        const msg = data.success
+          ? `导入完成:共 ${data.total || 0} 条,成功 ${data.success || 0} 条`
+          : (response.msg || '上传成功')
+        this.$message.success(msg)
+        this.getList()
+        return
+      }
+      this.$message.error((response && response.msg) || '上传失败')
+    },
+    handleUploadError(err) {
+      this.upload.isUploading = false
+      try {
+        const errorMsg = JSON.parse(err.message || '{}')
+        this.$message.error(errorMsg.msg || '上传失败,请检查文件格式')
+      } catch {
+        this.$message.error('上传失败,请检查文件格式')      
+      }
+    },
+    resetQaForm() {
+      this.qaForm = {
+        id: undefined,
+        question: '',
+        answer: ''
+      }
+    },
+    handleOpenQaDialog() {
+      this.qaDialogTitle = '新增QA'
+      this.qaDialogVisible = true
+      this.resetQaForm()
+      this.$nextTick(() => {
+        if (this.$refs.qaFormRef) {
+          this.$refs.qaFormRef.resetFields()
+        }
+      })
+    },
+    handleEdit(row) {
+      this.qaDialogTitle = '修改QA'
+      this.qaDialogVisible = true
+      this.qaForm = {
+        id: row.id,
+        question: row.question || row.name || '',
+        answer: row.answer || row.content || ''
+      }
+      this.$nextTick(() => {
+        if (this.$refs.qaFormRef) {
+          this.$refs.qaFormRef.clearValidate()
+        }
+      })
+    },
+    submitQa() {
+      if (!this.baseId) {
+        this.$message.error('缺少知识库ID,无法新增QA')
+        return
+      }
+      this.$refs.qaFormRef.validate(async valid => {
+        if (!valid) {
+          return
+        }
+        this.submitLoading = true
+        try {
+          const params = {
+            baseId: this.baseId,
+            collectionId: this.cId,
+            collectionName: this.cName,
+            question: this.qaForm.question,
+            answer: this.qaForm.answer
+          }
+          if (this.qaForm.id) {
+            await knowledgeApi.update(this.qaForm.id, params)
+            this.$message.success('修改成功')
+          } else {
+            await knowledgeApi.create(params)
+            this.$message.success('新增成功')
+          }
+          this.qaDialogVisible = false
+          this.getList()
+        } catch (error) {
+          this.$message.error(this.qaForm.id ? '修改QA失败' : '新增QA失败')
+        } finally {
+          this.submitLoading = false
+        }
+      })
+    },
+    handleDelete(row) {
+      this.$confirm('是否确认删除该条数据?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        await knowledgeApi.delete(row.id)
+        this.$message.success('删除成功')
+        if (this.tableData.length === 1 && this.queryParams.pageNum > 1) {
+          this.queryParams.pageNum -= 1
+        }
+        this.getList()
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.knowledge-data-page .mb8 {
+  margin-bottom: 16px;
+}
+</style>

+ 310 - 0
src/views/company/tag/binding.vue

@@ -0,0 +1,310 @@
+<template>
+  <div class="tag-binding">
+    <el-card>
+      <div slot="header" class="card-header">
+        <span>标签-模板绑定管理</span>
+        <el-button type="primary" @click="handleAdd">
+          <i class="el-icon-plus"></i>新增绑定
+        </el-button>
+      </div>
+
+      <el-alert
+        title="标签绑定说明"
+        description="通过配置用户标签与工作流模板的绑定关系,系统可以根据用户标签自动匹配最合适的工作流模板进行跟进。"
+        type="info"
+        show-icon
+        :closable="false"
+        style="margin-bottom: 20px"
+      />
+
+      <el-form :inline="true" :model="searchForm" class="search-form">
+        <el-form-item label="标签编码">
+          <el-input v-model="searchForm.tagCode" placeholder="请输入" clearable />
+        </el-form-item>
+        <el-form-item label="模板">
+          <el-select v-model="searchForm.templateId" placeholder="请选择" clearable style="width: 200px">
+            <el-option v-for="t in templateOptions" :key="t.id" :label="t.templateName" :value="t.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">
+            <i class="el-icon-search"></i>查询
+          </el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table :data="tableData" v-loading="loading" border>
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column prop="tagCode" label="标签编码" width="150" />
+        <el-table-column prop="tagName" label="标签名称" width="150" />
+        <el-table-column prop="templateName" label="绑定模板" min-width="200" />
+        <el-table-column prop="priority" label="优先级" width="100" align="center">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.priority >= 50 ? 'danger' : row.priority >= 30 ? 'warning' : 'info'">
+              {{ row.priority }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="matchCondition" label="匹配条件" min-width="200" show-overflow-tooltip>
+          <template slot-scope="{ row }">
+            <span v-if="row.matchCondition">{{ formatCondition(row.matchCondition) }}</span>
+            <span v-else style="color: #909399">无特殊条件</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" width="100" align="center">
+          <template slot-scope="{ row }">
+            <el-switch v-model="row.status" :active-value="1" :inactive-value="0" @change="handleStatusChange(row)" />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="250" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="primary" size="mini" @click="handleEdit(row)">编辑</el-button>
+            <el-button type="success" size="mini" @click="handleTestMatch(row)">测试匹配</el-button>
+            <el-button type="danger" size="mini" @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px">
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+        <el-form-item label="标签编码" prop="tagCode">
+          <el-input v-model="form.tagCode" placeholder="请输入标签编码,如:VIP_USER" />
+        </el-form-item>
+        <el-form-item label="标签名称" prop="tagName">
+          <el-input v-model="form.tagName" placeholder="请输入标签名称" />
+        </el-form-item>
+        <el-form-item label="绑定模板" prop="templateId">
+          <el-select v-model="form.templateId" placeholder="请选择工作流模板" style="width: 100%">
+            <el-option v-for="t in templateOptions" :key="t.id" :label="t.templateName" :value="t.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="优先级">
+          <el-slider v-model="form.priority" :max="100" show-input />
+        </el-form-item>
+        <el-form-item label="匹配条件">
+          <el-input v-model="form.matchCondition" type="textarea" :rows="3" placeholder="请输入JSON格式的匹配条件" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 测试匹配对话框 -->
+    <el-dialog :visible.sync="testDialogVisible" title="测试标签匹配" width="500px">
+      <el-form :model="testForm" label-width="100px">
+        <el-form-item label="测试标签">
+          <el-input v-model="testForm.tags" type="textarea" :rows="4" placeholder="请输入标签编码,多个标签用逗号分隔" />
+        </el-form-item>
+      </el-form>
+      <div v-if="testResult" style="margin-top: 20px">
+        <el-alert :title="testResult.matched ? '匹配成功' : '未匹配'" :type="testResult.matched ? 'success' : 'warning'" :closable="false">
+          <div v-if="testResult.matched">
+            <p>模板:{{ testResult.templateName }}</p>
+            <p>优先级:{{ testResult.priority }}</p>
+          </div>
+          <div v-else>
+            <p>{{ testResult.message }}</p>
+          </div>
+        </el-alert>
+      </div>
+      <div slot="footer">
+        <el-button @click="testDialogVisible = false">关闭</el-button>
+        <el-button type="primary" @click="handleTestMatchSubmit">测试</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { tagBindingApi } from '@/api/company/tagBinding'
+import { listWorkflowTemplateByStatus } from '@/api/company/workflowLobster'
+
+export default {
+  name: 'TagBinding',
+  data() {
+    return {
+      loading: false,
+      tableData: [],
+      searchForm: {
+        tagCode: '',
+        templateId: null
+      },
+      templateOptions: [],
+      dialogVisible: false,
+      dialogTitle: '',
+      form: {
+        id: null,
+        tagCode: '',
+        tagName: '',
+        templateId: null,
+        priority: 0,
+        matchCondition: '',
+        templateName: ''
+
+      },
+      rules: {
+        tagCode: [{ required: true, message: '请输入标签编码', trigger: 'blur' }],
+        tagName: [{ required: true, message: '请输入标签名称', trigger: 'blur' }],
+        templateId: [{ required: true, message: '请选择模板', trigger: 'change' }]
+      },
+      testDialogVisible: false,
+      testForm: {
+        id: null,
+        tags: ''
+      },
+      testResult: null
+    }
+  },
+  created() {
+    this.getList()
+    this.loadTemplateOptions()
+  },
+  methods: {
+    async getList() {
+      this.loading = true
+      try {
+        const res = await tagBindingApi.getList(this.searchForm)
+        this.tableData = res.data || []
+      } catch (error) {
+        this.$message.error('获取列表失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadTemplateOptions() {
+      try {
+        const res = await listWorkflowTemplateByStatus({ status: 1 })
+        this.templateOptions = res.data || []
+      } catch (error) {
+        this.templateOptions = []
+        this.$message.error('获取模板列表失败')
+      }
+    },
+    handleSearch() {
+      this.getList()
+    },
+    handleReset() {
+      this.searchForm = {
+        tagCode: '',
+        templateId: null
+      }
+      this.handleSearch()
+    },
+    handleAdd() {
+      this.dialogTitle = '新增绑定'
+      this.form = {
+        id: null,
+        tagCode: '',
+        tagName: '',
+        templateId: null,
+        templateName: '',
+        priority: 0,
+        matchCondition: ''
+      }
+      this.dialogVisible = true
+      this.$nextTick(() => {
+        this.$refs.formRef && this.$refs.formRef.resetFields()
+      })
+    },
+    handleEdit(row) {
+      this.dialogTitle = '编辑绑定'
+      this.form = {
+        id: row.id,
+        tagCode: row.tagCode,
+        tagName: row.tagName,
+        templateId: row.templateId,
+        templateName: row.templateName || '',
+        priority: row.priority,
+        matchCondition: row.matchCondition
+      }
+      this.dialogVisible = true
+    },
+    async handleSubmit() {
+      try {
+        await this.$refs.formRef.validate()
+        const selectedTemplate = this.templateOptions.find(item => item.id === this.form.templateId)
+        this.form.templateName = selectedTemplate ? selectedTemplate.templateName : ''
+        if (this.form.id) {
+          await tagBindingApi.update(this.form.id, this.form)
+          this.$message.success('修改成功')
+        } else {
+          await tagBindingApi.create(this.form)
+          this.$message.success('新增成功')
+        }
+        this.dialogVisible = false
+        this.getList()
+      } catch (error) {
+        if (error !== false) {
+          this.$message.error('操作失败')
+        }
+      }
+    },
+    async handleDelete(row) {
+      try {
+        await this.$confirm('确定要删除该绑定关系吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        await tagBindingApi.delete(row.id)
+        this.$message.success('删除成功')
+        this.getList()
+      } catch (error) {
+        if (error !== 'cancel') {
+          this.$message.error('删除失败')
+        }
+      }
+    },
+    async handleStatusChange(row) {
+      try {
+        await tagBindingApi.update(row.id, { status: row.status })
+        this.$message.success('状态更新成功')
+      } catch (error) {
+        this.$message.error('状态更新失败')
+        this.getList()
+      }
+    },
+    handleTestMatch(row) {
+      this.testForm = {
+        id: row.id,
+        tags: row.tagCode
+      }
+      this.testResult = null
+      this.testDialogVisible = true
+    },
+    async handleTestMatchSubmit() {
+      try {
+        const tags = this.testForm.tags.split(',').map(t => t.trim()).filter(t => t)
+        const res = await tagBindingApi.testMatch(this.testForm.id, { tags })
+        this.testResult = res.data
+      } catch (error) {
+        this.$message.error('测试失败')
+      }
+    },
+    formatCondition(condition) {
+      try {
+        const obj = JSON.parse(condition)
+        return Object.entries(obj).map(([k, v]) => `${k}: ${v}`).join(', ')
+      } catch {
+        return condition
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.search-form {
+  margin-bottom: 20px;
+}
+</style>

+ 419 - 0
src/views/company/workflowExternalApi/index.vue

@@ -0,0 +1,419 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never" class="mb8">
+      <div slot="header">
+        <span>外部接口管理配置</span>
+      </div>
+
+      <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="70px">
+        <el-form-item label="接口类型">
+          <el-input v-model="queryParams.apiType" placeholder="请输入" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="接口名称">
+          <el-input v-model="queryParams.apiName" placeholder="请输入" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="接口编码">
+          <el-input v-model="queryParams.apiCode" placeholder="请输入" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 120px">
+            <el-option label="启用" :value="1" />
+            <el-option label="停用" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增接口</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table v-loading="loading" :data="list" border>
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column prop="apiName" label="接口名称" min-width="160" show-overflow-tooltip />
+      <el-table-column prop="apiCode" label="接口编码" min-width="160" show-overflow-tooltip />
+      <el-table-column prop="apiType" label="接口类型" width="120" />
+      <el-table-column prop="apiUrl" label="请求地址" min-width="260" show-overflow-tooltip />
+      <el-table-column prop="httpMethod" label="请求方式" width="110" />
+      <el-table-column label="限频规则" width="140">
+        <template slot-scope="scope">
+          <span v-if="scope.row.rateMaxCount && scope.row.rateWindowSeconds">
+            {{ scope.row.rateMaxCount }}次/{{ formatWindow(scope.row.rateWindowSeconds) }}
+          </span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="priority" label="优先级" width="90" />
+      <el-table-column label="状态" width="110">
+        <template slot-scope="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="1"
+            :inactive-value="0"
+            @change="val => handleStatusChange(scope.row, val)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="260" fixed="right">
+        <template slot-scope="scope">
+          <el-button type="text" size="mini" @click="handleTest(scope.row)">测试</el-button>
+          <el-button type="text" size="mini" @click="handleEdit(scope.row)">编辑</el-button>
+          <el-button type="text" size="mini" @click="handleLogs(scope.row)">日志</el-button>
+          <el-button type="text" size="mini" style="color:#F56C6C" @click="handleDelete(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 新增/编辑 -->
+    <el-dialog :title="form.id ? '编辑接口' : '新增接口'" :visible.sync="openForm" width="720px" append-to-body>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
+        <el-row :gutter="12">
+          <el-col :span="12">
+            <el-form-item label="接口类型" prop="apiType">
+              <el-input v-model="form.apiType" placeholder="例如 WEATHER" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="接口名称" prop="apiName">
+              <el-input v-model="form.apiName" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="接口编码" prop="apiCode">
+              <el-input v-model="form.apiCode" :disabled="!!form.id" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="请求方式" prop="httpMethod">
+              <el-select v-model="form.httpMethod" placeholder="请选择" style="width:100%">
+                <el-option label="GET" value="GET" />
+                <el-option label="POST" value="POST" />
+                <el-option label="PUT" value="PUT" />
+                <el-option label="DELETE" value="DELETE" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="请求地址" prop="apiUrl">
+              <el-input v-model="form.apiUrl" placeholder="https://..." />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="限频窗口(秒)">
+              <el-input-number v-model="form.rateWindowSeconds" :min="1" :step="1" style="width:100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="窗口最大次数">
+              <el-input-number v-model="form.rateMaxCount" :min="1" :step="1" style="width:100%" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="优先级">
+              <el-input-number v-model="form.priority" :min="0" :step="1" style="width:100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态">
+              <el-select v-model="form.status" style="width:100%">
+                <el-option label="启用" :value="1" />
+                <el-option label="停用" :value="0" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="超时(ms)">
+              <el-input-number v-model="form.timeoutMs" :min="1000" :step="500" style="width:100%" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24">
+            <el-form-item label="默认请求头(JSON)">
+              <el-input v-model="form.defaultHeadersJson" type="textarea" :rows="3" placeholder='{"Content-Type":"application/json"}' />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="默认请求体(JSON)">
+              <el-input v-model="form.defaultBodyJson" type="textarea" :rows="4" placeholder='{"k":"v"}' />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="openForm = false">取消</el-button>
+        <el-button type="primary" @click="submitForm">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 测试 -->
+    <el-dialog title="测试接口" :visible.sync="openTest" width="760px" append-to-body>
+      <el-form :model="testForm" label-width="130px">
+        <el-form-item label="覆盖请求头(JSON)">
+          <el-input v-model="testForm.headersJson" type="textarea" :rows="3" />
+        </el-form-item>
+        <el-form-item label="覆盖请求体(JSON)">
+          <el-input v-model="testForm.bodyJson" type="textarea" :rows="4" />
+        </el-form-item>
+        <el-form-item label="覆盖超时(ms)">
+          <el-input-number v-model="testForm.timeoutMs" :min="1000" :step="500" />
+        </el-form-item>
+      </el-form>
+
+      <el-divider content-position="left">响应</el-divider>
+      <el-descriptions :column="2" border size="small">
+        <el-descriptions-item label="状态码">{{ testResult.responseStatus || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="耗时(ms)">{{ testResult.durationMs || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="是否成功">{{ testResult.success === true ? '是' : (testResult.success === false ? '否' : '-') }}</el-descriptions-item>
+        <el-descriptions-item label="错误信息">{{ testResult.errorMessage || '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <el-input v-model="testResult.responseBody" type="textarea" :rows="10" readonly style="margin-top: 10px" />
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="openTest = false">关闭</el-button>
+        <el-button type="primary" :loading="testLoading" @click="doTest">开始测试</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 日志 -->
+    <el-dialog title="调用日志" :visible.sync="openLogs" width="980px" append-to-body>
+      <el-table v-loading="logsLoading" :data="logs" border height="520px">
+        <el-table-column type="index" label="序号" width="60" />
+        <el-table-column prop="createTime" label="时间" width="170" />
+        <el-table-column prop="httpMethod" label="方法" width="90" />
+        <el-table-column prop="requestUrl" label="URL" min-width="260" show-overflow-tooltip />
+        <el-table-column prop="responseStatus" label="状态码" width="90" />
+        <el-table-column prop="durationMs" label="耗时(ms)" width="100" />
+        <el-table-column prop="success" label="成功" width="80">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.success === 1" type="success">成功</el-tag>
+            <el-tag v-else type="danger">失败</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="errorMessage" label="错误信息" min-width="160" show-overflow-tooltip />
+      </el-table>
+
+      <pagination
+        v-show="logsTotal > 0"
+        :total="logsTotal"
+        :page.sync="logsQuery.pageNum"
+        :limit.sync="logsQuery.pageSize"
+        @pagination="getLogs"
+      />
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="openLogs = false">关闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  pageExternalApi,
+  getExternalApi,
+  saveOrUpdateExternalApi,
+  changeExternalApiStatus,
+  deleteExternalApi,
+  testExternalApi,
+  pageExternalApiLogs
+} from '@/api/company/externalApi'
+
+export default {
+  name: 'CompanyWorkflowExternalApi',
+  data() {
+    return {
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        apiType: undefined,
+        apiName: undefined,
+        apiCode: undefined,
+        status: undefined
+      },
+
+      openForm: false,
+      form: this.emptyForm(),
+      rules: {
+        apiType: [{ required: true, message: '接口类型不能为空', trigger: 'blur' }],
+        apiName: [{ required: true, message: '接口名称不能为空', trigger: 'blur' }],
+        apiCode: [{ required: true, message: '接口编码不能为空', trigger: 'blur' }],
+        apiUrl: [{ required: true, message: '请求地址不能为空', trigger: 'blur' }],
+        httpMethod: [{ required: true, message: '请求方式不能为空', trigger: 'change' }]
+      },
+
+      openTest: false,
+      testLoading: false,
+      currentRow: null,
+      testForm: {
+        headersJson: '',
+        bodyJson: '',
+        timeoutMs: 10000
+      },
+      testResult: {
+        responseStatus: null,
+        responseBody: '',
+        durationMs: null,
+        success: null,
+        errorMessage: ''
+      },
+
+      openLogs: false,
+      logsLoading: false,
+      logs: [],
+      logsTotal: 0,
+      logsQuery: {
+        pageNum: 1,
+        pageSize: 10,
+        configId: null
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    emptyForm() {
+      return {
+        id: null,
+        apiType: '',
+        apiName: '',
+        apiCode: '',
+        apiUrl: '',
+        httpMethod: 'GET',
+        rateWindowSeconds: null,
+        rateMaxCount: null,
+        priority: 0,
+        status: 1,
+        timeoutMs: 10000,
+        defaultHeadersJson: '',
+        defaultBodyJson: ''
+      }
+    },
+    formatWindow(seconds) {
+      if (!seconds) return '-'
+      if (seconds % 86400 === 0) return (seconds / 86400) + '天'
+      if (seconds % 3600 === 0) return (seconds / 3600) + '小时'
+      if (seconds % 60 === 0) return (seconds / 60) + '分钟'
+      return seconds + '秒'
+    },
+    getList() {
+      this.loading = true
+      pageExternalApi(this.queryParams).then(res => {
+        const page = res.data || {}
+        this.list = page.records || []
+        this.total = page.total || 0
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        apiType: undefined,
+        apiName: undefined,
+        apiCode: undefined,
+        status: undefined
+      }
+      this.getList()
+    },
+    handleAdd() {
+      this.form = this.emptyForm()
+      this.openForm = true
+      this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate())
+    },
+    handleEdit(row) {
+      getExternalApi(row.id).then(res => {
+        this.form = Object.assign(this.emptyForm(), res.data || {})
+        this.openForm = true
+        this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate())
+      })
+    },
+    submitForm() {
+      this.$refs.formRef.validate(valid => {
+        if (!valid) return
+        saveOrUpdateExternalApi(this.form).then(() => {
+          this.$message.success('保存成功')
+          this.openForm = false
+          this.getList()
+        })
+      })
+    },
+    handleStatusChange(row, val) {
+      changeExternalApiStatus(row.id, val).then(() => {
+        this.$message.success('状态已更新')
+      }).catch(() => {
+        row.status = val === 1 ? 0 : 1
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确认删除接口【${row.apiName}】?`, '提示', { type: 'warning' })
+        .then(() => deleteExternalApi(row.id))
+        .then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+        })
+        .catch(() => {})
+    },
+    handleTest(row) {
+      this.currentRow = row
+      this.testForm = { headersJson: '', bodyJson: '', timeoutMs: row.timeoutMs || 10000 }
+      this.testResult = { responseStatus: null, responseBody: '', durationMs: null, success: null, errorMessage: '' }
+      this.openTest = true
+    },
+    doTest() {
+      if (!this.currentRow) return
+      this.testLoading = true
+      testExternalApi(this.currentRow.id, this.testForm).then(res => {
+        this.testResult = res.data || this.testResult
+      }).finally(() => {
+        this.testLoading = false
+      })
+    },
+    handleLogs(row) {
+      this.logsQuery = { pageNum: 1, pageSize: 10, configId: row.id }
+      this.openLogs = true
+      this.getLogs()
+    },
+    getLogs() {
+      this.logsLoading = true
+      pageExternalApiLogs(this.logsQuery).then(res => {
+        const page = res.data || {}
+        this.logs = page.records || []
+        this.logsTotal = page.total || 0
+      }).finally(() => {
+        this.logsLoading = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+</style>
+

+ 275 - 0
src/views/company/workflowLobster/WorkflowSolutionEditor.vue

@@ -0,0 +1,275 @@
+<template>
+  <div class="workflow-solution-editor" :class="{ 'is-compact': compact }">
+    <el-divider v-if="dividerTitle" content-position="left">{{ dividerTitle }}</el-divider>
+
+    <el-form label-width="100px" :size="formSize">
+      <el-row :gutter="12">
+        <el-col :span="12">
+          <el-form-item label="方案名称">
+            <el-input v-model="model.templateName" :size="fieldSize" :disabled="readonly" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="行业类型">
+            <el-select v-model="model.industryType" :size="fieldSize" style="width:100%" :disabled="readonly">
+              <el-option label="旅游" value="travel" />
+              <el-option label="教育" value="education" />
+              <el-option label="医疗" value="medical" />
+              <el-option label="医美" value="medical_beauty" />
+              <el-option label="装修" value="decoration" />
+              <el-option label="零售" value="retail" />
+              <el-option label="通用" value="general" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="方案描述">
+        <el-input v-model="model.description" :size="fieldSize" type="textarea" :rows="compact ? 2 : 3" :disabled="readonly" />
+      </el-form-item>
+    </el-form>
+
+    <div class="section-title">动态变量定义({{ (model.variables || []).length }}个)</div>
+    <div v-for="(item, index) in model.variables" :key="'var-' + index" class="edit-row">
+      <el-row :gutter="8">
+        <el-col :span="4"><el-input v-model="item.varCode" :size="fieldSize" placeholder="变量编码" :disabled="readonly" /></el-col>
+        <el-col :span="4"><el-input v-model="item.varName" :size="fieldSize" placeholder="变量名称" :disabled="readonly" /></el-col>
+        <el-col :span="3">
+          <el-select v-model="item.varType" :size="fieldSize" style="width:100%" :disabled="readonly">
+            <el-option label="文本" value="string" />
+            <el-option label="日期" value="date" />
+            <el-option label="数字" value="number" />
+            <el-option label="布尔" value="boolean" />
+          </el-select>
+        </el-col>
+        <el-col :span="4">
+          <el-select v-model="item.sourceType" :size="fieldSize" style="width:100%" :disabled="readonly">
+            <el-option label="手动输入" value="manual" />
+            <el-option label="客户画像" value="user_profile" />
+            <el-option label="外部API" value="external_api" />
+          </el-select>
+        </el-col>
+        <el-col :span="2"><el-switch v-model="item.required" :active-value="1" :inactive-value="0" :disabled="readonly" /></el-col>
+        <el-col :span="5"><el-input v-model="item.description" :size="fieldSize" placeholder="说明" :disabled="readonly" /></el-col>
+        <el-col :span="2"><el-button v-if="!readonly" type="danger" :size="btnSize" @click="removeVariable(index)">删除</el-button></el-col>
+      </el-row>
+    </div>
+    <el-button v-if="!readonly" type="primary" plain :size="btnSize" @click="addVariable">添加变量</el-button>
+
+    <div class="section-title">工作流节点({{ (model.nodes || []).length }}个)</div>
+    <draggable v-model="model.nodes" handle=".drag-handle" class="node-list" :disabled="readonly">
+      <transition-group>
+        <el-card
+          v-for="(node, index) in model.nodes"
+          :key="(node.nodeCode || 'node') + '-' + index"
+          shadow="hover"
+          class="node-card"
+        >
+          <div slot="header" class="node-header">
+            <div>
+              <i class="el-icon-rank drag-handle" />
+              <span class="node-index">#{{ index + 1 }}</span>
+            </div>
+            <el-button v-if="!readonly" type="text" style="color:#f56c6c" @click="removeNode(index)">删除</el-button>
+          </div>
+          <el-form label-width="90px" :size="formSize">
+            <el-row :gutter="8">
+              <el-col :span="12">
+                <el-form-item label="节点名称">
+                  <el-input v-model="node.nodeName" :size="fieldSize" :disabled="readonly" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="节点类型">
+                  <el-select v-model="node.nodeType" :size="fieldSize" style="width:100%" :disabled="readonly">
+                    <el-option label="开始节点" :value="1" />
+                    <el-option label="消息节点" :value="2" />
+                    <el-option label="判断节点" :value="3" />
+                    <el-option label="等待节点" :value="4" />
+                    <el-option label="结束节点" :value="5" />
+                    <el-option label="API调用节点" :value="6" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-form-item label="节点编码"><el-input v-model="node.nodeCode" :size="fieldSize" :disabled="readonly" /></el-form-item>
+            <el-form-item label="下个节点"><el-input v-model="node.nextNodeCode" :size="fieldSize" :disabled="readonly" /></el-form-item>
+            <el-form-item label="消息模板" v-if="node.nodeType === 2">
+              <el-input v-model="node.messageTemplate" :size="fieldSize" type="textarea" :rows="compact ? 2 : 3" :disabled="readonly" />
+            </el-form-item>
+            <el-form-item label="发送时间" v-if="node.nodeType === 2">
+              <el-input v-model="node.sendTime" :size="fieldSize" :disabled="readonly" placeholder="例如:09:00" />
+            </el-form-item>
+            <el-form-item label="条件表达式" v-if="node.nodeType === 3">
+              <el-input v-model="node.conditionExpr" :size="fieldSize" type="textarea" :rows="compact ? 2 : 3" :disabled="readonly" />
+            </el-form-item>
+            <el-form-item label="节点配置">
+              <el-input v-model="node.nodeConfig" :size="fieldSize" type="textarea" :rows="compact ? 2 : 3" :disabled="readonly" />
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </transition-group>
+    </draggable>
+    <el-button v-if="!readonly" type="primary" plain :size="btnSize" @click="addNode">添加节点</el-button>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'WorkflowSolutionEditor',
+  components: { draggable },
+  props: {
+    value: {
+      type: Object,
+      required: true
+    },
+    readonly: {
+      type: Boolean,
+      default: false
+    },
+    dividerTitle: {
+      type: String,
+      default: ''
+    },
+    compact: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    formSize() {
+      return this.compact ? 'mini' : 'small'
+    },
+    fieldSize() {
+      return this.compact ? 'mini' : 'small'
+    },
+    btnSize() {
+      return this.compact ? 'mini' : 'mini'
+    },
+    model: {
+      get() {
+        return this.value
+      },
+      set(val) {
+        this.$emit('input', val)
+      }
+    }
+  },
+  methods: {
+    addVariable() {
+      if (!this.model.variables) this.$set(this.model, 'variables', [])
+      const idx = this.model.variables.length + 1
+      this.model.variables.push({
+        varCode: 'VAR_' + idx,
+        varName: '新变量' + idx,
+        varType: 'string',
+        sourceType: 'manual',
+        required: 0,
+        description: ''
+      })
+    },
+    removeVariable(index) {
+      if (!this.model.variables) return
+      this.model.variables.splice(index, 1)
+    },
+    addNode() {
+      if (!this.model.nodes) this.$set(this.model, 'nodes', [])
+      const idx = this.model.nodes.length + 1
+      this.model.nodes.push({
+        nodeCode: 'NODE_' + idx,
+        nodeName: '新节点' + idx,
+        nodeType: 2,
+        nodeConfig: '{}',
+        messageTemplate: '',
+        sendTime: '',
+        conditionExpr: '',
+        nextNodeCode: ''
+      })
+    },
+    removeNode(index) {
+      if (!this.model.nodes) return
+      this.model.nodes.splice(index, 1)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.section-title {
+  margin: 14px 0 10px;
+  font-weight: 600;
+  color: #303133;
+}
+.edit-row {
+  margin-bottom: 8px;
+}
+.node-list {
+  min-height: 20px;
+}
+.node-card {
+  margin-bottom: 10px;
+}
+.node-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.drag-handle {
+  cursor: move;
+  margin-right: 8px;
+  color: #409eff;
+}
+.node-index {
+  color: #909399;
+  font-size: 12px;
+}
+
+.workflow-solution-editor.is-compact {
+  .section-title {
+    margin: 10px 0 8px;
+    font-size: 15px;
+  }
+  .edit-row {
+    margin-bottom: 6px;
+  }
+  .node-card {
+    margin-bottom: 8px;
+  }
+  ::v-deep .el-card__header {
+    padding: 8px 12px;
+  }
+  ::v-deep .el-card__body {
+    padding: 10px 12px;
+  }
+  ::v-deep .el-form-item {
+    margin-bottom: 10px;
+  }
+  ::v-deep .el-form-item__label {
+    font-size: 14px;
+  }
+  ::v-deep .el-input__inner {
+    height: 32px;
+    line-height: 32px;
+    font-size: 14px;
+  }
+  ::v-deep .el-textarea__inner {
+    padding: 6px 10px;
+    line-height: 1.5;
+    font-size: 14px;
+    min-height: 56px !important;
+  }
+  ::v-deep .el-select .el-input__inner {
+    font-size: 14px;
+  }
+  ::v-deep .el-switch__label {
+    font-size: 13px;
+  }
+  .node-index {
+    font-size: 13px;
+  }
+  ::v-deep .el-form-item__content {
+    font-size: 14px;
+  }
+}
+</style>

+ 472 - 0
src/views/company/workflowLobster/index.vue

@@ -0,0 +1,472 @@
+<template>
+  <div class="workflow-lobster app-container">
+    <el-card shadow="never" class="ai-generate-card">
+      <div slot="header" class="card-header">
+        <span><i class="el-icon-s-opportunity" /> AI智能生成工作流</span>
+      </div>
+
+      <el-input
+        v-model="userRequirement"
+        type="textarea"
+        :rows="5"
+        placeholder="请输入业务需求,点击生成工作流方案"
+      />
+
+      <div v-if="availableApis.length" class="api-select-section">
+        <div class="api-select-label">配合的接口</div>
+        <el-checkbox-group v-model="selectedApiIds">
+          <el-checkbox
+            v-for="api in availableApis"
+            :key="api.id"
+            :label="api.id"
+            :disabled="true"
+            border
+            size="small"
+            class="api-item"
+          >
+            {{ api.apiName }}({{ api.apiType }})
+          </el-checkbox>
+        </el-checkbox-group>
+      </div>
+
+      <div class="generate-actions">
+        <el-button type="primary" size="medium" :loading="generating" @click="handleGenerate">生成工作流方案</el-button>
+        <el-button size="medium" @click="clearRequirement">清空</el-button>
+      </div>
+
+      <div v-if="generationResult" class="generation-result">
+        <workflow-solution-editor
+          v-model="generationResult"
+          :readonly="false"
+          divider-title="AI生成结果"
+        />
+
+        <div class="confirm-actions">
+          <el-button type="success" :loading="saving" @click="handleConfirmSave">确认并保存方案</el-button>
+        </div>
+      </div>
+    </el-card>
+
+    <el-card shadow="never" class="template-list-card">
+      <div slot="header" class="card-header">
+        <span>已保存工作流模板</span>
+      </div>
+      <el-table v-loading="loading" :data="tableData" border>
+        <el-table-column type="index" label="序号" width="60" />
+        <el-table-column prop="templateName" label="模板名称" min-width="180" />
+        <el-table-column prop="templateCode" label="模板编码" min-width="140" />
+        <el-table-column prop="industryType" label="行业类型" width="110" />
+        <el-table-column prop="status" label="状态" width="90">
+          <template slot-scope="scope">
+            <el-tag size="mini" :type="scope.row.status === 1 ? 'success' : 'info'">
+              {{ scope.row.status === 1 ? '已发布' : '未发布' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createTime" label="创建时间" width="160" />
+        <el-table-column label="操作" width="220" fixed="right">
+          <template slot-scope="scope">
+            <div class="table-action-group">
+              <el-button class="action-btn" type="text" @click="handlePreview(scope.row)">预览</el-button>
+              <el-button v-if="scope.row.status !== 1" class="action-btn" type="text" @click="handleEditTemplate(scope.row)">编辑</el-button>
+              <el-button class="action-btn" type="text" @click="handleVisual(scope.row)">流程图</el-button>
+              <el-button
+                v-if="scope.row.status !== 1"
+                class="action-btn danger-btn"
+                type="text"
+                @click="handleDeleteTemplate(scope.row)"
+              >删除</el-button>
+              <el-button
+                v-if="scope.row.status !== 1"
+                class="action-btn"
+                type="text"
+                :loading="statusChangingId === scope.row.id"
+                @click="handlePublish(scope.row)"
+              >发布</el-button>
+              <el-button
+                v-else
+                class="action-btn"
+                type="text"
+                :loading="statusChangingId === scope.row.id"
+                @click="handleUnpublish(scope.row)"
+              >取消发布</el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="pagination">
+        <el-pagination
+          v-model:current-page="page.current"
+          v-model:page-size="page.size"
+          :total="page.total"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+
+    <el-dialog :title="templateDialogMode === 'preview' ? '模板预览' : '模板编辑'" :visible.sync="templateDialogVisible" width="1000px">
+      <workflow-solution-editor
+        v-model="templateForm"
+        :readonly="templateDialogMode === 'preview'"
+        :compact="true"
+      />
+
+      <div slot="footer">
+        <el-button @click="templateDialogVisible = false">关闭</el-button>
+        <el-button v-if="templateDialogMode === 'edit'" type="primary" :loading="templateSaving" @click="submitTemplateEdit">保存修改</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import WorkflowSolutionEditor from './WorkflowSolutionEditor.vue'
+import {
+  getActiveExternalApis,
+  listWorkflowTemplate,
+  getWorkflowTemplateDetail,
+  updateWorkflowTemplate,
+  updateWorkflowTemplateStatus,
+  deleteWorkflowTemplate,
+  aiGenerateWorkflow,
+  getGenerateResultDetail,
+  confirmGenerateResultEdited
+} from '@/api/company/workflowLobster'
+
+export default {
+  name: 'WorkflowLobster',
+  components: { WorkflowSolutionEditor },
+  data() {
+    return {
+      loading: false,
+      generating: false,
+      saving: false,
+      userRequirement: '',
+      availableApis: [],
+      selectedApiIds: [],
+      generationResult: null,
+      currentRecordId: null,
+      tableData: [],
+      templateDialogVisible: false,
+      templateDialogMode: 'preview',
+      templateSaving: false,
+      statusChangingId: null,
+      page: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
+      templateForm: {
+        id: null,
+        templateName: '',
+        industryType: '',
+        description: '',
+        variables: [],
+        nodes: [],
+        templateCode: '',
+      }
+    }
+  },
+  created() {
+    this.loadAvailableApis()
+    this.loadTemplateList()
+  },
+  methods: {
+    async loadAvailableApis() {
+      try {
+        const res = await getActiveExternalApis()
+        const data = this.extractListData(res)
+        this.availableApis = data || []
+        this.selectedApiIds = this.availableApis.map(item => item.id)
+      } catch (e) {
+        this.availableApis = []
+        this.selectedApiIds = []
+      }
+    },
+    extractListData(res) {
+      if (!res) return []
+      if (Array.isArray(res.data)) return res.data
+      if (res.data && Array.isArray(res.data.records)) return res.data.records
+      if (Array.isArray(res.rows)) return res.rows
+      return []
+    },
+    clearRequirement() {
+      this.userRequirement = ''
+    },
+    async loadTemplateList() {
+      this.loading = true
+      try {
+        const res = await listWorkflowTemplate({ page: this.page.current, size: this.page.size })
+        if (Array.isArray(res.data)) {
+          this.tableData = res.data
+          this.page.total = res.data.length
+          return
+        }
+        if (res.data && Array.isArray(res.data.rows)) {
+          this.tableData = res.data.rows
+          this.page.total = res.data.total || 0
+          return
+        }
+        if (res.rows) {
+          this.tableData = res.rows
+          this.page.total = res.total || 0
+          return
+        }
+        this.tableData = []
+        this.page.total = 0
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSizeChange(size) {
+      this.page.size = size
+      this.page.current = 1
+      this.loadTemplateList()
+    },
+    handleCurrentChange(current) {
+      this.page.current = current
+      this.loadTemplateList()
+    },
+    async handleGenerate() {
+      if (!this.userRequirement.trim()) {
+        this.$message.warning('请输入业务需求描述')
+        return
+      }
+      this.generating = true
+      try {
+        const res = await aiGenerateWorkflow({
+          requirement: this.userRequirement,
+          selectedApiIds: this.selectedApiIds.length ? this.selectedApiIds : undefined
+        })
+        const recordId = res.data && res.data.recordId
+        if (!recordId) throw new Error('未返回生成记录ID')
+        this.currentRecordId = recordId
+        await this.pollGenerationResult(recordId)
+      } catch (e) {
+        this.$message.error(e.message || '生成失败')
+      } finally {
+        this.generating = false
+      }
+    },
+    async pollGenerationResult(recordId) {
+      const maxAttempts = 420
+      let attempts = 0
+      while (attempts < maxAttempts) {
+        const res = await getGenerateResultDetail(recordId)
+        const result = res.data || {}
+        if (result.status === 1) {
+          this.generationResult = {
+            templateName: result.templateName || '',
+            industryType: result.industryType || 'general',
+            description: result.description || '',
+            variables: result.variables || [],
+            nodes: result.nodes || [],
+            edges: result.edges || [] // 添加edges数据
+          }
+          this.$message.success('工作流方案生成成功,可拖拽和编辑节点后再保存')
+          return
+        }
+        if (result.status === 2) {
+          throw new Error(result.errorMsg || '生成失败')
+        }
+        await this.waitOneSecond()
+        attempts += 1
+      }
+      throw new Error('生成超时,请稍后重试')
+    },
+    waitOneSecond() {
+      return new Promise(resolve => {
+        setTimeout(resolve, 1000)
+      })
+    },
+    async handleConfirmSave() {
+      if (!this.currentRecordId) {
+        this.$message.warning('请先生成工作流')
+        return
+      }
+      this.saving = true
+      try {
+        const payload = {
+          templateName: this.generationResult.templateName,
+          industryType: this.generationResult.industryType,
+          description: this.generationResult.description,
+          variables: this.generationResult.variables || [],
+          nodes: this.generationResult.nodes || [],
+          edges: this.generationResult.edges || [] // 添加edges数据
+        }
+        const hasEdited = true
+        if (hasEdited) {
+          await confirmGenerateResultEdited(this.currentRecordId, payload)
+        } else {
+          await confirmGenerateResult(this.currentRecordId)
+        }
+        this.$message.success('方案已确认保存')
+        this.generationResult = null
+        this.currentRecordId = null
+        this.userRequirement = ''
+        this.selectedApiIds = this.availableApis.map(item => item.id)
+        this.loadTemplateList()
+      } catch (e) {
+        this.$message.error(e.message || '保存失败')
+      } finally {
+        this.saving = false
+      }
+    },
+    async handlePreview(row) {
+      await this.openTemplateDialog(row, 'preview')
+    },
+    async handleEditTemplate(row) {
+      await this.openTemplateDialog(row, 'edit')
+    },
+    handleVisual(row) {
+      this.$router.push({
+        path: '/workflow/visual/' + row.id,
+      })
+    },
+    async openTemplateDialog(row, mode) {
+      this.templateDialogMode = mode
+      this.templateDialogVisible = true
+
+      try {
+        const res = await getWorkflowTemplateDetail(row.id)
+        const data = (res && res.data) ? res.data : row
+        this.fillTemplateForm(data, row,row.templateCode )
+      } catch (e) {
+        this.fillTemplateForm(row, row,row.templateCode )
+      }
+    },
+    fillTemplateForm(data, fallbackRow,templateCode) {
+      const safe = data || fallbackRow || {}
+      this.templateForm = {
+        id: safe.id || fallbackRow.id,
+        templateName: safe.templateName || '',
+        industryType: safe.industryType || '',
+        description: safe.description || '',
+        variables: Array.isArray(safe.variables) ? JSON.parse(JSON.stringify(safe.variables)) : [],
+        nodes: Array.isArray(safe.nodes) ? JSON.parse(JSON.stringify(safe.nodes)) : []
+      }
+      console.log(this.templateForm)
+    },
+    async submitTemplateEdit() {
+      this.templateSaving = true
+      
+      try {
+        const payload = {
+          templateName: this.templateForm.templateName,
+          industryType: this.templateForm.industryType,
+          description: this.templateForm.description,
+          variables: this.templateForm.variables || [],
+          nodes: this.templateForm.nodes || []
+        }
+        
+        await updateWorkflowTemplate(this.templateForm.id, payload)
+        this.$message.success('模板修改成功')
+        this.templateDialogVisible = false
+        await this.loadTemplateList()
+      } catch (e) {
+        this.$message.error(e.message || '修改失败')
+      } finally {
+        this.templateSaving = false
+      }
+    },
+    async handleDeleteTemplate(row) {
+      try {
+        await this.$confirm('确认删除该模板吗?', '提示', { type: 'warning' })
+        await deleteWorkflowTemplate(row.id)
+        this.$message.success('删除成功')
+        await this.loadTemplateList()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || '删除失败')
+        }
+      }
+    },
+    async handlePublish(row) {
+      await this.changeTemplateStatus(row, 1)
+    },
+    async handleUnpublish(row) {
+      await this.changeTemplateStatus(row, 0)
+    },
+    async changeTemplateStatus(row, status) {
+      const actionText = status === 1 ? '发布' : '取消发布'
+      try {
+        await this.$confirm(`确认${actionText}该模板吗?`, '提示', { type: 'warning' })
+        this.statusChangingId = row.id
+        await updateWorkflowTemplateStatus(row.id, status)
+        this.$message.success(`${actionText}成功`)
+        await this.loadTemplateList()
+      } catch (e) {
+        if (e !== 'cancel') {
+          this.$message.error(e.message || `${actionText}失败`)
+        }
+      } finally {
+        this.statusChangingId = null
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.workflow-lobster {
+  .ai-generate-card {
+    margin-bottom: 16px;
+  }
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-weight: 600;
+  }
+  .api-select-section {
+    margin-top: 12px;
+    background: #f5f7fa;
+    border-radius: 6px;
+    padding: 10px;
+  }
+  .api-select-label {
+    font-size: 13px;
+    color: #606266;
+    margin-bottom: 8px;
+  }
+  .api-item {
+    margin-bottom: 8px;
+  }
+  .generate-actions {
+    margin-top: 12px;
+  }
+  .generation-result {
+    margin-top: 12px;
+  }
+  .confirm-actions {
+    margin-top: 16px;
+    text-align: center;
+  }
+  .pagination {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+  }
+
+  .table-action-group {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 2px 10px;
+  }
+
+  ::v-deep .table-action-group .action-btn {
+    margin: 0;
+    min-width: 44px;
+    text-align: center;
+    padding: 2px 0;
+  }
+
+  ::v-deep .table-action-group .danger-btn {
+    color: #f56c6c;
+  }
+}
+</style>

+ 1896 - 0
src/views/company/workflowLobster/visual.vue

@@ -0,0 +1,1896 @@
+<template>
+  <div class="workflow-canvas-editor" :class="{ 'is-fullscreen': isFullscreen }" v-loading="loading">
+    <!-- 顶部工具栏 -->
+    <div class="canvas-toolbar" v-show="!isFullscreen">
+      <div class="toolbar-left">
+        <el-button @click="goBack" icon="el-icon-arrow-left" size="small">返回</el-button>
+        <el-divider direction="vertical"></el-divider>
+        <span class="template-name">{{ templateData.templateName || '加载中...' }}</span>
+        <el-tag v-if="templateData.industryType" size="mini" type="info">{{ templateData.industryType }}</el-tag>
+        <el-tag v-if="isPublished" size="mini" type="success">已发布(只读)</el-tag>
+      </div>
+      <div class="toolbar-right">
+        <el-radio-group v-model="viewMode" size="small" class="view-switch-group">
+          <el-radio-button label="canvas">画布</el-radio-button>
+          <el-radio-button label="list">列表</el-radio-button>
+        </el-radio-group>
+        <el-button size="small" icon="el-icon-connection" @click="autoLayoutNodes" title="自动排列">自动排列</el-button>
+        <el-divider direction="vertical"></el-divider>
+        <el-button size="small" icon="el-icon-zoom-in" @click="zoomIn">放大</el-button>
+        <el-button size="small" icon="el-icon-zoom-out" @click="zoomOut">缩小</el-button>
+        <el-button size="small" icon="el-icon-rank" @click="fitView">适应</el-button>
+        <el-divider direction="vertical"></el-divider>
+        <el-button size="small" icon="el-icon-full-screen" @click="toggleFullscreen" title="全屏">全屏</el-button>
+        <el-divider direction="vertical"></el-divider>
+        <el-button type="primary" size="small" :loading="saving" :disabled="isPublished" @click="saveCanvas">保存</el-button>
+      </div>
+    </div>
+
+    <!-- 全屏模式浮动工具栏 -->
+    <div class="fullscreen-toolbar" v-if="isFullscreen">
+      <el-button @click="toggleFullscreen" icon="el-icon-close" type="info" plain size="small" title="退出全屏">退出全屏</el-button>
+      <span class="fullscreen-title" v-if="templateData">{{ templateData.templateName }}</span>
+      <el-button type="primary" size="small" :loading="saving" :disabled="isPublished" @click="saveCanvas">保存</el-button>
+    </div>
+
+    <!-- 基本信息卡片 -->
+    <el-card class="info-card" shadow="never" v-show="!isFullscreen && templateData">
+      <el-descriptions :column="4" size="small" border>
+        <el-descriptions-item label="模板名称">{{ templateData.templateName }}</el-descriptions-item>
+        <el-descriptions-item label="行业类型">{{ templateData.industryType || '通用' }}</el-descriptions-item>
+        <el-descriptions-item label="模板编码">{{ templateData.templateCode || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="节点数量">{{ nodes.length }} 个</el-descriptions-item>
+      </el-descriptions>
+    </el-card>
+
+    <!-- 画布模式 -->
+    <div v-if="viewMode === 'canvas'" class="canvas-mode">
+      <div class="canvas-meta">
+        <div class="meta-item">
+          <span class="meta-label">节点数</span>
+          <span class="meta-value">{{ nodes.length }}</span>
+        </div>
+        <div class="meta-item">
+          <span class="meta-label">连线数</span>
+          <span class="meta-value">{{ edges.length }}</span>
+        </div>
+        <div class="meta-item">
+          <span class="meta-label">缩放</span>
+          <span class="meta-value">{{ Math.round(scale * 100) }}%</span>
+        </div>
+      </div>
+
+    <div class="canvas-workspace" v-loading="loading">
+      <!-- 左侧节点面板 -->
+      <div class="node-panel">
+        <div class="panel-title">节点类型</div>
+        <div class="node-category" v-for="category in nodeCategories" :key="category.key">
+          <div class="category-title">{{ category.name }}</div>
+          <div class="node-list">
+            <div
+              class="node-item"
+              v-for="nodeType in category.types"
+              :key="nodeType.type"
+              :draggable="!isPublished"
+              :class="{ disabled: isPublished }"
+              @dragstart="onDragStart($event, nodeType)"
+            >
+              <i :class="nodeType.icon" :style="{ color: nodeType.color }"></i>
+              <span>{{ nodeType.label }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 中间画布区域 -->
+      <div
+        class="canvas-container"
+        ref="canvasContainer"
+        tabindex="0"
+        @drop="onDrop"
+        @dragover.prevent
+        @click="onCanvasClick"
+        @mousedown="onCanvasMouseDown"
+        @mousemove="onCanvasMouseMove"
+        @mouseup="onCanvasMouseUp"
+        @wheel.prevent="onMouseWheel"
+        @keydown.delete="handleDelete"
+        @keydown.backspace="handleDelete"
+      >
+        <svg
+          class="canvas-svg"
+          ref="canvasSvg"
+          :width="canvasSize.width"
+          :height="canvasSize.height"
+          :class="{ 'dragging-canvas': isDraggingCanvas }"
+        >
+          <defs>
+            <!-- 阴影滤镜 -->
+            <filter id="node-shadow" x="-5%" y="-5%" width="115%" height="120%">
+              <feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000000" flood-opacity="0.08" />
+            </filter>
+            <filter id="node-shadow-hover" x="-5%" y="-5%" width="115%" height="120%">
+              <feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#000000" flood-opacity="0.12" />
+            </filter>
+            <filter id="node-shadow-selected" x="-10%" y="-10%" width="130%" height="140%">
+              <feDropShadow dx="0" dy="3" stdDeviation="6" flood-color="#409eff" flood-opacity="0.35" />
+            </filter>
+            <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
+              <feGaussianBlur stdDeviation="3" result="blur" />
+              <feMerge>
+                <feMergeNode in="blur" />
+                <feMergeNode in="SourceGraphic" />
+              </feMerge>
+            </filter>
+            <!-- 点阵网格 -->
+            <pattern id="dot-grid" width="24" height="24" patternUnits="userSpaceOnUse">
+              <circle cx="12" cy="12" r="1.2" fill="#dce0e6" />
+            </pattern>
+            <!-- 节点渐变背景 -->
+            <linearGradient id="node-bg-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
+              <stop offset="0%" stop-color="#ffffff" />
+              <stop offset="100%" stop-color="#f8f9fb" />
+            </linearGradient>
+            <!-- 箭头标记 -->
+            <marker id="arrowhead" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
+              <path d="M0,1 L6,4 L0,7 Q0,4 0,1" fill="#b0b8c1" />
+            </marker>
+            <marker id="arrowhead-selected" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
+              <path d="M0,1 L6,4 L0,7 Q0,4 0,1" fill="#409eff" />
+            </marker>
+          </defs>
+
+          <!-- 网格背景 -->
+          <rect :width="canvasSize.width" :height="canvasSize.height" fill="url(#dot-grid)" />
+
+          <!-- 连线 -->
+          <g class="edges-layer" :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
+            <g
+              v-for="edge in edges"
+              :key="edge.edgeKey"
+              class="edge-group"
+              :class="{ selected: selectedEdge === edge }"
+              @click.stop="selectEdge(edge)"
+            >
+              <path
+                :d="getEdgePath(edge)"
+                stroke="transparent"
+                stroke-width="16"
+                fill="none"
+                class="edge-interaction"
+                style="cursor: pointer"
+              />
+              <path
+                :d="getEdgePath(edge)"
+                :stroke="edge.edgeColor || '#b0b8c1'"
+                stroke-width="2"
+                fill="none"
+                :marker-end="selectedEdge === edge ? 'url(#arrowhead-selected)' : 'url(#arrowhead)'"
+                class="edge-path"
+              />
+              <text
+                v-if="edge.edgeLabel"
+                :x="getEdgeLabelPos(edge).x"
+                :y="getEdgeLabelPos(edge).y"
+                text-anchor="middle"
+                font-size="11"
+                font-weight="500"
+                fill="#86909c"
+              >{{ edge.edgeLabel }}</text>
+            </g>
+
+            <!-- 正在绘制的临时连线 -->
+            <path
+              v-if="drawingEdge"
+              :d="getDrawingEdgePath()"
+              stroke="#409eff"
+              stroke-width="2"
+              stroke-dasharray="6,4"
+              fill="none"
+              opacity="0.7"
+              marker-end="url(#arrowhead-selected)"
+            />
+          </g>
+
+          <!-- 节点 -->
+          <g class="nodes-layer" :transform="`translate(${canvasOffset.x}, ${canvasOffset.y}) scale(${scale})`">
+            <g
+              v-for="node in nodes"
+              :key="node.nodeCode"
+              class="node-group"
+              :class="{ selected: selectedNode === node, dragging: isDraggingNode && draggingNode === node }"
+              :transform="`translate(${node.positionX || 0}, ${node.positionY || 0})`"
+              @click.stop="selectNode(node)"
+              :filter="selectedNode === node ? 'url(#node-shadow-selected)' : 'url(#node-shadow)'"
+            >
+              <!-- 节点背景 -->
+              <rect
+                x="0"
+                y="0"
+                :width="node.width || 200"
+                :height="node.height || 80"
+                rx="10"
+                fill="url(#node-bg-gradient)"
+                :stroke="selectedNode === node ? '#409eff' : '#e8ecf1'"
+                stroke-width="2"
+                class="node-rect"
+                @mousedown="onNodeMouseDown($event, node)"
+                style="cursor: move; pointer-events: all"
+              />
+
+              <!-- 左侧色彩装饰条 -->
+              <rect
+                x="0"
+                y="0"
+                width="5"
+                :height="node.height || 80"
+                rx="10"
+                class="node-accent"
+                :fill="getNodeAccentColor(node.nodeType)"
+                style="pointer-events: none"
+              />
+
+              <!-- 拖拽指示器 -->
+              <text
+                :x="(node.width || 200) - 14"
+                :y="14"
+                font-size="9"
+                fill="#c8cdd4"
+                class="drag-indicator"
+                style="cursor: move"
+              >⋮⋮</text>
+
+              <!-- 图标背景圆角方形 -->
+              <rect
+                x="14"
+                y="12"
+                rx="6"
+                width="36"
+                height="36"
+                :fill="getNodeAccentBg(node.nodeType)"
+                class="node-icon-bg"
+                style="pointer-events: none"
+              />
+
+              <!-- 节点类型图标 -->
+              <text
+                x="32"
+                y="37"
+                font-size="20"
+                text-anchor="middle"
+                fill="#ffffff"
+                style="pointer-events: none"
+              >{{ getNodeIcon(node.nodeType) }}</text>
+
+              <!-- 节点名称 -->
+              <text
+                x="60"
+                y="26"
+                font-size="13"
+                font-weight="700"
+                fill="#1d2129"
+                style="pointer-events: none"
+              >{{ node.nodeName }}</text>
+
+              <!-- 节点类型标签 -->
+              <rect
+                x="60"
+                y="33"
+                :width="getTypeTagWidth(node) + 8"
+                height="17"
+                rx="4"
+                :fill="getNodeAccentBg(node.nodeType)"
+                opacity="0.15"
+                style="pointer-events: none"
+              />
+              <text
+                x="64"
+                y="45"
+                font-size="10"
+                font-weight="600"
+                :fill="getNodeAccentColor(node.nodeType)"
+                style="pointer-events: none"
+              >{{ getNodeTypeName(node.nodeType) }}</text>
+
+              <!-- 底部描述 -->
+              <text
+                x="14"
+                y="68"
+                font-size="11"
+                fill="#86909c"
+                style="pointer-events: none"
+              >{{ getNodeBottomDesc(node) }}</text>
+
+              <!-- 连接点 - 上 -->
+              <circle
+                :cx="(node.width || 200) / 2"
+                cy="0"
+                r="5"
+                fill="#ffffff"
+                stroke="#c0c8d4"
+                stroke-width="1.5"
+                class="port port-top"
+                @mousedown.stop="startDrawEdge($event, node, 'top')"
+                style="cursor: crosshair; pointer-events: all"
+              />
+              <!-- 连接点 - 下 -->
+              <circle
+                :cx="(node.width || 200) / 2"
+                :cy="node.height || 80"
+                r="5"
+                fill="#ffffff"
+                stroke="#c0c8d4"
+                stroke-width="1.5"
+                class="port port-bottom"
+                @mousedown.stop="startDrawEdge($event, node, 'bottom')"
+                style="cursor: crosshair; pointer-events: all"
+              />
+              <!-- 连接点 - 左 -->
+              <circle
+                cx="0"
+                :cy="(node.height || 80) / 2"
+                r="5"
+                fill="#ffffff"
+                stroke="#c0c8d4"
+                stroke-width="1.5"
+                class="port port-left"
+                @mousedown.stop="startDrawEdge($event, node, 'left')"
+                style="cursor: crosshair; pointer-events: all"
+              />
+              <!-- 连接点 - 右 -->
+              <circle
+                :cx="node.width || 200"
+                :cy="(node.height || 80) / 2"
+                r="5"
+                fill="#ffffff"
+                stroke="#c0c8d4"
+                stroke-width="1.5"
+                class="port port-right"
+                @mousedown.stop="startDrawEdge($event, node, 'right')"
+                style="cursor: crosshair; pointer-events: all"
+              />
+            </g>
+          </g>
+        </svg>
+      </div>
+
+      <!-- 右侧属性面板(固定预留宽度) -->
+      <div class="property-panel">
+        <div class="panel-title">
+          {{ selectedNode ? '节点属性' : (selectedEdge ? '连线属性' : '属性面板') }}
+          <el-button size="mini" icon="el-icon-close" @click="clearSelection" style="float: right"></el-button>
+        </div>
+
+        <div v-if="!selectedNode && !selectedEdge" class="property-empty">
+          请先在画布中选择节点或连线
+        </div>
+
+        <!-- 节点属性表单 -->
+        <div class="panel-subtitle" v-if="selectedNode">基础配置</div>
+        <el-form v-if="selectedNode" label-width="80px" size="small" class="property-form">
+          <el-form-item label="节点名称">
+            <el-input v-model="selectedNode.nodeName" :disabled="isPublished" />
+          </el-form-item>
+          <el-form-item label="节点类型">
+            <el-select v-model="selectedNode.nodeType" style="width: 100%" :disabled="isPublished">
+              <el-option label="开始节点" :value="1" />
+              <el-option label="消息节点" :value="2" />
+              <el-option label="判断节点" :value="3" />
+              <el-option label="等待节点" :value="4" />
+              <el-option label="结束节点" :value="5" />
+              <el-option label="API节点" :value="6" />
+              <el-option label="购物车节点" :value="7" />
+              <el-option label="优惠券节点" :value="8" />
+              <el-option label="标签节点" :value="9" />
+              <el-option label="赠礼节点" :value="10" />
+              <el-option label="文档节点" :value="11" />
+              <el-option label="用户节点" :value="12" />
+              <el-option label="数据分析节点" :value="13" />
+              <el-option label="AI节点" :value="14" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="节点编码">
+            <el-input v-model="selectedNode.nodeCode" disabled />
+          </el-form-item>
+          <el-form-item label="消息模板" v-if="selectedNode.nodeType === 2">
+            <el-input v-model="selectedNode.messageTemplate" type="textarea" :rows="3" :disabled="isPublished" />
+          </el-form-item>
+          <el-form-item label="发送时间" v-if="selectedNode.nodeType === 2">
+            <el-input v-model="selectedNode.sendTime" :disabled="isPublished" placeholder="例如:09:00" />
+          </el-form-item>
+          <el-form-item label="条件表达式" v-if="selectedNode.nodeType === 3">
+            <el-input v-model="selectedNode.conditionExpr" type="textarea" :rows="3" :disabled="isPublished" />
+          </el-form-item>
+          <el-form-item label="节点配置">
+            <el-input v-model="selectedNode.nodeConfig" type="textarea" :rows="4" placeholder="JSON格式" :disabled="isPublished" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="danger" size="small" :disabled="isPublished" @click="deleteSelectedNode">删除节点</el-button>
+          </el-form-item>
+        </el-form>
+
+        <!-- 连线属性表单 -->
+        <div class="panel-subtitle" v-if="selectedEdge">连线配置</div>
+        <el-form v-if="selectedEdge" label-width="80px" size="small" class="property-form">
+          <el-form-item label="连线标签">
+            <el-input v-model="selectedEdge.edgeLabel" :disabled="isPublished" />
+          </el-form-item>
+          <el-form-item label="连线颜色">
+            <el-color-picker v-model="selectedEdge.edgeColor" :disabled="isPublished" />
+          </el-form-item>
+          <el-form-item label="条件表达式">
+            <el-input v-model="selectedEdge.conditionExpr" type="textarea" :rows="3" :disabled="isPublished" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="danger" size="small" :disabled="isPublished" @click="deleteSelectedEdge">删除连线</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+    </div>
+
+    <!-- 列表模式 -->
+    <div v-if="viewMode === 'list'" class="list-container" v-show="!isFullscreen">
+      <el-card shadow="never">
+        <div slot="header" class="card-header">
+          <span>节点列表({{ nodes.length }}个)</span>
+          <el-button type="primary" size="small" icon="el-icon-plus" @click="showAddNodeDialog">添加节点</el-button>
+        </div>
+        <el-timeline v-if="nodes.length > 0">
+          <el-timeline-item
+            v-for="(node, index) in sortedNodes"
+            :key="node.nodeCode"
+            :type="getNodeTimelineType(node.nodeType)"
+            :icon="getNodeTimelineIcon(node.nodeType)"
+          >
+            <el-card shadow="hover" class="timeline-node-card">
+              <div class="node-header">
+                <div class="node-title">
+                  <span class="node-name">{{ node.nodeName }}</span>
+                  <el-tag size="mini" :type="getNodeTimelineType(node.nodeType)">{{ getNodeTypeName(node.nodeType) }}</el-tag>
+                  <el-tag size="mini" type="info">{{ node.nodeCode }}</el-tag>
+                </div>
+                <div class="node-actions">
+                  <el-button type="text" size="small" icon="el-icon-edit" @click="selectNode(node); viewMode = 'canvas'">编辑</el-button>
+                  <el-button type="text" size="small" icon="el-icon-delete" style="color: #f56c6c" :disabled="isPublished" @click="deleteListNode(node)">删除</el-button>
+                </div>
+              </div>
+              <div class="node-body">
+                <p v-if="node.messageTemplate" class="node-preview">{{ truncateText(node.messageTemplate, 100) }}</p>
+                <p class="node-next">
+                  <i class="el-icon-right"></i>
+                  下一步: {{ node.nextNodeCode || '无' }}
+                </p>
+              </div>
+            </el-card>
+          </el-timeline-item>
+        </el-timeline>
+        <el-empty v-else description="暂无节点数据" />
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getWorkflowTemplateDetail, saveWorkflowCanvas } from '@/api/company/workflowLobster'
+
+export default {
+  name: 'WorkflowCanvasEditor',
+  components: {},
+  data() {
+    return {
+      loading: false,
+      saving: false,
+      viewMode: 'canvas',
+      templateId: null,
+      templateData: {}, // 初始化为空对象,避免null访问错误
+      nodes: [],
+      edges: [],
+      variables: [],
+      editorForm: {
+        templateName: '',
+        industryType: '',
+        description: '',
+        variables: [],
+        nodes: [],
+        edges: [] // 添加edges字段
+      },
+      // 画布控制
+      scale: 1,
+      canvasOffset: { x: 0, y: 0 },
+      canvasSize: { width: 5000, height: 5000 },
+      isDraggingCanvas: false,
+      dragStart: { x: 0, y: 0 },
+      // 节点拖拽
+      isDraggingNode: false,
+      draggingNode: null,
+      dragNodeOffset: { x: 0, y: 0 }, // 节点内的偏移量,避免跳动
+      canvasRect: null, // 缓存画布位置,提升拖动性能
+      dragAnimationFrame: null, // requestAnimationFrame ID,避免频繁更新
+      pendingDragClient: null, // 最新鼠标位置,供requestAnimationFrame消费
+      hasDragged: false, // 标记是否发生了拖动,用于区分click和drag
+      // 连线绘制
+      drawingEdge: null,
+      edgeStartNode: null,
+      edgeStartPort: null,
+      mousePos: { x: 0, y: 0 },
+      // 选中状态
+      selectedNode: null,
+      selectedEdge: null,
+      routeStatus: null,
+      // 全屏状态
+      isFullscreen: false,
+      // 网格配置
+      gridSize: 20, // 网格大小,用于对齐
+      // 节点类型配置
+      nodeCategories: [
+        {
+          key: 'basic',
+          name: '基础节点',
+          types: [
+            { type: 1, label: '开始节点', icon: 'el-icon-video-play', color: '#22c55e' },
+            { type: 5, label: '结束节点', icon: 'el-icon-video-pause', color: '#ef4444' }
+          ]
+        },
+        {
+          key: 'action',
+          name: '动作节点',
+          types: [
+            { type: 2, label: '消息节点', icon: 'el-icon-message', color: '#3b82f6' },
+            { type: 4, label: '等待节点', icon: 'el-icon-time', color: '#8b5cf6' },
+            { type: 6, label: 'API节点', icon: 'el-icon-link', color: '#6366f1' }
+          ]
+        },
+        {
+          key: 'logic',
+          name: '逻辑节点',
+          types: [
+            { type: 3, label: '判断节点', icon: 'el-icon-question', color: '#f59e0b' }
+          ]
+        },
+        {
+          key: 'ecommerce',
+          name: '电商节点',
+          types: [
+            { type: 7, label: '购物车节点', icon: 'el-icon-goods', color: '#22c55e' },
+            { type: 8, label: '优惠券节点', icon: 'el-icon-tickets', color: '#f59e0b' },
+            { type: 9, label: '标签节点', icon: 'el-icon-price-tag', color: '#ec4899' },
+            { type: 10, label: '赠礼节点', icon: 'el-icon-present', color: '#ec4899' }
+          ]
+        },
+        {
+          key: 'crm',
+          name: '客户节点',
+          types: [
+            { type: 11, label: '文档节点', icon: 'el-icon-document', color: '#6366f1' },
+            { type: 12, label: '用户节点', icon: 'el-icon-user-solid', color: '#8b5cf6' }
+          ]
+        },
+        {
+          key: 'analysis',
+          name: '分析节点',
+          types: [
+            { type: 13, label: '数据分析节点', icon: 'el-icon-data-line', color: '#14b8a6' },
+            { type: 14, label: 'AI节点', icon: 'el-icon-magic-stick', color: '#ec4899' }
+          ]
+        }
+      ]
+    }
+  },
+  created() {
+    this.templateId = this.$route.params.id
+    this.routeStatus = this.$route.query && this.$route.query.status !== undefined
+      ? Number(this.$route.query.status)
+      : null
+    this.fetchTemplateDetail()
+  },
+  computed: {
+    isPublished() {
+      const detailStatus = this.templateData && this.templateData.status !== undefined
+        ? Number(this.templateData.status)
+        : null
+      const status = detailStatus !== null && !Number.isNaN(detailStatus) ? detailStatus : this.routeStatus
+      return Number(status) === 1
+    },
+    sortedNodes() {
+      return [...this.nodes].sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0))
+    }
+  },
+  mounted() {
+    // 自动聚焦到画布以接收键盘事件
+    this.$nextTick(() => {
+      if (this.$refs.canvasContainer) {
+        this.$refs.canvasContainer.focus()
+      }
+    })
+    document.addEventListener('fullscreenchange', this.onFullscreenChange)
+    document.addEventListener('webkitfullscreenchange', this.onFullscreenChange)
+  },
+  beforeDestroy() {
+    // 清理事件监听器,防止内存泄漏
+    document.removeEventListener('mousemove', this.onNodeDragMove)
+    document.removeEventListener('mouseup', this.onNodeDragEnd)
+    document.removeEventListener('mousemove', this.onEdgeDrawMove)
+    document.removeEventListener('mouseup', this.onEdgeDrawEnd)
+    document.removeEventListener('fullscreenchange', this.onFullscreenChange)
+    document.removeEventListener('webkitfullscreenchange', this.onFullscreenChange)
+    if (this.dragAnimationFrame) {
+      cancelAnimationFrame(this.dragAnimationFrame)
+      this.dragAnimationFrame = null
+    }
+  },
+  methods: {
+    ensureEditable() {
+      if (!this.isPublished) return true
+      this.$message.warning('模板已发布,无法修改')
+      return false
+    },
+    async fetchTemplateDetail() {
+      this.loading = true
+      try {
+        const res = await getWorkflowTemplateDetail(this.templateId)
+        const data = res && res.data ? res.data : {}
+        if (data && data.status !== undefined && data.status !== null && !Number.isNaN(Number(data.status))) {
+          this.routeStatus = Number(data.status)
+        }
+        this.templateData = data
+        this.nodes = Array.isArray(data.nodes) ? data.nodes : []
+        this.edges = Array.isArray(data.edges) ? data.edges : []
+        this.variables = Array.isArray(data.variables) ? data.variables : []
+        
+        // 自动布局节点,避免堆积
+        this.autoLayoutNodes()
+        
+        this.editorForm = {
+          templateName: data.templateName || '',
+          industryType: data.industryType || 'general',
+          description: data.description || '',
+          variables: JSON.parse(JSON.stringify(this.variables)),
+          nodes: JSON.parse(JSON.stringify(this.nodes)),
+          edges: JSON.parse(JSON.stringify(this.edges)) // 添加edges数据
+        }
+      } catch (e) {
+        this.$message.error(e.message || '获取模板详情失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    /**
+     * 自动布局节点
+     * 根据节点的sortNo和连接关系,自动计算位置,避免堆积
+     * 每5个节点进行换行排列
+     */
+    autoLayoutNodes() {
+      if (!this.nodes || this.nodes.length === 0) {
+        this.$message.warning('没有可布局的节点')
+        return
+      }
+      
+      // 配置参数
+      const nodeWidth = 200
+      const nodeHeight = 80
+      const horizontalGap = 56   // 水平间距
+      const verticalGap = 72     // 垂直间距(行间距)
+      const startX = 100         // 起始X坐标
+      const startY = 100         // 起始Y坐标
+      const nodesPerRow = 4      // 每行节点数
+      
+      // 按照连接关系和sortNo排序节点
+      const sortedNodes = this.sortNodesForLayout()
+      
+      // 按照每行4个节点进行布局
+      sortedNodes.forEach((node, index) => {
+        // 计算行号和列号
+        const row = Math.floor(index / nodesPerRow)
+        const col = index % nodesPerRow
+        
+        // 计算位置
+        node.positionX = startX + col * (nodeWidth + horizontalGap)
+        node.positionY = startY + row * (nodeHeight + verticalGap)
+        
+        // 设置节点尺寸
+        if (!node.width) node.width = nodeWidth
+        if (!node.height) node.height = nodeHeight
+      })
+      
+      this.$message.success(`节点已自动排列(每行${nodesPerRow}个)`)
+      console.log('节点自动布局完成', sortedNodes.map(n => ({
+        code: n.nodeCode,
+        name: n.nodeName,
+        x: n.positionX,
+        y: n.positionY
+      })))
+    },
+    
+    /**
+     * 对节点进行排序,确保连线关系合理的节点排列在一起
+     */
+    sortNodesForLayout() {
+      if (!this.nodes || this.nodes.length === 0) return []
+      
+      // 构建节点映射表
+      const nodeMap = {}
+      this.nodes.forEach(node => {
+        nodeMap[node.nodeCode] = node
+      })
+      
+      // 使用BFS遍历,确保连接的节点相邻
+      const ordered = []
+      const visited = new Set()
+      
+      // 从START节点开始
+      const startNode = this.nodes.find(n => n.nodeType === 1)
+      if (startNode) {
+        const queue = [startNode.nodeCode]
+        visited.add(startNode.nodeCode)
+        
+        while (queue.length > 0) {
+          const code = queue.shift()
+          ordered.push(nodeMap[code])
+          
+          // 查找所有从当前节点出发的边
+          this.edges.forEach(edge => {
+            if (edge.sourceNodeCode === code && !visited.has(edge.targetNodeCode)) {
+              visited.add(edge.targetNodeCode)
+              queue.push(edge.targetNodeCode)
+            }
+          })
+        }
+      }
+      
+      // 添加未访问的节点(根据sortNo排序)
+      const unvisitedNodes = this.nodes
+        .filter(n => !visited.has(n.nodeCode))
+        .sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0))
+      
+      return [...ordered, ...unvisitedNodes]
+    },
+    goBack() {
+      this.$router.back()
+    },
+    // 全屏切换
+    toggleFullscreen() {
+      if (!this.isFullscreen) {
+        document.body.classList.add('is-visual-fullscreen')
+        const el = document.documentElement
+        if (el.requestFullscreen) {
+          el.requestFullscreen()
+        } else if (el.webkitRequestFullscreen) {
+          el.webkitRequestFullscreen()
+        } else if (el.msRequestFullscreen) {
+          el.msRequestFullscreen()
+        }
+      } else {
+        document.body.classList.remove('is-visual-fullscreen')
+        if (document.exitFullscreen) {
+          document.exitFullscreen()
+        } else if (document.webkitExitFullscreen) {
+          document.webkitExitFullscreen()
+        } else if (document.msExitFullscreen) {
+          document.msExitFullscreen()
+        }
+      }
+    },
+    onFullscreenChange() {
+      this.isFullscreen = !!document.fullscreenElement
+      if (!document.fullscreenElement) {
+        document.body.classList.remove('is-visual-fullscreen')
+      }
+    },
+    // 时间线节点类型颜色
+    getNodeTimelineType(type) {
+      const map = {
+        1: 'success', 2: 'primary', 3: 'warning', 4: 'info', 5: 'danger', 6: '',
+        7: 'success', 8: 'warning', 9: '', 10: 'primary',
+        11: 'info', 12: '', 13: 'success', 14: 'primary'
+      }
+      return map[type] || 'info'
+    },
+    getNodeTimelineIcon(type) {
+      const map = {
+        1: 'el-icon-video-play', 2: 'el-icon-message', 3: 'el-icon-question',
+        4: 'el-icon-time', 5: 'el-icon-video-pause', 6: 'el-icon-link',
+        7: 'el-icon-goods', 8: 'el-icon-tickets', 9: 'el-icon-price-tag', 10: 'el-icon-present',
+        11: 'el-icon-document', 12: 'el-icon-user-solid', 13: 'el-icon-data-line', 14: 'el-icon-magic-stick'
+      }
+      return map[type] || 'el-icon-connection'
+    },
+    // 列表视图添加节点
+    showAddNodeDialog() {
+      this.$message.info('请切换到画布视图拖拽添加节点')
+      this.viewMode = 'canvas'
+    },
+    // 列表视图删除节点
+    deleteListNode(node) {
+      if (this.isPublished) {
+        this.$message.warning('模板已发布,无法修改')
+        return
+      }
+      this.$confirm('确定删除该节点吗?', '提示', { type: 'warning' }).then(() => {
+        const index = this.nodes.findIndex(n => n.nodeCode === node.nodeCode)
+        if (index > -1) {
+          this.nodes.splice(index, 1)
+          this.edges = this.edges.filter(e =>
+            e.sourceNodeCode !== node.nodeCode && e.targetNodeCode !== node.nodeCode
+          )
+        }
+      }).catch(() => {})
+    },
+    // 文本截断
+    truncateText(str, len) {
+      if (!str) return ''
+      return str.length > len ? str.substring(0, len) + '...' : str
+    },
+    // 缩放控制
+    zoomIn() {
+      this.scale = Math.min(this.scale + 0.1, 2)
+    },
+    zoomOut() {
+      this.scale = Math.max(this.scale - 0.1, 0.5)
+    },
+    fitView() {
+      this.scale = 1
+      this.canvasOffset = { x: 0, y: 0 }
+    },
+    // 鼠标滚轮缩放
+    onMouseWheel(event) {
+      const delta = event.deltaY > 0 ? -0.05 : 0.05
+      this.scale = Math.max(0.5, Math.min(2, this.scale + delta))
+    },
+    // 节点类型辅助方法 - 现代配色
+    getNodeAccentColor(type) {
+      const map = {
+        1: '#22c55e', 2: '#3b82f6', 3: '#f59e0b',
+        4: '#8b5cf6', 5: '#ef4444', 6: '#6366f1',
+        7: '#22c55e', 8: '#f59e0b', 9: '#ec4899', 10: '#ec4899',
+        11: '#6366f1', 12: '#8b5cf6', 13: '#14b8a6', 14: '#ec4899'
+      }
+      return map[type] || '#6b7280'
+    },
+    getNodeAccentBg(type) {
+      const map = {
+        1: '#22c55e', 2: '#3b82f6', 3: '#f59e0b',
+        4: '#8b5cf6', 5: '#ef4444', 6: '#6366f1',
+        7: '#22c55e', 8: '#f59e0b', 9: '#ec4899', 10: '#ec4899',
+        11: '#6366f1', 12: '#8b5cf6', 13: '#14b8a6', 14: '#ec4899'
+      }
+      return map[type] || '#6b7280'
+    },
+    getNodeBottomDesc(node) {
+      const typeName = this.getNodeTypeName(node.nodeType)
+      if (node.nodeType === 2 && node.sendTime) {
+        return typeName + ' · ' + this.formatSendTime(node.sendTime)
+      }
+      const descMap = {
+        1: '工作流起始节点',
+        2: '发送消息给客户',
+        3: '条件判断分支',
+        4: '等待指定时长',
+        5: '工作流结束节点',
+        6: '调用外部API接口',
+        7: '购物车营销动作',
+        8: '优惠券发放与核销',
+        9: '客户标签管理',
+        10: '赠礼营销动作',
+        11: '文档处理与生成',
+        12: '用户信息管理',
+        13: '数据分析与报表',
+        14: 'AI智能处理'
+      }
+      return descMap[node.nodeType] || typeName
+    },
+    getTypeTagWidth(node) {
+      const name = this.getNodeTypeName(node.nodeType)
+      return name.replace(/[^\x00-\xff]/g, 'aa').length * 5.5
+    },
+    getNodeIcon(type) {
+      const icons = {
+        1: '\u25b6',  2: '\u2709',  3: '\u2699',
+        4: '\u23f3',  5: '\u25a0',  6: '\u21c4',
+        7: '\u266a',  8: '\u2726',  9: '\u2665', 10: '\u2605',
+        11: '\u270e', 12: '\u263a', 13: '\u2191', 14: '\u2603'
+      }
+      return icons[type] || '\u25cf'
+    },
+    getNodeTypeName(type) {
+      const names = {
+        1: '开始节点', 2: '消息节点', 3: '判断节点',
+        4: '等待节点', 5: '结束节点', 6: 'API节点',
+        7: '购物车节点', 8: '优惠券节点', 9: '标签节点', 10: '赠礼节点',
+        11: '文档节点', 12: '用户节点', 13: '数据分析节点', 14: 'AI节点'
+      }
+      return names[type] || '未知类型'
+    },
+    formatSendTime(sendTime) {
+      if (!sendTime) return ''
+      if (typeof sendTime === 'string') return sendTime
+      return String(sendTime)
+    },
+    // 拖拽添加节点
+    onDragStart(event, nodeType) {
+      if (!this.ensureEditable()) {
+        event.preventDefault()
+        return
+      }
+      event.dataTransfer.setData('nodeType', JSON.stringify(nodeType))
+      event.dataTransfer.effectAllowed = 'copy'
+    },
+    onDrop(event) {
+      if (!this.ensureEditable()) return
+      event.preventDefault()
+      const nodeType = JSON.parse(event.dataTransfer.getData('nodeType'))
+      const rect = this.$refs.canvasContainer.getBoundingClientRect()
+      const x = (event.clientX - rect.left - this.canvasOffset.x) / this.scale
+      const y = (event.clientY - rect.top - this.canvasOffset.y) / this.scale
+
+      // 对齐到网格
+      const snappedX = Math.round((x - 100) / this.gridSize) * this.gridSize
+      const snappedY = Math.round((y - 40) / this.gridSize) * this.gridSize
+
+      const nodeCode = 'NODE_' + Date.now()
+      const newNode = {
+        nodeCode: nodeCode,
+        nodeName: nodeType.label,
+        nodeType: nodeType.type,
+        positionX: snappedX,
+        positionY: snappedY,
+        width: 200,
+        height: 80,
+        sortNo: this.nodes.length + 1,
+        nodeConfig: '{}',
+        messageTemplate: '',
+        sendTime: '',
+        conditionExpr: '',
+        nextNodeCode: '',
+        delFlag: 0
+      }
+      this.nodes.push(newNode)
+      this.$message.success('已添加节点:' + nodeType.label)
+    },
+    // 画布拖拽 - 优化版
+    onCanvasMouseDown(event) {
+      // 只有在空白区域点击时才拖拽画布
+      if (event.target === this.$refs.canvasSvg || event.target.tagName === 'rect') {
+        this.isDraggingCanvas = true
+        this.dragStart = { 
+          x: event.clientX - this.canvasOffset.x, 
+          y: event.clientY - this.canvasOffset.y 
+        }
+        this.clearSelection()
+        // 改变鼠标样式
+        this.$refs.canvasContainer.style.cursor = 'grabbing'
+      }
+    },
+    onCanvasMouseMove(event) {
+      // 更新鼠标位置(用于连线预览)
+      if (this.drawingEdge) {
+        this.mousePos = {
+          x: event.offsetX,
+          y: event.offsetY
+        }
+      }
+      
+      // 画布拖动
+      if (this.isDraggingCanvas) {
+        this.canvasOffset.x = event.clientX - this.dragStart.x
+        this.canvasOffset.y = event.clientY - this.dragStart.y
+      }
+    },
+    onCanvasMouseUp(event) {
+      this.isDraggingCanvas = false
+      this.isDraggingNode = false
+      this.draggingNode = null
+      // 恢复鼠标样式
+      if (this.$refs.canvasContainer) {
+        this.$refs.canvasContainer.style.cursor = 'grab'
+      }
+      // 恢复文本选择
+      document.body.style.userSelect = ''
+      document.body.style.webkitUserSelect = ''
+      // 完成连线绘制
+      if (this.drawingEdge && this.edgeStartNode) {
+        // 查找鼠标位置下的节点作为目标节点
+        const targetNode = this.findNodeAtPosition(event)
+        if (targetNode && targetNode.nodeCode !== this.edgeStartNode.nodeCode) {
+          this.createEdge(this.edgeStartNode, this.edgeStartPort, targetNode)
+        }
+        this.drawingEdge = null
+        this.edgeStartNode = null
+        this.edgeStartPort = null
+      }
+    },
+    // 节点拖动开始
+    onNodeMouseDown(event, node) {
+      if (!this.ensureEditable()) return
+      event.preventDefault()
+      event.stopPropagation()
+      
+      this.isDraggingNode = true
+      this.draggingNode = node
+      this.selectedNode = node
+      this.hasDragged = false // 重置拖动标记
+      
+      // 计算鼠标相对于节点左上角的偏移
+      const nodeX = node.positionX || 0
+      const nodeY = node.positionY || 0
+      
+      // 缓存画布位置,避免重复计算
+      this.canvasRect = this.$refs.canvasContainer.getBoundingClientRect()
+      const mouseX = (event.clientX - this.canvasRect.left - this.canvasOffset.x) / this.scale
+      const mouseY = (event.clientY - this.canvasRect.top - this.canvasOffset.y) / this.scale
+      
+      this.dragNodeOffset = {
+        x: mouseX - nodeX,
+        y: mouseY - nodeY
+      }
+      this.pendingDragClient = null
+      
+      // 添加全局事件监听,使用passive提升性能
+      document.addEventListener('mousemove', this.onNodeDragMove, { passive: true })
+      document.addEventListener('mouseup', this.onNodeDragEnd)
+      document.body.style.cursor = 'move'
+      document.body.style.userSelect = 'none'
+    },
+    
+    // 节点拖动中:仅记录坐标,实际更新交给requestAnimationFrame
+    onNodeDragMove(event) {
+      if (!this.isDraggingNode || !this.draggingNode) return
+      this.pendingDragClient = { x: event.clientX, y: event.clientY }
+      this.scheduleNodeDragUpdate()
+    },
+
+    scheduleNodeDragUpdate() {
+      if (this.dragAnimationFrame) return
+      this.dragAnimationFrame = requestAnimationFrame(() => {
+        this.dragAnimationFrame = null
+        this.applyNodeDragPosition()
+      })
+    },
+
+    applyNodeDragPosition() {
+      if (!this.isDraggingNode || !this.draggingNode || !this.pendingDragClient) return
+      const canvasRect = this.canvasRect || this.$refs.canvasContainer.getBoundingClientRect()
+      const { x: clientX, y: clientY } = this.pendingDragClient
+      this.pendingDragClient = null
+
+      // 计算新的节点位置(光标位置 - 偏移量)
+      let newX = (clientX - canvasRect.left - this.canvasOffset.x) / this.scale - this.dragNodeOffset.x
+      let newY = (clientY - canvasRect.top - this.canvasOffset.y) / this.scale - this.dragNodeOffset.y
+
+      // 边界限制
+      const nodeWidth = this.draggingNode.width || 200
+      const nodeHeight = this.draggingNode.height || 80
+      newX = Math.max(0, Math.min(newX, this.canvasSize.width - nodeWidth))
+      newY = Math.max(0, Math.min(newY, this.canvasSize.height - nodeHeight))
+
+      this.draggingNode.positionX = newX
+      this.draggingNode.positionY = newY
+      this.hasDragged = true
+    },
+    
+    // 节点拖动结束
+    onNodeDragEnd() {
+      // 结束前应用最后一次鼠标位置,避免“慢一拍”
+      this.applyNodeDragPosition()
+      if (this.dragAnimationFrame) {
+        cancelAnimationFrame(this.dragAnimationFrame)
+        this.dragAnimationFrame = null
+      }
+
+      // 拖动结束时进行网格对齐
+      if (this.draggingNode) {
+        this.draggingNode.positionX = Math.round(this.draggingNode.positionX / this.gridSize) * this.gridSize
+        this.draggingNode.positionY = Math.round(this.draggingNode.positionY / this.gridSize) * this.gridSize
+      }
+      
+      this.isDraggingNode = false
+      this.draggingNode = null
+      this.pendingDragClient = null
+      
+      // 移除全局事件监听
+      document.removeEventListener('mousemove', this.onNodeDragMove)
+      document.removeEventListener('mouseup', this.onNodeDragEnd)
+      document.body.style.cursor = ''
+      document.body.style.userSelect = ''
+    },
+    selectNode(node) {
+      // 如果刚发生了拖动,不处理click事件
+      if (this.hasDragged) {
+        this.hasDragged = false
+        return
+      }
+      
+      this.selectedNode = node
+      this.selectedEdge = null
+    },
+    clearSelection() {
+      this.selectedNode = null
+      this.selectedEdge = null
+    },
+    deleteSelectedNode() {
+      if (!this.ensureEditable()) return
+      if (!this.selectedNode) return
+      const index = this.nodes.findIndex(n => n.nodeCode === this.selectedNode.nodeCode)
+      if (index > -1) {
+        this.nodes.splice(index, 1)
+        // 删除相关连线
+        this.edges = this.edges.filter(e =>
+          e.sourceNodeCode !== this.selectedNode.nodeCode &&
+          e.targetNodeCode !== this.selectedNode.nodeCode
+        )
+        this.selectedNode = null
+      }
+    },
+    // 开始绘制连线
+    startDrawEdge(event, node, port) {
+      if (!this.ensureEditable()) return
+      console.log('开始连线:', node.nodeName, port)
+      event.preventDefault()
+      event.stopPropagation()
+      
+      this.drawingEdge = true
+      this.edgeStartNode = node
+      this.edgeStartPort = port
+      
+      // 记录鼠标位置用于绘制预览线
+      const canvasRect = this.$refs.canvasContainer.getBoundingClientRect()
+      this.mousePos = {
+        x: event.clientX - canvasRect.left,
+        y: event.clientY - canvasRect.top
+      }
+      
+      // 添加全局事件监听
+      document.addEventListener('mousemove', this.onEdgeDrawMove)
+      document.addEventListener('mouseup', this.onEdgeDrawEnd)
+      document.body.style.cursor = 'crosshair'
+      document.body.style.userSelect = 'none'
+    },
+    
+    // 连线绘制中
+    onEdgeDrawMove(event) {
+      if (!this.drawingEdge) return
+      
+      const canvasRect = this.$refs.canvasContainer.getBoundingClientRect()
+      this.mousePos = {
+        x: event.clientX - canvasRect.left,
+        y: event.clientY - canvasRect.top
+      }
+    },
+    
+    // 连线绘制结束
+    onEdgeDrawEnd(event) {
+      console.log('结束连线')
+      
+      if (this.drawingEdge && this.edgeStartNode) {
+        // 查找目标节点
+        const targetNode = this.findNodeAtPosition(event)
+        if (targetNode && targetNode.nodeCode !== this.edgeStartNode.nodeCode) {
+          this.createEdge(this.edgeStartNode, this.edgeStartPort, targetNode)
+          this.$message.success('连线创建成功')
+        }
+      }
+      
+      // 清理状态
+      this.drawingEdge = false
+      this.edgeStartNode = null
+      this.edgeStartPort = null
+      
+      // 移除全局事件监听
+      document.removeEventListener('mousemove', this.onEdgeDrawMove)
+      document.removeEventListener('mouseup', this.onEdgeDrawEnd)
+      document.body.style.cursor = ''
+      document.body.style.userSelect = ''
+    },
+    selectEdge(edge) {
+      this.selectedEdge = edge
+      this.selectedNode = null
+    },
+    deleteSelectedEdge() {
+      if (!this.ensureEditable()) return
+      if (!this.selectedEdge) return
+      const index = this.edges.findIndex(e => e.edgeKey === this.selectedEdge.edgeKey)
+      if (index > -1) {
+        this.edges.splice(index, 1)
+        this.selectedEdge = null
+      }
+    },
+    getEdgePath(edge) {
+      const sourceNode = this.nodes.find(n => n.nodeCode === edge.sourceNodeCode)
+      const targetNode = this.nodes.find(n => n.nodeCode === edge.targetNodeCode)
+      if (!sourceNode || !targetNode) return ''
+
+      // 根据连接点计算起点和终点
+      const { x: sx, y: sy } = this.getPortPosition(sourceNode, edge.sourcePort || 'right')
+      const { x: tx, y: ty } = this.getPortPosition(targetNode, edge.targetPort || 'left')
+
+      // 使用贝塞尔曲线,根据连接点方向计算控制点
+      const dx = Math.abs(tx - sx)
+      const dy = Math.abs(ty - sy)
+      
+      // 控制点偏移量,使曲线更平滑
+      const offset = Math.max(50, Math.min(dx * 0.5, dy * 0.5))
+      
+      let cx1, cy1, cx2, cy2
+      
+      // 根据源连接点方向设置控制点
+      switch(edge.sourcePort) {
+        case 'right':
+          cx1 = sx + offset
+          cy1 = sy
+          break
+        case 'left':
+          cx1 = sx - offset
+          cy1 = sy
+          break
+        case 'bottom':
+          cx1 = sx
+          cy1 = sy + offset
+          break
+        case 'top':
+        default:
+          cx1 = sx
+          cy1 = sy - offset
+          break
+      }
+      
+      // 根据目标连接点方向设置控制点
+      switch(edge.targetPort) {
+        case 'right':
+          cx2 = tx + offset
+          cy2 = ty
+          break
+        case 'left':
+          cx2 = tx - offset
+          cy2 = ty
+          break
+        case 'bottom':
+          cx2 = tx
+          cy2 = ty + offset
+          break
+        case 'top':
+        default:
+          cx2 = tx
+          cy2 = ty - offset
+          break
+      }
+
+      // 返回贝塞尔曲线路径
+      return `M ${sx} ${sy} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${tx} ${ty}`
+    },
+    // 获取连接点的位置
+    getPortPosition(node, port) {
+      const x = node.positionX || 0
+      const y = node.positionY || 0
+      const w = node.width || 200
+      const h = node.height || 80
+      
+      switch(port) {
+        case 'top':
+          return { x: x + w / 2, y: y }
+        case 'bottom':
+          return { x: x + w / 2, y: y + h }
+        case 'left':
+          return { x: x, y: y + h / 2 }
+        case 'right':
+        default:
+          return { x: x + w, y: y + h / 2 }
+      }
+    },
+    getEdgeLabelPos(edge) {
+      const sourceNode = this.nodes.find(n => n.nodeCode === edge.sourceNodeCode)
+      const targetNode = this.nodes.find(n => n.nodeCode === edge.targetNodeCode)
+      if (!sourceNode || !targetNode) return { x: 0, y: 0 }
+
+      // 根据连接点计算起点和终点
+      const { x: sx, y: sy } = this.getPortPosition(sourceNode, edge.sourcePort || 'right')
+      const { x: tx, y: ty } = this.getPortPosition(targetNode, edge.targetPort || 'left')
+
+      // 标签位置在曲线中点上方
+      return {
+        x: (sx + tx) / 2,
+        y: (sy + ty) / 2 - 10
+      }
+    },
+    getDrawingEdgePath() {
+      if (!this.edgeStartNode) return ''
+      const startPos = this.getPortPosition(this.edgeStartNode, this.edgeStartPort)
+      const mx = (this.mousePos.x - this.canvasOffset.x) / this.scale
+      const my = (this.mousePos.y - this.canvasOffset.y) / this.scale
+      
+      // 使用简单的直线作为预览,贝塞尔曲线计算较复杂
+      return `M ${startPos.x} ${startPos.y} L ${mx} ${my}`
+    },
+    handleDelete() {
+      if (!this.ensureEditable()) return
+      if (this.selectedNode) {
+        this.deleteSelectedNode()
+      } else if (this.selectedEdge) {
+        this.deleteSelectedEdge()
+      }
+    },
+    onCanvasClick(event) {
+      // 如果刚发生了拖动,不处理click事件
+      if (this.hasDragged) {
+        this.hasDragged = false
+        return
+      }
+      
+      // 点击空白区域取消选中
+      // 检查点击的是否是SVG背景或网格矩形
+      const target = event.target
+      const isSvgBackground = target === this.$refs.canvasSvg || 
+                             (target.tagName === 'rect' && target.getAttribute('fill') === 'url(#dot-grid)')
+      
+      if (isSvgBackground) {
+        this.clearSelection()
+      }
+    },
+    // 辅助方法:查找鼠标位置下的节点
+    findNodeAtPosition(event) {
+      const rect = this.$refs.canvasContainer.getBoundingClientRect()
+      const x = (event.clientX - rect.left - this.canvasOffset.x) / this.scale
+      const y = (event.clientY - rect.top - this.canvasOffset.y) / this.scale
+
+      for (const node of this.nodes) {
+        const nx = node.positionX || 0
+        const ny = node.positionY || 0
+        const nw = node.width || 200
+        const nh = node.height || 80
+        if (x >= nx && x <= nx + nw && y >= ny && y <= ny + nh) {
+          return node
+        }
+      }
+      return null
+    },
+    // 创建连线
+    createEdge(sourceNode, sourcePort, targetNode) {
+      const edgeKey = 'EDGE_' + Date.now()
+      const newEdge = {
+        edgeKey: edgeKey,
+        sourceNodeCode: sourceNode.nodeCode,
+        targetNodeCode: targetNode.nodeCode,
+        sourcePort: sourcePort || 'right',
+        targetPort: 'left',
+        edgeLabel: '',
+        edgeColor: '#b0b8c1',
+        conditionExpr: '',
+        sortNo: this.edges.length + 1,
+        delFlag: 0
+      }
+      this.edges.push(newEdge)
+    },
+    // 保存画布
+    async saveCanvas() {
+      if (!this.ensureEditable()) return
+      if (!this.templateData || !this.templateData.templateName) {
+        this.$message.warning('模板数据未加载完成')
+        return
+      }
+      
+      this.saving = true
+      try {
+        const payload = {
+          templateName: this.templateData.templateName,
+          industryType: this.templateData.industryType,
+          description: this.templateData.description,
+          canvasData: JSON.stringify({
+            scale: this.scale,
+            offset: this.canvasOffset
+          }),
+          variables: this.variables,
+          nodes: this.nodes,
+          edges: this.edges
+        }
+        await saveWorkflowCanvas(this.templateId, payload)
+        this.$message.success('保存成功')
+        await this.fetchTemplateDetail()
+      } catch (e) {
+        this.$message.error(e.message || '保存失败')
+      } finally {
+        this.saving = false
+      }
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.workflow-canvas-editor {
+  height: calc(100vh - 84px);
+  display: flex;
+  flex-direction: column;
+  background: #f0f2f5;
+  overflow: hidden;
+  padding: 16px;
+  box-sizing: border-box;
+
+  .canvas-toolbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 14px 18px;
+    background: #fff;
+    border-radius: 12px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+    margin-bottom: 10px;
+
+    .toolbar-left {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+
+      .template-name {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+        letter-spacing: 0.2px;
+      }
+    }
+
+    .toolbar-right {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+
+      .view-switch-group {
+        margin-right: 10px;
+      }
+
+      ::v-deep .el-button {
+        border-radius: 8px;
+        padding: 8px 12px;
+        min-height: 32px;
+      }
+
+      ::v-deep .el-radio-button__inner {
+        border-radius: 8px;
+        padding: 8px 12px;
+        min-height: 32px;
+        line-height: 14px;
+      }
+
+      ::v-deep .el-divider--vertical {
+        margin: 0 4px;
+        height: 18px;
+        background-color: #e5e7eb;
+      }
+    }
+  }
+
+  // 基本信息卡片
+  .info-card {
+    margin-bottom: 12px;
+    border-radius: 12px;
+    border: none;
+    box-shadow: 0 1px 8px rgba(0, 0, 0, 0.04);
+  }
+
+  // 全屏浮动工具栏
+  .fullscreen-toolbar {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 8px 16px;
+    background: #fff;
+    border-bottom: 1px solid #ebeef5;
+    height: 48px;
+    box-sizing: border-box;
+    flex-shrink: 0;
+
+    .fullscreen-title {
+      font-size: 15px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .canvas-mode {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
+  }
+
+  .canvas-meta {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-bottom: 8px;
+
+    .meta-item {
+      min-width: 96px;
+      padding: 7px 12px;
+      border-radius: 12px;
+      background: #fff;
+      border: none;
+      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+    }
+
+    .meta-label {
+      font-size: 12px;
+      color: #909399;
+      margin-right: 8px;
+    }
+
+    .meta-value {
+      font-size: 16px;
+      font-weight: 700;
+      color: #303133;
+    }
+  }
+
+  .canvas-workspace {
+    flex: 1;
+    display: flex;
+    overflow: hidden;
+    gap: 12px;
+    min-height: 0;
+
+    .node-panel {
+      width: 240px;
+      background: #fff;
+      border: none;
+      border-radius: 12px;
+      padding: 14px;
+      overflow-y: auto;
+      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+      .panel-title {
+        font-size: 14px;
+        font-weight: 600;
+        margin-bottom: 14px;
+        color: #303133;
+      }
+
+      .node-category {
+        margin-bottom: 20px;
+
+        .category-title {
+          font-size: 12px;
+          color: #909399;
+          margin-bottom: 6px;
+        }
+
+        .node-list {
+          display: flex;
+          flex-direction: column;
+          gap: 8px;
+
+          .node-item {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            padding: 10px 12px;
+            background: #f7f8fa;
+            border: 1px solid #ebeef5;
+            border-radius: 8px;
+            cursor: move;
+            transition: all 0.2s ease;
+
+            &.disabled {
+              cursor: not-allowed;
+              opacity: 0.55;
+            }
+
+            &:hover {
+              background: #eef5ff;
+              border-color: #c6ddf7;
+              box-shadow: 0 2px 8px rgba(59, 130, 246, 0.12);
+              transform: translateY(-1px);
+            }
+
+            &.disabled:hover {
+              background: #f7f8fa;
+              border-color: #ebeef5;
+              box-shadow: none;
+            }
+
+            i {
+              font-size: 15px;
+            }
+
+            span {
+              font-size: 12px;
+              color: #606266;
+            }
+          }
+        }
+      }
+    }
+
+    .canvas-container {
+      flex: 1;
+      position: relative;
+      overflow: hidden;
+      background: #fff;
+      outline: none;
+      border-radius: 12px;
+      border: none;
+      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+      .canvas-svg {
+        min-width: 100%;
+        min-height: 100%;
+        cursor: grab;
+        background: #f7f8fa;
+        user-select: none;
+        -webkit-user-select: none;
+
+        &.dragging-canvas {
+          cursor: grabbing;
+        }
+
+        .edge-group {
+          will-change: transform;
+          transition: opacity 0.2s;
+
+          .edge-path {
+            transition: stroke 0.2s ease, stroke-width 0.2s ease;
+          }
+
+          &.selected {
+            .edge-path {
+              stroke: #409eff !important;
+              stroke-width: 2.5;
+            }
+          }
+
+          &:hover:not(.selected) {
+            .edge-path {
+              stroke: #86909c !important;
+              stroke-width: 2.5;
+            }
+          }
+
+          .edge-interaction {
+            &:hover {
+              stroke: rgba(64, 158, 255, 0.06);
+            }
+          }
+        }
+
+        .node-group {
+          cursor: move;
+          pointer-events: all;
+          transition: filter 0.2s, opacity 0.2s;
+
+          .node-rect {
+            transition: stroke 0.2s ease, stroke-width 0.2s ease;
+          }
+
+          .node-accent {
+            transition: opacity 0.2s;
+          }
+
+          &.selected {
+            .node-rect {
+              stroke: #409eff !important;
+              stroke-width: 2.5;
+            }
+          }
+
+          &.dragging {
+            transition: none !important;
+            .node-rect {
+              stroke: #409eff !important;
+              stroke-width: 2.5;
+            }
+            opacity: 0.92;
+          }
+
+          &:hover:not(.selected):not(.dragging) {
+            filter: url(#node-shadow-hover) !important;
+            .node-rect {
+              stroke: #c8cdd4;
+            }
+          }
+
+          .drag-indicator {
+            opacity: 0;
+            transition: opacity 0.15s;
+            user-select: none;
+          }
+
+          &:hover .drag-indicator {
+            opacity: 1;
+          }
+
+          .port {
+            opacity: 0;
+            transition: all 0.2s ease;
+            cursor: crosshair;
+
+            &:hover {
+              r: 7;
+              fill: #409eff;
+              stroke: #409eff;
+              stroke-width: 2;
+              opacity: 1;
+              filter: url(#glow);
+            }
+          }
+
+          &:hover .port {
+            opacity: 0.85;
+          }
+        }
+      }
+    }
+
+    .property-panel {
+      width: 320px;
+      min-width: 320px;
+      background: #fff;
+      border: none;
+      border-radius: 12px;
+      overflow-y: auto;
+      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+      .panel-title {
+        padding: 11px 14px;
+        font-size: 14px;
+        font-weight: 600;
+        border-bottom: 1px solid #f0f2f5;
+        background: #f8fafc;
+        border-radius: 12px 12px 0 0;
+      }
+
+      .panel-subtitle {
+        margin: 10px 12px 4px;
+        font-size: 12px;
+        color: #909399;
+        font-weight: 600;
+      }
+
+      .property-empty {
+        margin: 14px 12px;
+        padding: 14px 12px;
+        border: 1px dashed #dcdfe6;
+        border-radius: 8px;
+        color: #909399;
+        font-size: 13px;
+        background: #fafafa;
+        text-align: center;
+      }
+
+      .property-form {
+        padding: 8px 12px 12px;
+      }
+
+      ::v-deep .el-form-item__label {
+        color: #606266;
+        font-weight: 500;
+      }
+
+      ::v-deep .el-input__inner,
+      ::v-deep .el-textarea__inner,
+      ::v-deep .el-select .el-input__inner {
+        border-radius: 8px;
+      }
+    }
+  }
+
+  // 列表视图
+  .list-container {
+    flex: 1;
+    overflow-y: auto;
+    padding: 0;
+
+    ::v-deep .el-card {
+      border-radius: 12px;
+      border: none;
+      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+    }
+
+    .timeline-node-card {
+      margin-bottom: 8px;
+      border-radius: 8px;
+      transition: box-shadow 0.2s;
+
+      &:hover {
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+      }
+
+      .node-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 8px;
+
+        .node-title {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+
+          .node-name {
+            font-weight: 700;
+            font-size: 15px;
+            color: #303133;
+          }
+        }
+
+        .node-actions {
+          display: flex;
+          gap: 4px;
+        }
+      }
+
+      .node-body {
+        color: #606266;
+        font-size: 13px;
+
+        .node-preview {
+          margin: 0 0 8px 0;
+          line-height: 1.5;
+          color: #606266;
+        }
+
+        .node-next {
+          margin: 0;
+          color: #909399;
+          font-size: 12px;
+          display: flex;
+          align-items: center;
+          gap: 4px;
+        }
+      }
+    }
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+}
+
+// 全屏模式样式
+.workflow-canvas-editor.is-fullscreen {
+  padding: 0;
+  height: 100vh;
+  overflow: hidden;
+  background: #f8f9fb;
+
+  .canvas-workspace {
+    gap: 8px;
+  }
+
+  .canvas-workspace .canvas-container {
+    border-radius: 0;
+    box-shadow: none;
+  }
+
+  .canvas-workspace .node-panel {
+    border-radius: 0 12px 12px 0;
+    margin-left: 0;
+  }
+
+  .canvas-workspace .property-panel {
+    border-radius: 12px 0 0 12px;
+    margin-right: 0;
+  }
+}
+</style>
+
+<style lang="scss">
+.is-visual-fullscreen {
+  .sidebar,
+  .header,
+  .el-aside,
+  .ant-layout-sider {
+    display: none !important;
+  }
+
+  .main-content,
+  .ant-layout-content {
+    padding: 0 !important;
+    overflow: hidden !important;
+  }
+
+  .layout-container,
+  .ant-layout {
+    height: 100vh !important;
+  }
+}
+</style>

+ 154 - 46
src/views/crm/customer/customerDetail.vue

@@ -98,14 +98,13 @@
                     </div>
                 </div>
                 <!-- AI 沟通总结 -->
-                <div class="card card-highlight">
+                <div class="card">
                     <div class="card-header">
                         <h3><i class="fas fa-robot"></i> AI 沟通总结</h3>
                     </div>
-                    <div class="summary-content">
-                        <p class="summary-text">{{ getCommunicationSummary() }}</p>
+                    <div class="summary-text compact">
+                        {{ getCommunicationSummary() }}
                     </div>
-                    <div class="update-time-corner">沟通时间:{{ getUpdateTime() }}</div>
                 </div>
                 <!-- 沟通记录 -->
                 <div class="card card-table">
@@ -136,7 +135,7 @@
                                             {{ getIntentionDegreeFromRecord(record) }}
                                         </span>
                                 </td>
-                                <td class="record-cell">{{ record.createTime }}</td>
+                                <td class="record-cell">{{ parseTime(record.createTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</td>
                                 <td class="record-cell">
                                     <button @click="viewChat(record)" class="btn-view-chat">
                                         <i class="fas fa-comments"></i> 聊天详情
@@ -260,7 +259,7 @@
                     </div>
                     <div class="intention-section">
                         <div class="intention-header">
-                            <span class="intention-label">客户意向度</span>
+                            <span class="intention-label"><i class="fas fa-heart"></i> 客户意向度</span>
                             <el-tooltip placement="top" effect="light">
                                 <i class="el-icon-info intention-info-icon"></i>
                                 <div slot="content" class="intention-tooltip">
@@ -879,29 +878,35 @@ export default {
     background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
 }
 
+/* 徽章改为“标签”风格(类似 AI 标签) */
 .risk-unknown .risk-badge {
-    background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
-    box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3);
+    background: #f8fafc;
+    border-color: #e5eaf1;
+    color: #64748b;
 }
 
 .risk-none .risk-badge {
-    background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
-    box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+    background: #f0fdf4;
+    border-color: #bbf7d0;
+    color: #16a34a;
 }
 
 .risk-low .risk-badge {
-    background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
-    box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
+    background: #eff6ff;
+    border-color: #bfdbfe;
+    color: #2563eb;
 }
 
 .risk-medium .risk-badge {
-    background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
-    box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
+    background: #fffbeb;
+    border-color: #fde68a;
+    color: #d97706;
 }
 
 .risk-high .risk-badge {
-    background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
-    box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
+    background: #fef2f2;
+    border-color: #fecaca;
+    color: #dc2626;
 }
 
 .risk-card:hover {
@@ -914,38 +919,26 @@ export default {
     display: inline-flex;
     align-items: center;
     gap: 6px;
-    padding: 6px 14px;
-    border-radius: 8px;
-    font-size: 16px;
-    font-weight: 700;
-    color: white;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
-    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+    padding: 5px 10px;
+    border-radius: 10px;
+    font-size: 13px;
+    font-weight: 600;
+    border: 1px solid #e5eaf1;
+    box-shadow: none;
+    transition: all 0.2s ease;
 }
 
 .risk-badge::before {
     content: '';
-    width: 6px;
-    height: 6px;
-    background: white;
-    border-radius: 50%;
-    animation: pulse 2s infinite;
-}
-
-@keyframes pulse {
-    0%, 100% {
-        opacity: 1;
-        transform: scale(1);
-    }
-    50% {
-        opacity: 0.5;
-        transform: scale(1.2);
-    }
+    width: 8px;
+    height: 8px;
+    border-radius: 3px;
+    background: currentColor;
+    opacity: 0.35;
 }
 
 .risk-card:hover .risk-badge {
-    transform: scale(1.05);
+    transform: translateY(-1px);
 }
 
 /* 风险分析内容 */
@@ -1864,7 +1857,7 @@ export default {
     color: #64748b;
     display: flex;
     align-items: center;
-    gap: 4px;
+    gap: 6px;
 }
 
 .intention-label::before {
@@ -1875,6 +1868,19 @@ export default {
     border-radius: 2px;
 }
 
+.intention-label i {
+    width: 20px;
+    height: 20px;
+    border-radius: 6px;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 12px;
+    color: #fff;
+    background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
+    box-shadow: 0 2px 6px rgba(245, 158, 11, 0.35);
+}
+
 /* 水印风格意向度显示 - 按等级着色 */
 .intention-watermark {
     font-size: 59px;
@@ -2034,11 +2040,17 @@ export default {
 }
 
 .card-header h3 i {
-    font-size: 20px;
+    width: 24px;
+    height: 24px;
+    border-radius: 7px;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 13px;
+    color: #fff;
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    -webkit-background-clip: text;
-    -webkit-text-fill-color: transparent;
-    background-clip: text;
+    box-shadow: 0 3px 8px rgba(102, 126, 234, 0.35);
+    -webkit-text-fill-color: #fff;
 }
 
 .card-highlight .card-header h3 i {
@@ -2047,5 +2059,101 @@ export default {
     -webkit-text-fill-color: white;
 }
 
+/* 模仿参考图的企业微信分析台风格(覆盖) */
+.customer-container {
+    max-width: 100%;
+    padding: 12px;
+    background: #f4f6fa;
+}
+
+.main-grid-three-columns {
+    grid-template-columns: 300px minmax(640px, 1fr) 320px;
+    gap: 12px;
+    align-items: start;
+}
+
+.card {
+    border-radius: 10px;
+    border: 1px solid #e6ebf2;
+    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
+    margin-bottom: 10px;
+    padding: 12px;
+    background: #fff;
+}
+
+.card::before {
+    display: none;
+}
+
+.card:hover {
+    transform: none;
+    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
+    border-color: #dbe4f0;
+}
+
+.card-header {
+    border-bottom: 1px solid #edf1f7;
+    margin-bottom: 10px;
+    padding-bottom: 8px;
+}
+
+.card-header h3 {
+    font-size: 16px;
+    font-weight: 600;
+    color: #1f2937;
+}
+
+.card-header h3 i {
+    width: 18px;
+    height: 18px;
+    border-radius: 4px;
+    font-size: 10px;
+    background: #4f7cff;
+    box-shadow: none;
+}
+
+.summary-text.compact {
+    font-size: 14px;
+    line-height: 1.75;
+    color: #334155;
+    max-height: 132px;
+}
+
+.risk-card,
+.card-focus {
+    background: #fff;
+    border: 1px solid #e6ebf2;
+}
+
+.risk-analysis {
+    background: #f8fafc;
+    border: 1px solid #edf2f7;
+}
+
+.tag-item,
+.focus-item,
+.profile-item {
+    background: #f8fafc;
+    border-color: #e5eaf1;
+}
+
+.intention-label i {
+    width: 18px;
+    height: 18px;
+    border-radius: 4px;
+    font-size: 10px;
+    background: #4f7cff;
+    box-shadow: none;
+}
+
+.records-table th {
+    background: #f8fafc;
+    color: #475569;
+}
+
+.records-table tbody tr:hover {
+    background: #f8fbff;
+}
+
 
 </style>

+ 149 - 2
src/views/crm/customer/index.vue

@@ -129,6 +129,29 @@
               <el-option key="0"  label="否" value="0" />
             </el-select>
           </el-form-item>
+          <el-form-item label="流失风险" prop="attritionLevel">
+            <el-select style="width:220px" v-model="queryParams.attritionLevel" placeholder="请选择流失风险等级" clearable size="small">
+                <el-option label="全部" value="" />
+                <el-option label="未知" :value="0" />
+                <el-option label="无风险" :value="1" />
+                <el-option label="低风险" :value="2" />
+                <el-option label="中风险" :value="3" />
+                <el-option label="高风险" :value="4" />
+            </el-select>
+      </el-form-item>
+      <el-form-item label="意向度">
+          <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>
             <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
             <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@@ -234,7 +257,100 @@
                 <el-tag prop="status" v-for="(item, index) in typeOptions"    v-if="scope.row.customerType==item.dictValue">{{item.dictLabel}}</el-tag>
             </template>
           </el-table-column>
-          <el-table-column label="标签" align="center" prop="tags" />
+          <el-table-column label="标签" align="center" prop="properties" width="200">
+            <template slot-scope="scope">
+              <div v-if="scope.row.properties && scope.row.properties.length" style="text-align: left;">
+                <el-tooltip
+                  v-for="(item, index) in scope.row.properties.slice(0, 3)"
+                  :key="index"
+                  placement="top"
+                  effect="light"
+                >
+                  <div slot="content" style="max-width: 420px; word-break: break-word;">
+                    {{ item.propertyName }}:{{ item.propertyValue }}
+                  </div>
+                  <el-tag style="margin: 0 6px 6px 0; max-width: 100%;">
+                    {{ shortenText(item.propertyName + ':' + item.propertyValue, 16) }}
+                  </el-tag>
+                </el-tooltip>
+                <el-tooltip
+                  v-if="scope.row.properties.length > 3"
+                  placement="top"
+                  effect="light"
+                >
+                  <div slot="content" style="max-width: 360px;">
+                    <div
+                      v-for="(item, idx) in scope.row.properties.slice(3)"
+                      :key="'more-' + idx"
+                      style="margin-bottom: 4px;"
+                    >
+                      {{ item.propertyName }}:{{ item.propertyValue }}
+                    </div>
+                  </div>
+                  <el-tag type="info" style="margin: 0 6px 6px 0;">
+                    +{{ scope.row.properties.length - 3 }}
+                  </el-tag>
+                </el-tooltip>
+              </div>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="流失风险" align="center" prop="attritionLevel">
+            <template slot-scope="scope">
+              <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
+              <el-tag v-if="scope.row.attritionLevel === 0" type="info">未知</el-tag>
+              <el-tag v-else-if="scope.row.attritionLevel === 1" type="success">无风险</el-tag>
+              <el-tag v-else-if="scope.row.attritionLevel === 2" type="info">低风险</el-tag>
+              <el-tag v-else-if="scope.row.attritionLevel === 3" type="warning">中风险</el-tag>
+              <el-tag v-else-if="scope.row.attritionLevel === 4" type="danger">高风险</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="意向度" align="center" prop="intentionDegree">
+            <template slot-scope="scope">
+              <el-tag v-if="scope.row.intentionDegree !== null && scope.row.intentionDegree !== undefined && scope.row.intentionDegree !== ''">
+                {{ scope.row.intentionDegree }}
+              </el-tag>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="关注点" align="center" prop="customerFocusJson" width="220">
+            <template slot-scope="scope">
+              <div v-if="parseFocusPoints(scope.row.customerFocusJson).length" style="text-align: left;">
+                <el-tooltip
+                  v-for="(item, index) in parseFocusPoints(scope.row.customerFocusJson).slice(0, 2)"
+                  :key="index"
+                  placement="top"
+                  effect="light"
+                >
+                  <div slot="content" style="max-width: 420px; word-break: break-word;">
+                    {{ item }}
+                  </div>
+                  <el-tag style="margin: 0 6px 6px 0; max-width: 100%; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                    {{ shortenText(item, 14) }}
+                  </el-tag>
+                </el-tooltip>
+                <el-tooltip
+                  v-if="parseFocusPoints(scope.row.customerFocusJson).length > 2"
+                  placement="top"
+                  effect="light"
+                >
+                  <div slot="content" style="max-width: 420px;">
+                    <div
+                      v-for="(item, idx) in parseFocusPoints(scope.row.customerFocusJson).slice(2)"
+                      :key="'focus-more-' + idx"
+                      style="margin-bottom: 4px;"
+                    >
+                      {{ item }}
+                    </div>
+                  </div>
+                  <el-tag style="margin: 0 6px 6px 0; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                    +{{ parseFocusPoints(scope.row.customerFocusJson).length - 2 }}
+                  </el-tag>
+                </el-tooltip>
+              </div>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
           <el-table-column label="备注" align="center" prop="remark" />
           <el-table-column label="进线客户详情" align="center" :show-overflow-tooltip="true" prop="registerDesc" />
           <el-table-column label="领取时间" align="center" prop="receiveTime" />
@@ -262,6 +378,13 @@
                 @click="handleShow(scope.row)"
                 v-hasPermi="['crm:customer:query']"
               >查看</el-button>
+              <el-button
+                v-if="scope.row.attritionLevel !== null && scope.row.attritionLevel !== undefined"
+                size="mini"
+                type="text"
+                @click="openAiDrawer(scope.row)"
+                v-hasPermi="['crm:analyze:list']"
+              >AI 分析</el-button>
               <el-button
                 v-if="scope.row.isReceive==1"
                 size="mini"
@@ -292,6 +415,18 @@
       >
         <customer-details  ref="customerDetails" />
     </el-drawer>
+    <el-drawer
+    size="75%"
+    :title="aiAnalyze.title"
+    :visible.sync="aiAnalyze.open"
+    append-to-body
+  >
+    <customer-detail
+      ref="customerAiDetail"
+      :customer-id="aiAnalyze.customerId"
+      :customer-row="aiAnalyze.customerRow"
+    />
+  </el-drawer>
 
 
 
@@ -349,6 +484,7 @@ import editSource from '../components/editSource.vue';
 import customerSource from '../components/customerSource.vue';
 import customerAssignList from '../components/customerAssignList.vue';
 import assignUser from '../components/assignUser.vue';
+import customerDetail from './customerDetail.vue';
 export default {
   name: "Customer",
   components: {assignUser,customerAssignList,addBatchSms,editSource, customerDetails,Treeselect,customerSource },
@@ -487,7 +623,13 @@ export default {
         source: [
           { required: true, message: "客户来源不能为空", trigger: "blur" }
         ],
-      }
+      },
+      aiAnalyze: {
+        title: "AI 分析",
+        open: false,
+        customerId: null,
+        customerRow: null,
+      },
     };
   },
   watch: {
@@ -531,6 +673,11 @@ export default {
     this.getList();
   },
   methods: {
+    openAiDrawer(row) {
+      this.aiAnalyze.customerId = row.customerId;
+      this.aiAnalyze.customerRow = row;
+      this.aiAnalyze.open = true;
+    },
     handleShow(row){
       this.show.open=true;
       var that=this;

+ 30 - 2
src/views/fastGpt/fastGptChatSession/index.vue

@@ -149,7 +149,7 @@
       </el-table-column>
       <el-table-column label="角色昵称" align="center" prop="roleName" />
 	  <el-table-column label="企微账号" align="center" prop="qwUserName" />
-	  
+
       <el-table-column label="接待时间" align="center" prop="createTime" >
         <template slot-scope="scope">
           <span v-if="!scope.row.updateTime && scope.row.status === 1">
@@ -177,6 +177,11 @@
 	  </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
+          <el-button v-if="scope.row.isArtificial==0"
+                     size="mini"
+                     type="text"
+                     @click="notReply(scope.row)"
+          >永久取消AI回复</el-button>
           <el-button
             size="mini"
             type="text"
@@ -255,7 +260,7 @@
 </template>
 
 <script>
-import { listFastGptChatSession, getFastGptChatSession, delFastGptChatSession, addFastGptChatSession, updateFastGptChatSession, exportFastGptChatSession } from "@/api/fastGpt/fastGptChatSession";
+import { listFastGptChatSession, getFastGptChatSession, delFastGptChatSession, addFastGptChatSession, updateFastGptChatSession, exportFastGptChatSession,updateFastGptChatSessionNew } from "@/api/fastGpt/fastGptChatSession";
 import fastGptChatMsgDetails from "@/views/fastGpt/fastGptChatSession/fastGptChatMsgDetails.vue";
 
 export default {
@@ -339,6 +344,14 @@ export default {
 		    this.getList();
 		  });
 	  },
+    notReply(row){
+      this.form.sessionId=row.sessionId
+      updateFastGptChatSessionNew(this.form).then(response => {
+        this.msgSuccess("禁用成功");
+        this.open = false;
+        this.getList();
+      });
+    },
     /** 查询对话关系列表 */
     getList() {
       this.loading = true;
@@ -396,12 +409,27 @@ export default {
     },
     /** 搜索按钮操作 */
     handleQuery() {
+      // 处理时间参数
+      if (this.dateRange && this.dateRange.length === 2) {
+        this.queryParams.beginTime = this.dateRange[0];
+        this.queryParams.endTime = this.dateRange[1];
+      } else {
+        this.queryParams.beginTime = null;
+        this.queryParams.endTime = null;
+      }
       this.queryParams.pageNum = 1;
       this.getList();
     },
     /** 重置按钮操作 */
     resetQuery() {
+      // 清空时间选择器
+      this.dateRange = [];
+      // 清空时间查询参数
+      this.queryParams.beginTime = null;
+      this.queryParams.endTime = null;
+      // 重置表单
       this.resetForm("queryForm");
+      // 执行查询
       this.handleQuery();
     },
     // 多选框选中数据

+ 9 - 3
src/views/live/liveConfig/index.vue

@@ -72,6 +72,7 @@ import LiveRedConf from './liveRedConf.vue'
 import LiveLotteryConf from './liveLotteryConf.vue'
 import LiveReplay from './liveReplay.vue'
 import Preview from './preview.vue'
+import Cookies from 'js-cookie'
 import LiveCoupon from './liveCoupon.vue'
 import Barrage from './barrage.vue'
 import { listLive, getLive, delLive, addLive, updateLive, exportLive,selectCompanyTalent,handleShelfOrUn,handleDeleteSelected } from "@/api/live/live";
@@ -129,11 +130,16 @@ export default {
   },
   methods: {
     connectWebSocket() {
-      this.$store.dispatch('initLiveWs', {
+      const params = {
         liveWsUrl: this.liveWsUrl,
         liveId: this.liveId,
-        userId: this.userId
-      })
+        userId: this.userId,
+      };
+      const tenantCode = Cookies.get('tenantCode');
+      if (tenantCode) {
+        params.tenantCode = tenantCode;
+      }
+      this.$store.dispatch('initLiveWs', params)
       this.socket = this.$store.state.liveWs[this.liveId]
       this.socket.onmessage = (event) => this.handleWsMessage(event)
     },

+ 9 - 3
src/views/live/liveConsole/LiveConsole.vue

@@ -267,6 +267,7 @@ import { listLiveSingleMsg,delLiveMsg } from '@/api/live/liveMsg'
 import { getLive } from '@/api/live/live'
 import { consoleList } from '@/api/live/task'
 import ScreenScale from './ScreenScale.vue'; // 路径根据实际位置调整
+import Cookies from 'js-cookie'
 
 
 export default {
@@ -832,11 +833,16 @@ export default {
       })
     },
     connectWebSocket() {
-      this.$store.dispatch('initLiveWs', {
+      const params = {
         liveWsUrl: this.liveWsUrl,
         liveId: this.liveId,
-        userId: this.userId
-      })
+        userId: this.userId,
+      };
+      const tenantCode = Cookies.get('tenantCode');
+      if (tenantCode) {
+        params.tenantCode = tenantCode;
+      }
+      this.$store.dispatch('initLiveWs', params)
       this.socket = this.$store.state.liveWs[this.liveId]
       this.socket.onmessage = (event) => this.handleWsMessage(event)
     },

+ 12 - 19
src/views/login.vue

@@ -34,7 +34,6 @@
             @keyup.enter.native="handleLogin"
             class="password"
           >
-            <!-- <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" /> -->
             <img slot="prefix" src="../assets/images/pass.png" class="input-icon" />
             <img slot="suffix" src="../assets/images/eyeoff.png" class="input-icon2" v-if="ispassword" @click.stop="changetype()"/>
             <img slot="suffix" src="../assets/images/eyeopen.png" class="input-icon2" v-else @click.stop="changetype()"/>
@@ -49,7 +48,6 @@
             @keyup.enter.native="handleLogin"
 
           >
-            <!-- <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon"/> -->
             <img slot="prefix" src="../assets/images/code.png" class="input-icon" />
           </el-input>
           <div class="login-code">
@@ -72,7 +70,6 @@
       </el-form>
     </div>
 
-    <!-- 微信扫码弹框 -->
     <WechatLoginDialog
       ref="wechatDialog"
       :ticket="loginForm.username"
@@ -81,7 +78,6 @@
       :redirect="redirect"
     />
 
-    <!--  底部  -->
     <div class="el-login-footer">
       <span>{{companyName}}</span>
       <a :href="icpUrl" target="_bank">{{icpRecord}}</a>
@@ -155,24 +151,19 @@ export default {
     this.getCookie();
   },
   methods: {
-    // 重新加载运行时配置
     async reloadRuntimeConfig() {
-
       try {
         const res = await getConfigByKey('his.adminUi.config')
 
         const configValue = res?.data?.configValue
         if (!configValue) return
 
-        // 后端配置 JSON
         const form = JSON.parse(configValue)
 
-        // 直接更新全局配置
         const config = {
           VUE_APP_LIVE_WS_URL: form.liveWebSocketUrl || '',
           VUE_APP_COURSE_DEFAULT: form.courseDefaultType || '1',
         }
-        // 更新到全局
         Vue.prototype.$runtimeConfig = config
 
       } catch (error) {
@@ -182,12 +173,9 @@ export default {
     checkFirstLogin() {
       getFirstLogin().then(res => {
         if (res.code === 200 && res.data) {
-          // 将首次登录状态存储到本地
           localStorage.setItem('isFirstLogin', res.data ? 'true' : 'false');
-          // 如果是首次登录,跳转到设置密码页面
           this.$router.push('/set-password');
         } else {
-          // 否则进入首页
           this.$router.push({ path: this.redirect || "/" });
         }
       }).catch(() => {
@@ -205,20 +193,21 @@ export default {
       const username = Cookies.get("username");
       const password = Cookies.get("password");
       const rememberMe = Cookies.get('rememberMe')
+      const tenantCode = Cookies.get('tenantCode')
       this.loginForm = {
+        ...this.loginForm,
         username: username === undefined ? this.loginForm.username : username,
         password: password === undefined ? this.loginForm.password : decrypt(password),
-        rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
+        rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
+        tenantCode: tenantCode === undefined ? this.loginForm.tenantCode : tenantCode
       };
     },
 
-    // 微信扫码成功回调
     handleWechatLoginSuccess(token) {
       this.loading = false
       console.log("父组件收到 loginSuccess:", token);
       this.$store.commit("SET_TOKEN", token);
       setToken(token);
-      // 登录成功后检查是否是首次登录
       this.checkFirstLogin();
     },
 
@@ -227,16 +216,22 @@ export default {
       this.$refs.loginForm.validate(valid => {
         if (valid) {
           this.loading = true;
+          if (this.loginForm.tenantCode) {
+            Cookies.set('tenantCode', this.loginForm.tenantCode);
+          } else {
+            Cookies.remove('tenantCode');
+          }
+
+          console.log("登录参数:", Cookies.get("tenantCode"))
+
           if (this.loginForm.rememberMe) {
             Cookies.set("username", this.loginForm.username, { expires: 30 });
             Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
             Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
-            Cookies.set('tenantCode', this.loginForm.tenantCode, { expires: 30 });
           } else {
             Cookies.remove("username");
             Cookies.remove("password");
             Cookies.remove('rememberMe');
-            Cookies.remove('tenantCode');
           }
           this.$store
             .dispatch("Login", this.loginForm)
@@ -245,14 +240,12 @@ export default {
               if (res.needSms) {
                 console.log("打开弹窗")
                 this.wechatDialogVisible = true;
-                // 等 visible 更新后,直接调用弹窗 open()
                 this.$nextTick(() => {
                   if (this.$refs.wechatDialog) {
                     this.$refs.wechatDialog.open(this.loginForm.username);
                   }
                 });
               } else {
-                // 登录成功后检查是否是首次登录
                 this.checkFirstLogin();
               }
 

+ 20 - 1
src/views/member/list.vue

@@ -545,8 +545,27 @@ export default {
 
     /** 重置按钮操作 */
     resetQuery() {
+      // 1. 清空日期
       this.dateRange = [];
-      this.resetForm("queryForm");
+
+      // 2. 清空表单绑定的 queryParams(真正生效的重置)
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        nickname: null,
+        isMyFsUser: false,
+        phone: null,
+        tagIds: [],
+        tabValue: "0",
+        watchCourseType: "0",
+        missCourseStatus: "0",
+        continueMissCourseSort: "0",
+        registerStartTime: null,
+        registerEndTime: null,
+        projectId: null,
+      };
+
+      // 3. 重新查询
       this.handleQuery();
     },
 

+ 24 - 1
src/views/member/mylist.vue

@@ -575,8 +575,31 @@ export default {
 
     /** 重置按钮操作 */
     resetQuery() {
+      // 1. 清空日期范围
       this.dateRange = [];
-      this.resetForm("queryForm");
+
+      // 2. 清空表单输入框(真正生效)
+      if (this.$refs.queryForm) {
+        this.$refs.queryForm.resetFields();
+      }
+
+      // 3. 强制还原查询参数(保险)
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        nickname: null,
+        phone: null,
+        tagIds: [],
+        tabValue: "0",
+        watchCourseType: "0",
+        missCourseStatus: "0",
+        continueMissCourseSort: "0",
+        registerStartTime: null,
+        registerEndTime: null,
+        projectId: null,
+      };
+
+      // 4. 重新查询
       this.handleQuery();
     },
 

+ 2305 - 0
src/views/qw/externalContact/customerDetail.vue

@@ -0,0 +1,2305 @@
+<template>
+    <div class="customer-container">
+        <div class="main-grid-three-columns">
+            <div class="left-column">
+                <!-- 客户画像 (成交要素) -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-id-card"></i> 客户画像(成交要素)</h3>
+                    </div>
+                    <div class="profile-grid">
+                        <div class="profile-item profile-item-main">
+                            <span class="label"><i class="fas fa-user"></i> 客户姓名:</span>
+                            <span class="value highlight">{{ (customerData && (customerData.customerName || customerData.name)) || '-' }}</span>
+                        </div>
+                        <template v-for="(value, key) in customerPortraitData">
+                            <div
+                                v-if="key !== '需求'"
+                                :key="key"
+                                class="profile-item"
+                            >
+                                <span class="label">
+                                    <i class="fas fa-info-circle"></i> {{ key }}:
+                                </span>
+                                <span class="value">{{ value }}</span>
+                            </div>
+                        </template>
+                        <!-- 需求单独显示,占满整行 -->
+                        <div
+                            v-if="customerPortraitData['需求']"
+                            key="需求"
+                            class="profile-item profile-item-full"
+                        >
+                            <span class="label">
+                                <i class="fas fa-bullseye"></i> 需求:
+                            </span>
+                            <span class="value long-text">{{ customerPortraitData['需求'] }}</span>
+                        </div>
+                    </div>
+                </div>
+                <!-- AI 标签 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3>
+                            <i class="fas fa-tags"></i> AI 标签
+                        </h3>
+                        <!-- <el-button
+                            v-if="allAiTags.length === 0"
+                            size="mini"
+                            type="primary"
+                            @click="handleAnalyzeTag"
+                        >
+                            AI分析标签
+                        </el-button> -->
+                    </div>
+                    <div class="tags-container">
+                        <div v-if="allAiTags.length > 0" class="tags-list">
+                            <div
+                                v-for="(item, index) in visibleTags"
+                                :key="item.id"
+                                class="tag-item"
+                                :class="{ 'tag-highlight': index < 3 }"
+                            >
+                                <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>
+                        </div>
+
+                        <!-- 加载更多按钮 -->
+                        <div v-if="allAiTags.length > tagsPageSize" class="tags-actions">
+                            <button
+                                v-if="!isExpanded"
+                                @click="loadMoreTags"
+                                class="btn-expand-tags"
+                                type="button"
+                            >
+                                <i class="fas fa-chevron-down"></i> 展开全部 ({{ allAiTags.length - tagsPageSize }})
+                            </button>
+
+                            <!-- 收起按钮 -->
+                            <button
+                                v-else
+                                @click="collapseTags"
+                                class="btn-collapse-tags"
+                                type="button"
+                            >
+                                <i class="fas fa-chevron-up"></i> 收起标签
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="middle-column">
+                <!-- 沟通摘要 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-comment-dots"></i> 沟通摘要</h3>
+                    </div>
+                    <div class="summary-text compact">
+                        {{ getCommunicationAbstract() }}
+                    </div>
+                </div>
+                <!-- AI 沟通总结 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-robot"></i> AI 沟通总结</h3>
+                    </div>
+                    <div class="summary-text compact">
+                        {{ getCommunicationSummary() }}
+                    </div>
+                </div>
+                <!-- 沟通记录 -->
+                <div class="card card-table">
+                    <div class="card-header">
+                        <h3><i class="fas fa-history"></i> 沟通记录</h3>
+                    </div>
+                    <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>
+                            </thead>
+                            <tbody>
+                            <tr v-for="record in communicationRecords" :key="record.id" class="record-row">
+                                <td class="record-cell">{{ (customerData && (customerData.customerName || customerData.name)) || '-' }}</td>
+                                <td class="record-cell">
+                                        <span class="risk-level-tag" :class="getRecordRiskLevelClass(record)">
+                                            {{ getRecordRiskLevelLabel(record) }}
+                                        </span>
+                                </td>
+                                <td class="record-cell">
+                                        <span class="intention-degree">
+                                            {{ getIntentionDegreeFromRecord(record) }}
+                                        </span>
+                                </td>
+                                <td class="record-cell">{{ parseTime(record.createTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</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>
+
+                        <!-- 分页组件 -->
+                        <div class="pagination-container" v-if="communicationRecordsTotal > 0">
+                            <el-pagination
+                                @current-change="handleCommunicationRecordsPageChange"
+                                @size-change="handleCommunicationRecordsSizeChange"
+                                :current-page="communicationRecordsPageNum"
+                                :page-sizes="[4, 10, 20, 50]"
+                                :page-size="communicationRecordsPageSize"
+                                layout="total, sizes, prev, pager, next, jumper"
+                                :total="communicationRecordsTotal"
+                            />
+                        </div>
+                    </div>
+                </div>
+                <!-- 微信风格聊天弹窗 -->
+                <div v-if="chatDialogVisible" class="chat-dialog-overlay" @click.self="closeChatDialog">
+                    <div class="chat-dialog">
+                        <div class="chat-dialog-header">
+                            <div class="chat-title">
+                                <i class="fas fa-comments"></i>
+                                <span>{{
+                                        (currentChatRecord && currentChatRecord.customerName) || (customerData && customerData.customerName)
+                                    }} - 历史聊天记录</span>
+                            </div>
+                            <button @click="closeChatDialog" class="btn-close">
+                                ×
+                            </button>
+                        </div>
+                        <div class="chat-dialog-body">
+                            <div class="chat-messages">
+                                <!-- 根据 aiChatRecord 数组循环显示聊天记录 -->
+                                <div
+                                    v-for="(msg, index) in parseChatMessages(currentChatRecord && currentChatRecord.aiChatRecord)"
+                                    :key="index"
+                                    class="message-item"
+                                    :class="msg.type === 'ai' ? 'message-left' : 'message-right'"
+                                >
+                                    <!-- 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')"/>
+                                        </div>
+                                        <div class="message-content">
+                                            <div class="message-name message-name-ai">AI</div>
+                                            <div class="message-bubble">
+                                                {{ msg.content }}
+                                            </div>
+                                        </div>
+                                    </div>
+
+                                    <!-- 客户消息:强制头像在右侧 -->
+                                    <div v-else class="message-item message-item-customer">
+                                        <div class="message-content-right">
+                                            <div class="message-bubble message-bubble-right">
+                                                {{ msg.content }}
+                                            </div>
+                                        </div>
+                                        <div class="message-avatar message-avatar-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">
+                                    <i class="fas fa-inbox"></i> 暂无聊天内容
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 流失风险等级 + 客户关注点 & 意向度 -->
+            <div class="right-column">
+                <!-- 流失风险等级 -->
+                <div class="card risk-card" :class="getRiskLevelClass()">
+                    <div class="card-header">
+                        <h3><i class="fas fa-chart-line"></i> 流失风险等级</h3>
+                        <span class="risk-badge" :class="getRiskLevelBadgeClass()">{{ getRiskLevelLabel() }}</span>
+                    </div>
+                    <div class="risk-analysis">
+                        <p class="risk-text">{{ getRiskLevelAnalysis() }}</p>
+                        <div class="risk-tip" v-if="getRiskLevelTip()">
+                            <i class="fas fa-exclamation-triangle"></i> {{ getRiskLevelTip() }}
+                        </div>
+                    </div>
+                </div>
+                <!-- 客户关注点 & 意向度 -->
+                <div class="card card-focus">
+                    <div class="card-header">
+                        <h3><i class="fas fa-lightbulb"></i> 客户关注点 & 意向度</h3>
+                    </div>
+                    <div class="focus-points">
+                        <div class="focus-title">
+                            <i class="fas fa-search"></i> 核心关注点:
+                        </div>
+                        <ul class="focus-list">
+                            <li class="focus-item">{{customerFocusPoints}}</li>
+                            <i class="fas fa-dot-circle"></i> {{ point }}
+                        </li>
+                        </ul>
+                    </div>
+                    <div class="intention-section">
+                        <div class="intention-header">
+                            <span class="intention-label">客户意向度</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>
+                            </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>
+            </div>
+        </div>
+        <el-dialog
+        title="AI分析标签"
+        :visible.sync="analyzeTagDialogVisible"
+        width="420px"
+        append-to-body
+    >
+        <el-form label-width="90px">
+            <el-form-item label="行业类型">
+                <el-select
+                    v-model="selectedTradeType"
+                    placeholder="请选择行业类型"
+                    clearable
+                    style="width: 100%;"
+                >
+                    <el-option
+                        v-for="item in tradeTypeOptions"
+                        :key="item.dictValue"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+            <el-button @click="analyzeTagDialogVisible = false">取 消</el-button>
+            <el-button type="primary" :loading="analyzeTagSubmitting" @click="confirmAnalyzeTag">确 定</el-button>
+        </div>
+    </el-dialog>
+    </div>
+
+
+</template>
+
+<script>
+
+import {listByCustomerId,analyzeAiTagByTrade} from "../../../api/qw/customerProperty";
+import {listAnalyze} from "../../../api/qw/qwAnalyze";
+
+export default {
+    props: {
+        // 抽屉模式参数(父组件传入)
+        analyzeUserId: {
+            type: [String, Number],
+            default: null
+        },
+        analyzeExternalUserId: {
+            type: String,
+            default: null
+        },
+        analyzeCorpId: {
+            type: String,
+            default: null
+        },
+        customerRow: {
+            type: Object,
+            default: null
+        }
+    },
+    data() {
+        return {
+            userId: null,
+            externalUserId: null,
+            corpId: null,
+            customerData: null, // 从列表页传递过来的完整客户数据
+            aiTags: [],// 需要显示的 AI 标签
+            allAiTags: [], // 存储所有 AI 标签
+            tagsPageSize: 5,//默认展开标签的数量
+            isExpanded: false, // 是否已展开显示全部标签
+            // 聊天记录分页相关
+            communicationRecords: [],
+            communicationRecordsTotal: 0,
+            communicationRecordsPageNum: 1,
+            communicationRecordsPageSize: 4,
+            // 聊天弹窗相关
+            chatDialogVisible: false, // 聊天弹窗是否显示
+            currentChatRecord: null, // 当前查看的聊天记录
+            analyzeTagDialogVisible: false,
+            analyzeTagSubmitting: false,
+            selectedTradeType: null,
+            tradeTypeOptions:[],
+        }
+    },
+    computed: {
+        // 根据是否展开控制显示的标签数量
+        visibleTags() {
+            if (this.isExpanded) {
+                return this.allAiTags;
+            } else {
+                // 未展开时只显示前 3 条
+                return this.allAiTags.slice(0, this.tagsPageSize);
+            }
+        },
+        // 客户画像数据(从最新的沟通记录中获取)
+        customerPortraitData() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return {};
+            }
+            // 获取最新的沟通记录
+            const latestRecord = this.communicationRecords[0];
+            if (latestRecord && latestRecord.customerPortraitJson) {
+                try {
+                    // 如果是字符串,解析为 JSON 对象
+                    if (typeof latestRecord.customerPortraitJson === 'string') {
+                        return JSON.parse(latestRecord.customerPortraitJson);
+                    }
+                    // 如果已经是对象,直接返回
+                    return latestRecord.customerPortraitJson;
+                } catch (error) {
+                    console.error('解析客户画像 JSON 失败:', error);
+                    return {};
+                }
+            }
+            return {};
+        },
+        // 客户关注点数据(从最新的沟通记录中获取)
+        customerFocusPoints() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return ['暂无分析数据'];
+            }
+            const latestRecord = this.communicationRecords[0];
+            if (latestRecord && latestRecord.customerFocusJson) {
+                return this.normalizeFocusPoints(latestRecord.customerFocusJson);
+            }
+            return ['暂无分析数据'];
+        }
+    },
+    created() {
+        this.initFromParentOrRoute();
+        this.getDicts("trade_type").then((response) => {
+      this.tradeTypeOptions = response.data;
+    });
+    },
+    watch: {
+        analyzeUserId: {
+            immediate: false,
+            handler() {
+                // 抽屉重复打开/切换客户时刷新
+                this.initFromParentOrRoute();
+            }
+        },
+        analyzeExternalUserId() {
+            this.initFromParentOrRoute();
+        },
+        analyzeCorpId() {
+            this.initFromParentOrRoute();
+        }
+    },
+    methods: {
+        normalizeFocusPoints(value) {
+            if (value === null || value === undefined) return [];
+            if (Array.isArray(value)) {
+                return value.map(v => String(v)).filter(Boolean);
+            }
+
+            // 字符串:可能是 JSON 数组字符串,也可能是普通字符串
+            if (typeof value === 'string') {
+                const raw = value.trim();
+                if (!raw) return [];
+
+                // JSON 数组 / JSON 字符串尝试解析
+                if (
+                    (raw.startsWith('[') && raw.endsWith(']')) ||
+                    (raw.startsWith('"') && raw.endsWith('"')) ||
+                    (raw.startsWith("'") && raw.endsWith("'"))
+                ) {
+                    try {
+                        const parsed = JSON.parse(raw);
+                        if (Array.isArray(parsed)) {
+                            return parsed.map(v => String(v)).map(s => s.trim()).filter(Boolean);
+                        }
+                        if (typeof parsed === 'string') {
+                            return this.normalizeFocusPoints(parsed);
+                        }
+                    } catch (e) {
+                        // ignore,走后面的兜底清洗
+                    }
+                }
+
+                // 兜底:去掉中括号/引号后按分隔符拆分
+                let cleaned = raw;
+                if (cleaned.startsWith('[') && cleaned.endsWith(']')) {
+                    cleaned = cleaned.slice(1, -1);
+                }
+                cleaned = cleaned.replace(/["']/g, '');
+
+                const parts = cleaned
+                    .split(/[,,、;;\n]/g)
+                    .map(s => s.trim())
+                    .filter(Boolean);
+
+                return parts.length ? parts : [cleaned.trim()].filter(Boolean);
+            }
+
+            // 其它类型兜底
+            return [String(value)].filter(Boolean);
+        },
+        initFromParentOrRoute() {
+            // 优先用父组件传参(抽屉模式)
+            if (
+                (this.analyzeUserId !== null && this.analyzeUserId !== undefined && this.analyzeUserId !== '') ||
+                (this.analyzeExternalUserId !== null && this.analyzeExternalUserId !== undefined && this.analyzeExternalUserId !== '')
+            ) {
+                this.userId = this.analyzeUserId;
+                this.externalUserId = this.analyzeExternalUserId || (this.customerRow && (this.customerRow.externalUserId || this.customerRow.id));
+                this.corpId = this.analyzeCorpId;
+                this.customerData = this.customerRow || null;
+            } else {
+                // 路由模式兜底
+                this.userId = this.$route.query.userId || this.$route.query.qwUserId || this.$route.params.userId || this.$route.params.qwUserId;
+                this.externalUserId = this.$route.query.externalUserId || this.$route.query.id || this.$route.params.externalUserId || this.$route.params.id;
+                this.corpId = this.$route.query.corpId || this.$route.params.corpId;
+                if (this.$route.query.customerData) {
+                    try {
+                        this.customerData = JSON.parse(this.$route.query.customerData);
+                    } catch (error) {
+                        console.error('解析客户数据失败:', error);
+                    }
+                }
+            }
+
+            // 重置分页(切换客户时)
+            this.communicationRecordsPageNum = 1;
+            // 获取客户标签
+            this.loadCustomerTags();
+            // 加载客户分析信息
+            this.getCustomerInfoList();
+        },
+        loadCustomerTags() {
+            const params = {
+                externalUserId: this.externalUserId,
+                corpId: this.corpId,
+                qwUserId: this.userId,
+            };
+            return listByCustomerId(params).then((response) => {
+                if (response.code === 200) {
+                    this.allAiTags = response.data || [];
+                    // 强制 Vue 更新视图
+                    this.$forceUpdate();
+                } else {
+                    console.error('获取 AI 标签失败:', response);
+                }
+            }).catch(error => {
+                console.error('获取 AI 标签异常:', error);
+            });
+        },
+        handleAnalyzeTag() {
+            this.analyzeTagDialogVisible = true;
+        },
+        confirmAnalyzeTag() {
+            if (!this.selectedTradeType) {
+                this.$message.warning('请选择行业类型');
+                return;
+            }
+            if (!this.externalUserId || !this.corpId || !this.userId) {
+                this.$message.warning('客户参数不完整,无法分析标签');
+                return;
+            }
+            const data = {
+                externalUserId: this.externalUserId,
+                corpId: this.corpId,
+                qwUserId: this.userId,
+                tradeType: this.selectedTradeType
+            };
+            this.analyzeTagSubmitting = true;
+            analyzeAiTagByTrade(data).then((res) => {
+                if (res && res.code === 200) {
+                    this.$message.success(res.msg || 'AI分析标签成功');
+                    this.analyzeTagDialogVisible = false;
+                    this.loadCustomerTags();
+                } else {
+                    this.$message.error((res && res.msg) || 'AI分析标签失败');
+                }
+            }).catch(() => {
+                this.$message.error('AI分析标签失败');
+            }).finally(() => {
+                this.analyzeTagSubmitting = false;
+            });
+        },
+        getCustomerInfoList() {
+            const params = {
+                pageNum: this.communicationRecordsPageNum,
+                pageSize: this.communicationRecordsPageSize,
+                qwUserId: this.userId,
+                externalUserId: this.externalUserId,
+                corpId: this.corpId
+            };
+            return listAnalyze(params).then((response) => {
+                if (response.code === 200) {
+                    this.communicationRecords = response.rows || [];
+                    this.communicationRecordsTotal = response.total || 0;
+                } else {
+                    console.error('获取客户信息失败:', response);
+                }
+            }).catch(error => {
+                console.error('获取客户信息异常:', error);
+            });
+        },
+        // 加载更多标签 - 显示全部
+        loadMoreTags() {
+            this.isExpanded = true;
+        },
+        // 收起标签 - 只显示前 3 条
+        collapseTags() {
+            this.isExpanded = false;
+        },
+        // 查看聊天内容
+        viewChat(record) {
+            this.currentChatRecord = record;
+            this.chatDialogVisible = true;
+        },
+        // 关闭聊天弹窗
+        closeChatDialog() {
+            this.chatDialogVisible = false;
+            this.currentChatRecord = null;
+        },
+        // 解析聊天消息数组
+        parseChatMessages(content) {
+            if (!content) {
+                return [];
+            }
+            // 如果 content 是字符串,尝试解析为 JSON 数组
+            if (typeof content === 'string') {
+                try {
+                    const parsed = JSON.parse(content);
+                    // 如果是数组,直接返回
+                    if (Array.isArray(parsed)) {
+                        return parsed.map(item => ({
+                            content: item.ai || item.user,
+                            type: item.ai ? 'ai' : 'user'
+                        }));
+                    }
+                    // 如果是对象,转换为数组
+                    return [{content: parsed.content, type: parsed.type || 'ai'}];
+                } catch (e) {
+                    // 解析失败,返回空数组
+                    console.error('解析聊天记录失败:', e);
+                    return [];
+                }
+            }
+            // 如果已经是数组,直接返回
+            if (Array.isArray(content)) {
+                return content;
+            }
+            // 如果是对象,转换为数组
+            return [content];
+        },
+        // 处理头像加载失败
+        handleAvatarError(event, type) {
+            const img = event.target;
+            if (type === 'ai') {
+                // AI 头像加载失败时,使用渐变色背景 + 机器人图标
+                img.style.display = 'none';
+                img.parentElement.innerHTML = '<i class="fas fa-robot" style="font-size: 24px; color: white;"></i>';
+            } else {
+                // 客户头像加载失败时,使用渐变色背景 + 用户图标
+                img.style.display = 'none';
+                img.parentElement.innerHTML = '<i class="fas fa-user" style="font-size: 24px; color: white;"></i>';
+            }
+        },
+
+        // 分页改变事件
+        handleCommunicationRecordsPageChange(pageNum) {
+            this.communicationRecordsPageNum = pageNum;
+            this.getCustomerInfoList();
+        },
+        // 每页条数改变事件
+        handleCommunicationRecordsSizeChange(pageSize) {
+            this.communicationRecordsPageSize = pageSize;
+            this.communicationRecordsPageNum = 1; // 重置为第一页
+            this.getCustomerInfoList();
+        },
+        // 获取沟通摘要
+        getCommunicationAbstract() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无沟通记录';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.communicationAbstract || '暂无沟通摘要';
+        },
+        // 获取 AI 沟通总结
+        getCommunicationSummary() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无沟通记录';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.communicationSummary || '暂无 AI 沟通总结';
+        },
+        // 获取最后更新时间
+        getUpdateTime() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '-';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.createTime || '-';
+        },
+        // 获取流失风险等级数值
+        getAttritionLevel() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return 0;
+            }
+            const latestRecord = this.communicationRecords[0];
+            return parseInt(latestRecord.attritionLevel) || 0;
+        },
+        // 获取流失风险等级标签
+        getRiskLevelLabel() {
+            const level = this.getAttritionLevel();
+            const labels = ['未知', '无风险', '低风险', '中风险', '高风险'];
+            return labels[level] || '未知';
+        },
+        // 获取流失风险等级徽章样式类
+        getRiskLevelBadgeClass() {
+            const level = this.getAttritionLevel();
+            const badgeClasses = [
+                'badge-unknown',
+                'badge-none',
+                'badge-low',
+                'badge-medium',
+                'badge-high'
+            ];
+            return badgeClasses[level] || 'badge-unknown';
+        },
+        // 获取客户意向度
+        getIntentionDegree() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '';
+            }
+            const latestRecord = this.communicationRecords[0];
+            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) {
+            if (!record) return 0;
+            return parseInt(record.attritionLevel) || 0;
+        },
+        // 获取单条记录的风险等级标签
+        getRecordRiskLevelLabel(record) {
+            const level = this.getRecordAttritionLevel(record);
+            const labels = ['未知', '无风险', '低风险', '中风险', '高风险'];
+            return labels[level] || '未知';
+        },
+        // 获取单条记录的风险等级样式类
+        getRecordRiskLevelClass(record) {
+            const level = this.getRecordAttritionLevel(record);
+            const classes = ['risk-unknown', 'risk-none', 'risk-low', 'risk-medium', 'risk-high'];
+            return classes[level] || 'risk-unknown';
+        },
+        // 获取单条记录的客户意向度
+        getIntentionDegreeFromRecord(record) {
+            if (!record) return 0;
+            return record.intentionDegree;
+        },
+        // 获取流失风险等级样式类
+        getRiskLevelClass() {
+            const level = this.getAttritionLevel();
+            const classes = ['risk-unknown', 'risk-none', 'risk-low', 'risk-medium', 'risk-high'];
+            return classes[level] || 'risk-unknown';
+        },
+        // 获取流失风险分析
+        getRiskLevelAnalysis() {
+            const level = this.getAttritionLevel();
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无分析数据';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.attritionLevelPrompt || "暂无分析数据";
+        },
+        // 获取流失风险提示
+        getRiskLevelTip() {
+            const level = this.getAttritionLevel();
+            const tips = [
+                '暂无分析',
+                '客户稳定,可以放心。',
+                '建议定期回访,了解客户最新需求。',
+                '建议安排专项跟进,深入了解客户痛点和需求。',
+                '建议立即联系客户,了解问题原因并提供解决方案。'
+            ];
+            return tips[level] || '';
+        },
+    }
+}</script>
+
+<style scoped>
+* {
+    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: 8px;
+    min-height: calc(100vh - 50px);
+}
+
+@keyframes pulse {
+    0%, 100% {
+        transform: scale(1);
+    }
+    50% {
+        transform: scale(1.1);
+    }
+}
+
+.main-grid-three-columns {
+    display: grid;
+    grid-template-columns: 380px 1fr 340px;
+    gap: 12px;
+    animation: fadeIn 0.6s ease-in-out;
+    align-items: stretch;
+}
+
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+@media (max-width: 1400px) {
+    .main-grid-three-columns {
+        grid-template-columns: 360px 1fr 320px;
+        gap: 24px;
+    }
+}
+
+@media (max-width: 1200px) {
+    .main-grid-three-columns {
+        grid-template-columns: 1fr;
+    }
+}
+
+.card {
+    background: white;
+    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;
+    overflow: hidden;
+}
+
+.card::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 3px;
+    background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+    transform: scaleX(0);
+    transition: transform 0.4s ease;
+}
+
+.card:hover::before {
+    transform: scaleX(1);
+}
+
+.card:hover {
+    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
+    transform: translateY(-4px);
+    border-color: rgba(102, 126, 234, 0.3);
+}
+
+/* 高亮卡片(AI 总结) */
+.card-highlight {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    border: none;
+    box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4);
+    position: relative;
+    overflow: hidden;
+}
+
+.card-highlight::after {
+    content: '';
+    position: absolute;
+    top: -50%;
+    right: -50%;
+    width: 200%;
+    height: 200%;
+    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%);
+    }
+}
+
+.card-highlight .card-header h3 {
+    color: white;
+}
+
+.card-highlight .summary-text {
+    color: white;
+    font-size: 17px;
+    line-height: 1.6;
+    max-height: 120px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar {
+    width: 6px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-track {
+    background: rgba(255, 255, 255, 0.2);
+    border-radius: 3px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-thumb {
+    background: rgba(255, 255, 255, 0.4);
+    border-radius: 3px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-thumb:hover {
+    background: rgba(255, 255, 255, 0.6);
+}
+
+.card-highlight .summary-meta span {
+    color: rgba(255, 255, 255, 0.9);
+}
+
+/* 表格卡片 */
+.card-table {
+    background: white;
+}
+
+/* 风险卡片 */
+.risk-card {
+    border-left: 4px solid #10b981;
+    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+    transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.risk-card.risk-unknown {
+    border-left-color: #6b7280;
+    background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
+}
+
+.risk-card.risk-unknown::before {
+    background: linear-gradient(90deg, #6b7280 0%, #4b5563 100%);
+}
+
+.risk-card::before {
+    background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
+}
+
+.risk-unknown .risk-badge {
+    background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
+    box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3);
+}
+
+.risk-none .risk-badge {
+    background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+    box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+}
+
+.risk-low .risk-badge {
+    background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
+    box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
+}
+
+.risk-medium .risk-badge {
+    background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
+    box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
+}
+
+.risk-high .risk-badge {
+    background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
+    box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
+}
+
+.risk-card:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.1);
+}
+
+/* 风险等级标签 */
+.risk-badge {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 6px 14px;
+    border-radius: 8px;
+    font-size: 16px;
+    font-weight: 700;
+    color: white;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+}
+
+.risk-badge::before {
+    content: '';
+    width: 6px;
+    height: 6px;
+    background: white;
+    border-radius: 50%;
+    animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+    0%, 100% {
+        opacity: 1;
+        transform: scale(1);
+    }
+    50% {
+        opacity: 0.5;
+        transform: scale(1.2);
+    }
+}
+
+.risk-card:hover .risk-badge {
+    transform: scale(1.05);
+}
+
+/* 风险分析内容 */
+.risk-analysis {
+    margin-top: 10px;
+    padding: 10px;
+    background: rgba(255, 255, 255, 0.7);
+    border-radius: 10px;
+    backdrop-filter: blur(10px);
+    max-height: 180px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.risk-analysis::-webkit-scrollbar {
+    width: 6px;
+}
+
+.risk-analysis::-webkit-scrollbar-track {
+    background: rgba(255, 255, 255, 0.5);
+    border-radius: 3px;
+}
+
+.risk-analysis::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.risk-analysis::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.risk-text {
+    font-size: 16px;
+    line-height: 1.7;
+    color: #475569;
+    margin: 0;
+}
+
+.risk-tip {
+    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: 6px;
+    font-size: 15px;
+    color: #92400e;
+    display: flex;
+    align-items: flex-start;
+    gap: 6px;
+}
+
+.risk-tip i {
+    font-size: 12px;
+    margin-top: 1px;
+    color: #f59e0b;
+}
+
+/* 关注点卡片 */
+.card-focus {
+    background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+    border: 2px solid #bae6fd;
+    box-shadow: 0 4px 16px rgba(186, 230, 253, 0.3);
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #eef2ff;
+    padding-bottom: 6px;
+    margin-bottom: 10px;
+    flex-wrap: wrap;
+}
+
+.card-header h3 {
+    font-size: 19px;
+    font-weight: 700;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    color: #0f172a;
+    letter-spacing: -0.02em;
+}
+
+.records-table-wrapper {
+    overflow-x: auto;
+}
+
+/* 分页容器样式 */
+.pagination-container {
+    padding: 16px 0 12px;
+    display: flex;
+    justify-content: center;
+    background: white;
+    border-top: 1px solid #f1f5f9;
+    margin-top: 12px;
+}
+
+/* Element UI 分页样式覆盖 */
+
+.pagination-container .el-pagination li {
+    min-width: 32px;
+    height: 32px;
+    line-height: 32px;
+    border-radius: 6px;
+    transition: all 0.3s ease;
+}
+
+.pagination-container .el-pagination li:hover {
+    background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+    border-color: #667eea;
+}
+
+.records-table {
+    width: 100%;
+    border-collapse: collapse;
+    font-size: 15px;
+}
+
+.records-table thead {
+    background: transparent;
+    color: #475569;
+}
+
+.records-table th {
+    padding: 12px 16px;
+    text-align: center !important;
+    font-weight: 600;
+    font-size: 14px;
+    border-bottom: 2px solid #e2e8f0;
+    color: #64748b;
+}
+
+.records-table th i {
+    margin-right: 6px;
+    opacity: 0.8;
+}
+
+.records-table tbody tr {
+    border-bottom: 1px solid #f1f5f9;
+    transition: all 0.2s ease;
+}
+
+.records-table tbody tr:hover {
+    background-color: #f8fafc;
+    transform: none;
+    box-shadow: none;
+}
+
+.records-table td {
+    padding: 12px 16px;
+    vertical-align: middle;
+}
+
+.record-cell {
+    font-size: 15px;
+    color: #334155;
+    text-align: center !important;
+}
+
+/* 风险等级标签 */
+.risk-level-tag {
+    display: inline-block;
+    padding: 4px 12px;
+    border-radius: 6px;
+    font-size: 13px;
+    font-weight: 500;
+    border: 1px solid;
+}
+
+/* 客户意向度 */
+.intention-degree {
+    display: inline-block;
+    padding: 4px 12px;
+    border-radius: 6px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #6366f1;
+    background: rgba(99, 102, 241, 0.08);
+    border: 1px solid rgba(99, 102, 241, 0.2);
+}
+
+.btn-view-chat {
+    background: transparent;
+    color: #667eea;
+    border: 1px solid #667eea;
+    padding: 6px 12px;
+    border-radius: 6px;
+    font-size: 14px;
+    cursor: pointer;
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    transition: all 0.2s ease;
+}
+
+.btn-view-chat:hover {
+    background: #f0f4ff;
+}
+
+.empty-tip {
+    color: #94a3b8;
+    font-size: 16px;
+    text-align: center;
+    padding: 40px 20px;
+    background: transparent;
+    border: none;
+}
+
+/* 沟通摘要样式 */
+.summary-text.compact {
+    max-height: 100px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    line-height: 1.6;
+    font-size: 16px;
+    color: #475569;
+    padding-right: 4px;
+}
+
+.summary-text.compact::-webkit-scrollbar {
+    width: 6px;
+}
+
+.summary-text.compact::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.summary-text.compact::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.summary-text.compact::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+/* AI 标签美化样式 */
+.tags-container {
+    padding: 0;
+    min-height: calc(16px * 5 + 6px * 4 + 12px * 2);
+    overflow-y: auto;
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+}
+
+.tags-container::-webkit-scrollbar {
+    width: 6px;
+}
+
+.tags-container::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.tags-container::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.tags-container::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+/* 标签列表 - 每行一个 */
+.tags-list {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+    margin-bottom: 6px;
+}
+
+.tag-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 6px 10px;
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border: 1px solid #e2e8f0;
+    border-radius: 6px;
+    font-size: 15px;
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    cursor: default;
+    position: relative;
+    overflow: hidden;
+}
+
+.tag-item::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 4px;
+    height: 100%;
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+    transition: width 0.3s ease;
+}
+
+.tag-item:hover {
+    background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
+    border-color: #cbd5e1;
+    transform: translateX(6px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.tag-item:hover::before {
+    width: 5px;
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+}
+
+.tag-highlight {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+}
+
+.tag-highlight::before {
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+}
+
+.tag-key {
+    font-weight: 600;
+    color: #475569;
+    white-space: nowrap;
+    flex-shrink: 0;
+    font-size: 15px;
+}
+
+.tag-separator {
+    color: #94a3b8;
+    font-weight: 300;
+    flex-shrink: 0;
+    font-size: 15px;
+}
+
+.tag-value {
+    color: #1e293b;
+    font-weight: 500;
+    word-break: break-word;
+    flex: 1;
+    min-width: 0;
+    font-size: 15px;
+}
+
+.empty-tags {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 40px 20px;
+    color: #94a3b8;
+    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;
+}
+
+.empty-tags i {
+    font-size: 32px;
+    margin-bottom: 12px;
+    opacity: 0.5;
+}
+
+.tags-actions {
+    display: flex;
+    justify-content: center;
+    padding: 16px 0;
+    border-top: 1px solid #f1f5f9;
+    margin-top: auto;
+    background: white;
+    position: sticky;
+    bottom: 0;
+    z-index: 10;
+}
+
+.btn-expand-tags,
+.btn-collapse-tags {
+    background: transparent;
+    color: #667eea;
+    border: 1px solid #667eea;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-size: 16px;
+    cursor: pointer;
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    transition: all 0.2s ease;
+}
+
+.btn-expand-tags:hover,
+.btn-collapse-tags:hover {
+    background: rgba(102, 126, 234, 0.05);
+    border-color: #5a67d8;
+    color: #5a67d8;
+}
+
+.btn-expand-tags:active,
+.btn-collapse-tags:active {
+    transform: scale(0.98);
+}
+
+/* 微信风格聊天弹窗 */
+.chat-dialog-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 9999;
+    backdrop-filter: blur(4px);
+}
+
+.chat-dialog {
+    background: white;
+    border-radius: 16px;
+    width: 90%;
+    max-width: 800px;
+    height: 600px;
+    display: flex;
+    flex-direction: column;
+    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+    animation: slideIn 0.3s ease-out;
+    position: relative;
+}
+
+@keyframes slideIn {
+    from {
+        opacity: 0;
+        transform: translateY(-20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.chat-dialog-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 16px 20px;
+    border-bottom: 1px solid #eef2ff;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    border-radius: 16px 16px 0 0;
+    color: white;
+}
+
+.chat-title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-size: 16px;
+    font-weight: 700;
+}
+
+.btn-close {
+    position: absolute;
+    top: 16px;
+    right: 16px;
+    background: white;
+    border: 2px solid #e2e8f0;
+    color: #1a202c;
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.3s ease;
+    font-size: 24px;
+    font-weight: bold;
+    line-height: 1;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    z-index: 10;
+}
+
+.btn-close:hover {
+    background: #ef4444;
+    border-color: #ef4444;
+    color: white;
+    transform: rotate(90deg) scale(1.1);
+    box-shadow: 0 4px 16px rgba(239, 68, 68, 0.4);
+}
+
+.chat-dialog-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 20px;
+    background: #f5f7fa;
+}
+
+.chat-messages {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+}
+
+.message-item {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 8px;
+}
+
+.message-left {
+    justify-content: flex-start;
+}
+
+.message-right {
+    justify-content: flex-end;
+}
+
+/* 客户消息强制布局:头像在右 */
+.message-item-customer {
+    display: flex !important;
+    flex-direction: row !important;
+    justify-content: flex-end !important;
+    gap: 10px !important;
+}
+
+.message-wrapper {
+    display: flex;
+    align-items: flex-start;
+    gap: 10px;
+    max-width: 75%;
+}
+
+.message-wrapper-left {
+    flex-direction: row;
+}
+
+.message-name {
+    font-size: 12px;
+    color: #94a3b8;
+    white-space: nowrap;
+    text-align: left;
+    margin-bottom: 4px;
+    line-height: 1.2;
+}
+
+.message-name-ai {
+    color: #667eea;
+    font-weight: 500;
+}
+
+.message-avatar {
+    width: 32px;
+    height: 32px;
+    border-radius: 6px;
+    overflow: hidden;
+    flex-shrink: 0;
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
+    background: #f0f0f0;
+}
+
+.message-avatar img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    transition: transform 0.3s ease;
+}
+
+.message-avatar:hover img {
+    transform: scale(1.1);
+}
+
+.message-wrapper-left .message-avatar {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.message-wrapper-right .message-avatar {
+    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.message-content {
+    display: flex;
+    flex-direction: column;
+    max-width: calc(100% - 42px);
+}
+
+.message-wrapper-left .message-content {
+    align-items: flex-start;
+    margin-left: 4px;
+}
+
+.message-wrapper-right .message-content {
+    align-items: flex-end !important;
+    margin-right: 4px;
+}
+
+/* 客户聊天内容容器 */
+.message-content-right {
+    flex: 0 0 auto !important;
+    max-width: calc(100% - 42px) !important;
+    display: flex;
+    align-items: flex-start !important;
+}
+
+.message-bubble {
+    background: white;
+    padding: 9px 13px;
+    border-radius: 6px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: #0f172a;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+    word-break: break-word;
+    position: relative;
+    max-width: 500px;
+}
+
+.message-bubble::before {
+    content: '';
+    position: absolute;
+    left: -6px;
+    top: 12px;
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-right: 6px solid white;
+}
+
+.message-bubble-right {
+    background: #d9fdd3;
+    color: #0f172a;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+    display: inline-block;
+    position: relative;
+}
+
+.message-bubble-right::before {
+    content: '';
+    position: absolute;
+    right: -6px;
+    left: auto;
+    top: 16px !important;
+    transform: none !important;
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-left: 6px solid #d9fdd3;
+    border-right: none;
+}
+
+.empty-chat-tip {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+    color: #94a3b8;
+    font-size: 14px;
+}
+
+.empty-chat-tip i {
+    font-size: 48px;
+    margin-bottom: 12px;
+    opacity: 0.5;
+}
+
+/* 客户画像样式 */
+.profile-grid {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    max-height: 350px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+    padding-top: 0;
+}
+
+.profile-grid::-webkit-scrollbar {
+    width: 6px;
+}
+
+.profile-grid::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.profile-grid::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.profile-grid::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.profile-item {
+    display: grid;
+    grid-template-columns: 42% 58%;
+    align-items: flex-start;
+    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;
+    word-break: break-word;
+}
+
+.profile-item:hover {
+    background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
+    transform: translateX(4px);
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
+    border-color: #cbd5e1;
+}
+
+.profile-item-main {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.profile-item-main:hover {
+    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+}
+
+.profile-item-full {
+    grid-template-columns: 42% 58%;
+}
+
+.profile-item .label {
+    font-size: 15px;
+    color: #64748b;
+    font-weight: 600;
+    white-space: normal;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    line-height: 1.4;
+    min-width: 0;
+}
+
+.profile-item .label i {
+    color: #94a3b8;
+    font-size: 12px;
+    width: 14px;
+    text-align: center;
+    flex-shrink: 0;
+}
+
+.profile-item .value {
+    font-size: 16px;
+    color: #0f172a;
+    font-weight: 500;
+    word-break: break-word;
+    line-height: 1.4;
+    min-width: 0;
+}
+
+.profile-item .value.highlight {
+    color: #0369a1;
+    font-size: 16px;
+    font-weight: 600;
+}
+
+.profile-item .value.long-text {
+    color: #334155;
+    font-weight: 400;
+}
+
+.update-time-corner {
+    position: absolute;
+    bottom: 12px;
+    right: 16px;
+    font-size: 12px;
+    color: #94a3b8;
+    font-style: italic;
+}
+
+/* 客户关注点 & 意向度样式 */
+.focus-points {
+    padding: 0;
+    margin-bottom: 10px;
+}
+
+.focus-title {
+    font-size: 17px;
+    color: #64748b;
+    font-weight: 600;
+    margin-bottom: 8px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.focus-title i {
+    color: #3b82f6;
+    font-size: 14px;
+}
+
+.focus-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+    max-height: 200px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.focus-list::-webkit-scrollbar {
+    width: 6px;
+}
+
+.focus-list::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.focus-list::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.focus-list::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.focus-item {
+    display: flex;
+    align-items: flex-start;
+    gap: 6px;
+    padding: 6px 10px;
+    margin-bottom: 6px;
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border-radius: 6px;
+    border: 1px solid #e2e8f0;
+    transition: all 0.3s ease;
+    font-size: 15px;
+    color: #334155;
+    line-height: 1.5;
+}
+
+.focus-item:hover {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+    transform: translateX(4px);
+    box-shadow: 0 2px 6px rgba(59, 130, 246, 0.1);
+}
+
+.focus-item i {
+    color: #3b82f6;
+    font-size: 10px;
+    margin-top: 1px;
+    flex-shrink: 0;
+}
+
+/* 意向度样式 - 水印风格 */
+.intention-section {
+    margin-top: 10px;
+    padding-top: 8px;
+    border-top: 1px solid #e2e8f0;
+}
+
+.intention-header {
+    margin-bottom: 6px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.intention-info-icon {
+    color: #94a3b8;
+    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: 16px;
+    font-weight: 600;
+    color: #64748b;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+}
+
+.intention-label::before {
+    content: '';
+    width: 3px;
+    height: 14px;
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+    border-radius: 2px;
+}
+
+/* 水印风格意向度显示 - 按等级着色 */
+.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;
+}
+
+/* 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);
+}
+
+.intention-grade-a::after {
+    content: 'A';
+    position: absolute;
+    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;
+}
+
+/* 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);
+}
+
+.intention-grade-b::after {
+    content: 'B';
+    position: absolute;
+    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;
+}
+
+/* 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);
+}
+
+.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 {
+    font-size: 20px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+    background-clip: text;
+}
+
+.card-highlight .card-header h3 i {
+    color: white;
+    background: none;
+    -webkit-text-fill-color: white;
+}
+
+/* 统一为 CRM 客户分析报告风格(覆盖) */
+.customer-container {
+    max-width: 100%;
+    padding: 12px;
+    background: #f4f6fa;
+}
+
+.main-grid-three-columns {
+    grid-template-columns: 300px minmax(640px, 1fr) 320px;
+    gap: 12px;
+    align-items: start;
+}
+
+.card {
+    border-radius: 10px;
+    border: 1px solid #e6ebf2;
+    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
+    margin-bottom: 10px;
+    padding: 12px;
+    background: #fff;
+}
+
+.card::before {
+    display: none;
+}
+
+.card:hover {
+    transform: none;
+    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
+    border-color: #dbe4f0;
+}
+
+.card-header {
+    border-bottom: 1px solid #edf1f7;
+    margin-bottom: 10px;
+    padding-bottom: 8px;
+}
+
+.card-header h3 {
+    font-size: 16px;
+    font-weight: 600;
+    color: #1f2937;
+}
+
+.card-header h3 i {
+    width: 18px;
+    height: 18px;
+    border-radius: 4px;
+    font-size: 10px;
+    background: #4f7cff;
+    box-shadow: none;
+    -webkit-text-fill-color: #fff;
+}
+
+.summary-text.compact {
+    font-size: 14px;
+    line-height: 1.75;
+    color: #334155;
+    max-height: 132px;
+}
+
+.risk-card,
+.card-focus {
+    background: #fff;
+    border: 1px solid #e6ebf2;
+}
+
+.risk-analysis {
+    background: #f8fafc;
+    border: 1px solid #edf2f7;
+}
+
+/* 风险徽章改为标签风格 */
+.risk-badge {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 5px 10px;
+    border-radius: 10px;
+    font-size: 13px;
+    font-weight: 600;
+    border: 1px solid #e5eaf1;
+    box-shadow: none;
+    transition: all 0.2s ease;
+    text-shadow: none;
+}
+
+.risk-badge::before {
+    content: '';
+    width: 8px;
+    height: 8px;
+    border-radius: 3px;
+    background: currentColor;
+    opacity: 0.35;
+}
+
+.risk-card:hover .risk-badge {
+    transform: translateY(-1px);
+}
+
+.risk-unknown .risk-badge {
+    background: #f8fafc;
+    border-color: #e5eaf1;
+    color: #64748b;
+}
+
+.risk-none .risk-badge {
+    background: #f0fdf4;
+    border-color: #bbf7d0;
+    color: #16a34a;
+}
+
+.risk-low .risk-badge {
+    background: #eff6ff;
+    border-color: #bfdbfe;
+    color: #2563eb;
+}
+
+.risk-medium .risk-badge {
+    background: #fffbeb;
+    border-color: #fde68a;
+    color: #d97706;
+}
+
+.risk-high .risk-badge {
+    background: #fef2f2;
+    border-color: #fecaca;
+    color: #dc2626;
+}
+
+/* 客户意向度:回滚为原来的“大水印渐变”样式(使用上方原始定义) */
+
+.tag-item,
+.focus-item,
+.profile-item {
+    background: #f8fafc;
+    border-color: #e5eaf1;
+}
+
+.records-table th {
+    background: #f8fafc;
+    color: #475569;
+}
+
+.records-table tbody tr:hover {
+    background: #f8fbff;
+}
+
+
+</style>

+ 130 - 4
src/views/qw/externalContact/deptIndex.vue

@@ -365,7 +365,62 @@
           </div>
         </template>
       </el-table-column>
-
+      <el-table-column label="流失风险" align="center" prop="attritionLevel">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
+          <el-tag v-if="scope.row.attritionLevel === 0" type="info">未知</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 1" type="success">无风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 2" type="info">低风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 3" type="warning">中风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 4" type="danger">高风险</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="意向度" align="center" prop="intentionDegree">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.intentionDegree !== null && scope.row.intentionDegree !== undefined && scope.row.intentionDegree !== ''">
+            {{ scope.row.intentionDegree }}
+          </el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关注点" align="center" prop="customerFocusJson" width="220">
+        <template slot-scope="scope">
+          <div v-if="parseFocusPoints(scope.row.customerFocusJson).length" style="text-align: left;">
+            <el-tooltip
+              v-for="(item, index) in parseFocusPoints(scope.row.customerFocusJson).slice(0, 2)"
+              :key="index"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px; word-break: break-word;">
+                {{ item }}
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; max-width: 100%; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                {{ shortenText(item, 14) }}
+              </el-tag>
+            </el-tooltip>
+            <el-tooltip
+              v-if="parseFocusPoints(scope.row.customerFocusJson).length > 2"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px;">
+                <div
+                  v-for="(item, idx) in parseFocusPoints(scope.row.customerFocusJson).slice(2)"
+                  :key="'focus-more-' + idx"
+                  style="margin-bottom: 4px;"
+                >
+                  {{ item }}
+                </div>
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                +{{ parseFocusPoints(scope.row.customerFocusJson).length - 2 }}
+              </el-tag>
+            </el-tooltip>
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
       <el-table-column label="状态" align="center" prop="status" width="120px" >
         <template slot-scope="scope">
           <dict-tag :options="statusOptions" :value="scope.row.status"/>
@@ -422,6 +477,13 @@
           <el-tag v-else type="info"> 未绑定</el-tag>
         </template>
       </el-table-column>
+      <el-table-column label="是否下载APP" width="100px" align="center" fixed="right">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.isDownloadApp === 1 ? 'success' : 'info'">
+            {{ scope.row.isDownloadApp === 1 ? '已下载' : '未下载' }}
+          </el-tag>
+        </template>
+      </el-table-column>
       <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
         <template slot-scope="scope">
           <el-button
@@ -500,6 +562,13 @@
             @click="healthHandledetails(scope.row)"
           >健康档案
           </el-button>
+          <el-button
+          v-if="scope.row.attritionLevel !== null && scope.row.attritionLevel !== undefined"
+          size="mini"
+          type="text"
+          @click="openAiDrawer(scope.row)"
+          v-hasPermi="['qw:externalContact:analyze:list']"
+        >AI 分析</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -793,6 +862,20 @@
       :visible.sync="showUser.open">
       <userDetails  ref="userDetails" />
     </el-drawer>
+    <el-drawer
+    size="75%"
+    :title="aiAnalyze.title"
+    :visible.sync="aiAnalyze.open"
+    append-to-body
+  >
+    <customer-detail
+      ref="customerAiDetail"
+      :analyze-user-id="aiAnalyze.userId"
+      :analyze-external-user-id="aiAnalyze.externalUserId"
+      :analyze-corp-id="aiAnalyze.corpId"
+      :customer-row="aiAnalyze.customerRow"
+    />
+  </el-drawer>
   </div>
 </template>
 
@@ -829,10 +912,11 @@ import {editTalk} from "@/api/qw/externalContactInfo";
 import healthRecordDetails from '@/views/store/components/healthRecordDetails.vue'
 import userDetails from '@/views/store/components/userDetails.vue';
 import PaginationMore from "../../../components/PaginationMore/index.vue";
+import customerDetail from './customerDetail.vue';
 
 export default {
   name: "deptExternalContact",
-  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,healthRecordDetails,userDetails},
+  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,healthRecordDetails,userDetails,customerDetail},
   data() {
     return {
 
@@ -1045,7 +1129,15 @@ export default {
       },
       tongueReportParams: {
         userId: null,
-      }
+      },
+      aiAnalyze: {
+        title: "AI 分析",
+        open: false,
+        userId: null,
+        externalUserId: null,
+        corpId: null,
+        customerRow: null,
+      },
     };
   },
   created() {
@@ -1095,7 +1187,41 @@ export default {
 
   },
   methods: {
-
+    shortenText(text, maxLen = 16) {
+      const str = text == null ? '' : String(text);
+      if (str.length <= maxLen) return str;
+      return str.slice(0, maxLen) + '...';
+    },
+    parseFocusPoints(value) {
+      if (!value) return [];
+      if (Array.isArray(value)) {
+        return value.map(item => String(item).trim()).filter(Boolean);
+      }
+      if (typeof value === 'string') {
+        const raw = value.trim();
+        if (!raw) return [];
+        try {
+          const parsed = JSON.parse(raw);
+          if (Array.isArray(parsed)) {
+            return parsed.map(item => String(item).trim()).filter(Boolean);
+          }
+          if (typeof parsed === 'string') {
+            return [parsed.trim()].filter(Boolean);
+          }
+        } catch (e) {
+          // ignore parse error and use raw fallback
+        }
+        return [raw.replace(/^\[|\]$/g, '').replace(/["']/g, '').trim()].filter(Boolean);
+      }
+      return [String(value).trim()].filter(Boolean);
+    },
+    openAiDrawer(row) {
+      this.aiAnalyze.userId = row.userId;
+      this.aiAnalyze.externalUserId = row.operUserid ;
+      this.aiAnalyze.corpId = row.corpId ;
+      this.aiAnalyze.customerRow = row;
+      this.aiAnalyze.open = true;
+    },
     onQwUserNameClear() {
       this.queryParams.qwUserId = null;  // 同时清空 qwUserId
     },

+ 245 - 13
src/views/qw/externalContact/index.vue

@@ -50,7 +50,7 @@
             class="suggestion-item"
             @click="selectQwUser(item.dictValue,item.dictLabel)"
           >
-            {{ item.dictLabel }}
+            {{ item.dictLabel }} ({{(item.dictValue)}})
           </div>
         </div>
       </el-form-item>
@@ -337,6 +337,15 @@
         v-hasPermi="['qw:externalContactInfo:updateTalk']"
 	    >批量更改交流状态</el-button>
 	  </el-col>
+    <el-col :span="1.5">
+      <el-button
+        type="warning"
+        plain
+        size="mini"
+        @click="addLobsterTag"
+        v-hasPermi="['qw:externalContact:addTag']"
+      >批量增加龙虾标签</el-button>
+    </el-col>
 <!--       <el-col :span="1.5">-->
 <!--        <el-button-->
 <!--          type="primary"-->
@@ -386,11 +395,6 @@
       <el-table-column label="备注" align="center" prop="remark" />
       <el-table-column label="描述信息" align="center" prop="description" />
       <el-table-column label="标签" align="center" prop="tagIdsName" width="300px">
-<!--        <template slot-scope="scope">-->
-<!--          <div v-for="name in scope.row.tagIdsName"  style="display: inline;">-->
-<!--          <el-tag type="success">{{ name }}</el-tag>-->
-<!--          </div>-->
-<!--        </template>-->
         <template slot-scope="scope">
           <div class="tag-container">
             <div class="tag-list">
@@ -406,6 +410,62 @@
           </div>
         </template>
       </el-table-column>
+      <el-table-column label="流失风险" align="center" prop="attritionLevel">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
+          <el-tag v-if="scope.row.attritionLevel === 0" type="info">未知</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 1" type="success">无风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 2" type="info">低风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 3" type="warning">中风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 4" type="danger">高风险</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="意向度" align="center" prop="intentionDegree">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.intentionDegree !== null && scope.row.intentionDegree !== undefined && scope.row.intentionDegree !== ''">
+            {{ scope.row.intentionDegree }}
+          </el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关注点" align="center" prop="customerFocusJson" width="220">
+        <template slot-scope="scope">
+          <div v-if="parseFocusPoints(scope.row.customerFocusJson).length" style="text-align: left;">
+            <el-tooltip
+              v-for="(item, index) in parseFocusPoints(scope.row.customerFocusJson).slice(0, 2)"
+              :key="index"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px; word-break: break-word;">
+                {{ item }}
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; max-width: 100%; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                {{ shortenText(item, 14) }}
+              </el-tag>
+            </el-tooltip>
+            <el-tooltip
+              v-if="parseFocusPoints(scope.row.customerFocusJson).length > 2"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px;">
+                <div
+                  v-for="(item, idx) in parseFocusPoints(scope.row.customerFocusJson).slice(2)"
+                  :key="'focus-more-' + idx"
+                  style="margin-bottom: 4px;"
+                >
+                  {{ item }}
+                </div>
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                +{{ parseFocusPoints(scope.row.customerFocusJson).length - 2 }}
+              </el-tag>
+            </el-tooltip>
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
       <el-table-column label="是否回复" align="center" prop="isReply" width="120px" >
         <template slot-scope="scope">
           <span v-if="scope.row.isReply === 1"><el-tag type="success">已回复</el-tag></span>
@@ -471,10 +531,17 @@
       </el-table-column>
       <el-table-column label="是否绑定会员" width="100px" align="center" fixed="right">
         <template slot-scope="scope">
-          <el-tag v-if="scope.row.fsUserId" >已绑定</el-tag>
+          <el-tag v-if="scope.row.fsUserId" >已绑定<br/>{{scope.row.fsUserId}}</el-tag>
           <el-tag v-else type="info"> 未绑定</el-tag>
         </template>
       </el-table-column>
+      <el-table-column label="是否下载APP" width="100px" align="center" fixed="right">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.isDownloadApp === 1 ? 'success' : 'info'">
+            {{ scope.row.isDownloadApp === 1 ? '已下载' : '未下载' }}
+          </el-tag>
+        </template>
+      </el-table-column>
       <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
         <template slot-scope="scope">
           <el-button
@@ -564,6 +631,13 @@
           >
             修改状态
           </el-button>
+          <el-button
+                v-if="scope.row.attritionLevel !== null && scope.row.attritionLevel !== undefined"
+                size="mini"
+                type="text"
+                @click="openAiDrawer(scope.row)"
+                v-hasPermi="['qw:externalContact:analyze:list']"
+              >AI 分析</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -575,7 +649,20 @@
       :limit.sync="queryParams.pageSize"
       @pagination="getList"
     />
-
+    <el-drawer
+    size="75%"
+    :title="aiAnalyze.title"
+    :visible.sync="aiAnalyze.open"
+    append-to-body
+  >
+    <customer-detail
+      ref="customerAiDetail"
+      :analyze-user-id="aiAnalyze.userId"
+      :analyze-external-user-id="aiAnalyze.externalUserId"
+      :analyze-corp-id="aiAnalyze.corpId"
+      :customer-row="aiAnalyze.customerRow"
+    />
+  </el-drawer>
     <el-drawer size="75%" :title="show.title" :visible.sync="show.open">
       <customer-details  ref="customerDetails" @refreshList="refreshList"/>
     </el-drawer>
@@ -666,6 +753,40 @@
         <el-button @click="addTagCancel">取 消</el-button>
       </div>
     </el-dialog>
+    <el-dialog title="批量增加龙虾标签" :visible.sync="lobsterTagOpen" width="700px" append-to-body>
+      <div v-loading="lobsterTagLoading">
+        <el-alert
+          title="龙虾标签来源于已启用的标签-模板绑定关系"
+          type="info"
+          :closable="false"
+          show-icon
+          style="margin-bottom: 16px"
+        />
+        <el-table
+          :data="lobsterTagList"
+          ref="lobsterTagTable"
+          @selection-change="handleLobsterTagSelect"
+          max-height="400"
+          border
+        >
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column prop="tagCode" label="标签编码" width="150" />
+          <el-table-column prop="tagName" label="标签名称" min-width="150" />
+          <el-table-column prop="templateName" label="绑定模板" min-width="180" show-overflow-tooltip />
+          <el-table-column prop="priority" label="优先级" width="100" align="center">
+            <template slot-scope="{ row }">
+              <el-tag :type="row.priority >= 50 ? 'danger' : row.priority >= 30 ? 'warning' : 'info'" size="mini">
+                {{ row.priority }}
+              </el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :disabled="lobsterSelectedTags.length === 0" @click="submitLobsterTag">确 定 (已选{{ lobsterSelectedTags.length }})</el-button>
+        <el-button @click="lobsterTagOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
     <el-dialog title="批量添加客户备注" :visible.sync="notesOpen.open" width="800px" append-to-body>
       <el-card>
         <el-row>
@@ -717,7 +838,7 @@
     <el-dialog title="批量移除标签" :visible.sync="tagDelOpen" width="800px" append-to-body>
       <div>搜索标签:
         <el-input v-model="queryTagParams.name" placeholder="请输入标签名称" clearable size="small" style="width: 200px;margin-right: 10px" />
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="getPageListTagGroup()">搜索</el-button>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags(queryTagParams.name)">搜索</el-button>
         <el-button type="primary" icon="el-icon-plus" size="mini" @click="cancelSearchTags">重置</el-button>
       </div>
       <el-form ref="form" :model="addTagForm"  label-width="80px">
@@ -952,13 +1073,15 @@ import  selectUser  from "@/views/qw/externalContact/selectUser.vue";
 import  collection  from "@/views/qw/externalContact/collection.vue";
 import info from "@/views/qw/externalContact/info.vue";
 import { editTalk } from "@/api/qw/externalContactInfo";
+import { tagBindingApi } from '@/api/company/tagBinding'
 import PaginationMore from "../../../components/PaginationMore/index.vue";
 import userDetails from '@/views/store/components/userDetails.vue';
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
 import Collection from './collection.vue';
+import customerDetail from './customerDetail.vue';
 export default {
   name: "ExternalContact",
-  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,userDetails,collection},
+  components:{PaginationMore, mycustomer,customerDetails,customerDetail,SopDialog,selectUser,info,userDetails,collection},
   data() {
     return {
       projectOptions: [],
@@ -1043,6 +1166,11 @@ export default {
       exportLoading: false,
       tagOpen:false,
       tagDelOpen:false,
+      // 龙虾标签弹窗
+      lobsterTagOpen: false,
+      lobsterTagLoading: false,
+      lobsterTagList: [],
+      lobsterSelectedTags: [],
       // 选中数组
       ids: [],
       isBindActiveName:"all",
@@ -1188,6 +1316,14 @@ export default {
       rules: {
       },
       userId:null,
+      aiAnalyze: {
+        title: "AI 分析",
+        open: false,
+        userId: null,
+        externalUserId: null,
+        corpId: null,
+        customerRow: null,
+      },
     };
   },
   created() {
@@ -1240,6 +1376,41 @@ export default {
 
   },
   methods: {
+    shortenText(text, maxLen = 16) {
+      const str = text == null ? '' : String(text);
+      if (str.length <= maxLen) return str;
+      return str.slice(0, maxLen) + '...';
+    },
+    parseFocusPoints(value) {
+      if (!value) return [];
+      if (Array.isArray(value)) {
+        return value.map(item => String(item).trim()).filter(Boolean);
+      }
+      if (typeof value === 'string') {
+        const raw = value.trim();
+        if (!raw) return [];
+        try {
+          const parsed = JSON.parse(raw);
+          if (Array.isArray(parsed)) {
+            return parsed.map(item => String(item).trim()).filter(Boolean);
+          }
+          if (typeof parsed === 'string') {
+            return [parsed.trim()].filter(Boolean);
+          }
+        } catch (e) {
+          // ignore parse error and use raw fallback
+        }
+        return [raw.replace(/^\[|\]$/g, '').replace(/["']/g, '').trim()].filter(Boolean);
+      }
+      return [String(value).trim()].filter(Boolean);
+    },
+    openAiDrawer(row) {
+      this.aiAnalyze.userId = row.userId;
+      this.aiAnalyze.externalUserId = row.operUserid ;
+      this.aiAnalyze.corpId = row.corpId ;
+      this.aiAnalyze.customerRow = row;
+      this.aiAnalyze.open = true;
+    },
     /** 重粉查看操作 */
     showLog(row) {
       this.log.queryParams.fsUserId = row.fsUserId;
@@ -1290,7 +1461,10 @@ export default {
       this.collection.open = false;
     },
     onQwUserNameClear() {
+      this.queryParams.qwUserName = '';  // 清空 qwUserName
       this.queryParams.qwUserId = null;  // 同时清空 qwUserId
+      this.qwUserSuggestions = [];       // 清空下拉建议
+      this.showQwUserDropdown = false;   // 隐藏下拉框
     },
     // 搜索企微用户
     searchQwUser(query) {
@@ -1723,7 +1897,6 @@ export default {
           if(this.tagGroupList[i].tag[x].isSelected==true){
             this.addTagForm.tagIds.push(this.tagGroupList[i].tag[x].tagId)
           }
-
         }
       }
       if(this.addTagForm.tagIds==[]||this.addTagForm.tagIds==null||this.addTagForm.tagIds==""){
@@ -1733,10 +1906,18 @@ export default {
       this.addTagForm.corpId=this.queryParams.corpId
       this.addTagForm.userIds=this.ids;
       this.addTagForm.filter = this.tagFilter;
-      let obj = JSON.parse(JSON.stringify(this.queryParams))
-      if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
+
+      // 修改这里:正确处理参数对象
+      let obj = JSON.parse(JSON.stringify(this.queryParams));
+
+      // 将逗号分隔的字符串转换为数组
+      if(obj.tagIds && typeof obj.tagIds === 'string') {
         obj.tagIds = obj.tagIds.split(",");
       }
+      if(obj.outTagIds && typeof obj.outTagIds === 'string') {
+        obj.outTagIds = obj.outTagIds.split(",");
+      }
+
       this.addTagForm.param = obj;
 
       let loadingRock = this.$loading({
@@ -1978,8 +2159,12 @@ export default {
       this.resetForm("queryForm");
       this.queryParams.transferStatus=null;
       this.queryParams.corpId= this.myQwCompanyList[0].dictValue;
+      this.queryParams.qwUserName = '';      // 清空销售企微昵称
+      this.queryParams.qwUserId = null;      // 清空销售企微ID
       this.selectTags=[];
       this.outSelectTags=[];
+      this.qwUserSuggestions = [];          // 清空下拉建议
+      this.showQwUserDropdown = false;      // 隐藏下拉框
 	   this.createTime=null;
 	  this.queryParams.sTime=null;
 	  this.queryParams.eTime=null;
@@ -2254,6 +2439,53 @@ export default {
 		    this.msgSuccess("成功");
 		  }).catch(() => {});
 	},
+    addLobsterTag() {
+      if (this.ids == null || this.ids.length === 0) {
+        return this.$message('请勾选需要添加龙虾标签的客户');
+      }
+      this.lobsterTagLoading = true;
+      this.lobsterTagOpen = true;
+      this.lobsterSelectedTags = [];
+      tagBindingApi.getListByStatus({ status: 1 }).then(res => {
+        this.lobsterTagList = res.data || [];
+      }).catch(() => {
+        this.$message.error('获取龙虾标签列表失败');
+      }).finally(() => {
+        this.lobsterTagLoading = false;
+        this.$nextTick(() => {
+          if (this.$refs.lobsterTagTable) {
+            this.$refs.lobsterTagTable.clearSelection();
+          }
+        });
+      });
+    },
+    handleLobsterTagSelect(selection) {
+      this.lobsterSelectedTags = selection;
+    },
+    submitLobsterTag() {
+      if (this.lobsterSelectedTags.length === 0) {
+        return this.$message('请选择龙虾标签');
+      }
+      const tagCodes = this.lobsterSelectedTags.map(t => t.tagCode);
+      const tagNames = this.lobsterSelectedTags.map(t => t.tagName);
+      this.$confirm(
+        '将为 ' + this.ids.length + ' 个客户添加以下标签:' + tagNames.join('、') + ',是否确认?',
+        '提示',
+        { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
+      ).then(() => {
+        tagBindingApi.batchBindLobsterTag({
+          userIds: this.ids,
+          tagCodes: tagCodes,
+          qwCorpId: this.queryParams.corpId
+        }).then(res => {
+          this.$message.success('龙虾标签添加任务已提交');
+          this.lobsterTagOpen = false;
+          this.lobsterSelectedTags = [];
+        }).catch(() => {
+          this.$message.error('龙虾标签添加失败');
+        });
+      }).catch(() => {});
+    },
     /** 导出按钮操作 */
     handleExport() {
       const { qwUserName, ...queryParams } = this.queryParams;

+ 141 - 2
src/views/qw/externalContact/myExternalContact.vue

@@ -11,6 +11,15 @@
                 />
               </el-select>
       </el-form-item>
+      <el-form-item label="企微客户ID" prop="id">
+        <el-input
+          v-model="queryParams.id"
+          placeholder="请输入企微客户ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
       <el-form-item label="客户名称" prop="name">
         <el-input
           v-model="queryParams.name"
@@ -375,6 +384,62 @@
           </div>
         </template>
       </el-table-column>
+      <el-table-column label="流失风险" align="center" prop="attritionLevel">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.attritionLevel == null" type="info">未分析</el-tag>
+          <el-tag v-if="scope.row.attritionLevel === 0" type="info">未知</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 1" type="success">无风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 2" type="info">低风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 3" type="warning">中风险</el-tag>
+          <el-tag v-else-if="scope.row.attritionLevel === 4" type="danger">高风险</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="意向度" align="center" prop="intentionDegree">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.intentionDegree !== null && scope.row.intentionDegree !== undefined && scope.row.intentionDegree !== ''">
+            {{ scope.row.intentionDegree }}
+          </el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关注点" align="center" prop="customerFocusJson" width="220">
+        <template slot-scope="scope">
+          <div v-if="parseFocusPoints(scope.row.customerFocusJson).length" style="text-align: left;">
+            <el-tooltip
+              v-for="(item, index) in parseFocusPoints(scope.row.customerFocusJson).slice(0, 2)"
+              :key="index"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px; word-break: break-word;">
+                {{ item }}
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; max-width: 100%; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                {{ shortenText(item, 14) }}
+              </el-tag>
+            </el-tooltip>
+            <el-tooltip
+              v-if="parseFocusPoints(scope.row.customerFocusJson).length > 2"
+              placement="top"
+              effect="light"
+            >
+              <div slot="content" style="max-width: 420px;">
+                <div
+                  v-for="(item, idx) in parseFocusPoints(scope.row.customerFocusJson).slice(2)"
+                  :key="'focus-more-' + idx"
+                  style="margin-bottom: 4px;"
+                >
+                  {{ item }}
+                </div>
+              </div>
+              <el-tag style="margin: 0 6px 6px 0; background: #fff; border: 1px solid #dcdfe6; color: #606266;">
+                +{{ parseFocusPoints(scope.row.customerFocusJson).length - 2 }}
+              </el-tag>
+            </el-tooltip>
+          </div>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
       <el-table-column label="是否回复" align="center" prop="isReply" width="120px" >
         <template slot-scope="scope">
           <span v-if="scope.row.isReply === 1"><el-tag type="success">已回复</el-tag></span>
@@ -441,10 +506,17 @@
       </el-table-column>
       <el-table-column label="是否绑定会员" width="100px" align="center" fixed="right">
         <template slot-scope="scope">
-          <el-tag v-if="scope.row.fsUserId" >已绑定</el-tag>
+          <el-tag v-if="scope.row.fsUserId" >已绑定<br/>{{scope.row.fsUserId}}</el-tag>
           <el-tag v-else type="info"> 未绑定</el-tag>
         </template>
       </el-table-column>
+      <el-table-column label="是否下载APP" width="100px" align="center" fixed="right">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.isDownloadApp === 1 ? 'success' : 'info'">
+            {{ scope.row.isDownloadApp === 1 ? '已下载' : '未下载' }}
+          </el-tag>
+        </template>
+      </el-table-column>
       <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
         <template slot-scope="scope">
           <el-button
@@ -546,6 +618,13 @@
         >
           修改状态
         </el-button>
+        <el-button
+                v-if="scope.row.attritionLevel !== null && scope.row.attritionLevel !== undefined"
+                size="mini"
+                type="text"
+                @click="openAiDrawer(scope.row)"
+                v-hasPermi="['qw:externalContact:analyze:list']"
+              >AI 分析</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -964,6 +1043,20 @@
 	 :title="show.title" :visible.sync="show.open">
 	  <info  ref="Details" />
 	</el-drawer>
+  <el-drawer
+  size="75%"
+  :title="aiAnalyze.title"
+  :visible.sync="aiAnalyze.open"
+  append-to-body
+>
+  <customer-detail
+    ref="customerAiDetail"
+    :analyze-user-id="aiAnalyze.userId"
+    :analyze-external-user-id="aiAnalyze.externalUserId"
+    :analyze-corp-id="aiAnalyze.corpId"
+    :customer-row="aiAnalyze.customerRow"
+  />
+</el-drawer>
     <el-dialog :title="user.title" :visible.sync="user.open" width="800px" append-to-body>
       <selectUser ref="selectUser" @bindMiniCustomerId="bindMiniCustomerId"></selectUser>
     </el-dialog>
@@ -1027,11 +1120,20 @@ import PaginationMore from "../../../components/PaginationMore/index.vue";
 import Collection from './collection.vue';
 import {courseList, videoList} from "@/api/course/courseRedPacketLog";
 import userDetails from '@/views/store/components/userDetails.vue';
+import customerDetail from './customerDetail.vue';
 export default {
   name: "ExternalContact",
-  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,Collection,userDetails},
+  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,Collection,userDetails,customerDetail},
   data() {
     return {
+      aiAnalyze: {
+        title: "AI 分析",
+        open: false,
+        userId: null,
+        externalUserId: null,
+        corpId: null,
+        customerRow: null,
+      },
       member:{
         title:"客户详情",
         open:false,
@@ -1199,6 +1301,7 @@ export default {
       queryParams: {
         pageNum: 1,
         pageSize: 10,
+        id:  null,
         userId: null,
         qwUserName:null,
         externalUserId: null,
@@ -1297,6 +1400,42 @@ export default {
 
   },
   methods: {
+    shortenText(text, maxLen = 16) {
+      const str = text == null ? '' : String(text);
+      if (str.length <= maxLen) return str;
+      return str.slice(0, maxLen) + '...';
+    },
+    parseFocusPoints(value) {
+      if (!value) return [];
+      if (Array.isArray(value)) {
+        return value.map(item => String(item).trim()).filter(Boolean);
+      }
+      if (typeof value === 'string') {
+        const raw = value.trim();
+        if (!raw) return [];
+        try {
+          const parsed = JSON.parse(raw);
+          if (Array.isArray(parsed)) {
+            return parsed.map(item => String(item).trim()).filter(Boolean);
+          }
+          if (typeof parsed === 'string') {
+            return [parsed.trim()].filter(Boolean);
+          }
+        } catch (e) {
+          // ignore parse error and use raw fallback
+        }
+        return [raw.replace(/^\[|\]$/g, '').replace(/["']/g, '').trim()].filter(Boolean);
+      }
+      return [String(value).trim()].filter(Boolean);
+    },
+    openAiDrawer(row) {
+      this.aiAnalyze.userId = row.userId;
+      this.aiAnalyze.externalUserId = row.operUserid ;
+      this.aiAnalyze.corpId = row.corpId ;
+      this.aiAnalyze.customerRow = row;
+      console.log(this.aiAnalyze);
+      this.aiAnalyze.open = true;
+    },
     /** 重粉查看操作 */
     showLog(row) {
       this.log.queryParams.fsUserId = row.fsUserId;

+ 11 - 2
src/views/qw/externalContactTransfer/index.vue

@@ -486,10 +486,19 @@ export default {
     },
     /** 重置按钮操作 */
     resetQuery() {
-      this.selectTags=[];
-      this.dateRange=null;
+      this.selectTags = [];
+      this.dateRange = null;
+      this.queryParams.transferStatus = null; // 手动清空转接状态
+      this.queryParams.pageNum = 1;
       this.changeTime();
       this.resetForm("queryForm");
+      // 确保重置后再次清空(防止resetForm没有清空)
+      this.$nextTick(() => {
+        if (this.$refs.queryForm) {
+          this.$refs.queryForm.resetFields();
+        }
+        this.queryParams.transferStatus = null;
+      });
       this.handleQuery();
     },
     // 多选框选中数据

+ 21 - 6
src/views/qw/friendWelcome/indexNew.vue

@@ -1089,7 +1089,7 @@ export default {
             return false;
           }
         });
-     
+
     },
 
     //取消附件
@@ -1157,12 +1157,27 @@ export default {
     },
     /** 重置按钮操作 */
     resetQuery() {
-      this.queryParams.createTime = null;
+      // 重置整个查询表单
       this.resetForm("queryForm");
-      this.queryParams.corpId= this.myQwCompanyList[0].dictValue
-      getQwAllUserList(this.queryParams.corpId).then(response => {
-        this.companyUserList = response.data;
-      });
+
+      // 重置 corpId 为公司列表第一个
+      this.queryParams.corpId = this.myQwCompanyList[0]?.dictValue || null;
+
+      // 重置 qwUserIds 为空数组
+      this.queryParams.qwUserIds = [];
+
+      // 重置创建时间和更新时间
+      this.queryParams.createTime = null;
+      this.queryParams.updateTime = null;
+
+      // 重新获取用户列表
+      if (this.queryParams.corpId) {
+        getQwAllUserList(this.queryParams.corpId).then(response => {
+          this.companyUserList = response.data;
+        });
+      }
+
+      // 执行查询
       this.handleQuery();
     },
     // 多选框选中数据

+ 3 - 3
src/views/qw/sopTemp/index.vue

@@ -360,7 +360,7 @@
         <el-button type="primary" @click="updateRedData" :disabled="redData.loading">保 存</el-button>
       </div>
     </el-dialog>
-    
+
     <el-dialog title="批量编辑官方群发" :visible.sync="official.open" width="500px" append-to-body>
        <el-form :model="officialForm" ref="officialForm" :inline="true">
          <el-form-item  label="官方群发" prop="isOfficial">
@@ -606,9 +606,9 @@ export default {
       this.openIsAtAllOptions = response.data;
     });
 
-    getSelectableRange().then(e => {
+/*    getSelectableRange().then(e => {
       this.startTimeRange = e.data;
-    })
+    })*/
     this.getDicts("sys_course_project").then(response => {
       this.projectOptions = response.data;
     });

+ 2 - 2
src/views/qw/user/index.vue

@@ -350,7 +350,7 @@
             icon="el-icon-unlock"
             plain
             v-hasPermi="['qw:user:aiStatus']"
-            v-if="scope.row.aiStatus == 0 && projectFrom === 'sxjz'"
+            v-if="scope.row.aiStatus == 0"
             @click="updateFastGptRoleStatus(scope.row)"
           >Ai客服下线</el-button>
           <el-button
@@ -359,7 +359,7 @@
             icon="el-icon-unlock"
             plain
             v-hasPermi="['qw:user:aiStatus']"
-            v-if="scope.row.aiStatus == 1 && projectFrom === 'sxjz'"
+            v-if="scope.row.aiStatus == 1"
             @click="updateFastGptRoleStatus(scope.row)"
           >Ai客服上线</el-button>
         </template>