فهرست منبع

ai销冠系统,工作流生成

lk 2 هفته پیش
والد
کامیت
d85810b450

+ 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
+  })
+}
+

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

@@ -0,0 +1,162 @@
+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 }
+    })
+  }
+}
+
+// 知识审核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'
+    })
+  }
+}

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

@@ -0,0 +1,74 @@
+import request from '@/utils/request'
+
+// 标签模板绑定API
+export const tagBindingApi = {
+  // 获取绑定列表
+  getList: (params) => {
+    return request({
+      url: '/workflow/tag-binding/list',
+      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
+    })
+  }
+}

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

@@ -0,0 +1,83 @@
+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 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 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
+  })
+}

+ 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
+  })
+}
+
+

+ 13 - 0
src/router/index.js

@@ -388,6 +388,19 @@ export const constantRoutes = [
                 }
             }
         ]
+    },
+    {
+        path: '/workflow/visual/:id',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: '',
+                component: () => import('@/views/company/workflowLobster/visual.vue'),
+                name: 'WorkflowLobsterVisual',
+                meta: { title: '工作流可视化编辑', activeMenu: '/company/workflowLobster' }
+            }
+        ]
     }
 
 ]

+ 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>

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

@@ -0,0 +1,416 @@
+<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>
+        </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-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 type="selection" width="55" align="center" />
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column prop="title" label="知识标题" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="question" label="问题" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="industryType" label="行业类型" width="100">
+          <template slot-scope="{ row }">
+            {{ getIndustryText(row.industryType) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="source" label="来源" width="120">
+          <template slot-scope="{ row }">
+            <el-tag :type="getSourceType(row.source)">
+              {{ getSourceText(row.source) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="auditStatus" label="审核状态" width="100" align="center">
+          <template slot-scope="{ row }">
+            <el-tag :type="getAuditStatusType(row.auditStatus)">
+              {{ getAuditStatusText(row.auditStatus) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="useCount" label="使用次数" width="100" align="center" />
+        <el-table-column prop="createTime" label="创建时间" width="180" />
+        <el-table-column label="操作" width="250" 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 v-if="row.auditStatus === 0" type="success" size="mini" @click="handleAudit(row)">审核</el-button>
+            <el-button type="danger" size="mini" @click="handleDelete(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination">
+        <el-pagination
+          :current-page="page.current"
+          :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="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="知识详情" :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'
+
+export default {
+  name: 'KnowledgeBase',
+  data() {
+    return {
+      loading: false,
+      tableData: [],
+      selectedRows: [],
+      searchForm: {
+        keyword: '',
+        industryType: '',
+        auditStatus: null
+      },
+      page: {
+        current: 1,
+        size: 10,
+        total: 0
+      },
+      dialogVisible: false,
+      dialogTitle: '',
+      form: {
+        id: null,
+        title: '',
+        question: '',
+        answer: '',
+        industryType: 'general'
+      },
+      rules: {
+        title: [{ required: true, message: '请输入知识标题', trigger: 'blur' }],
+        question: [{ required: true, message: '请输入问题', trigger: 'blur' }],
+        answer: [{ required: true, message: '请输入答案', trigger: 'blur' }]
+      },
+      viewDialogVisible: false,
+      currentItem: {},
+      auditDialogVisible: false,
+      auditForm: {
+        id: null,
+        auditStatus: 1,
+        comment: ''
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  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
+      }
+    },
+    handleSearch() {
+      this.page.current = 1
+      this.getList()
+    },
+    handleReset() {
+      this.searchForm = {
+        keyword: '',
+        industryType: '',
+        auditStatus: null
+      }
+      this.handleSearch()
+    },
+    handleSizeChange(size) {
+      this.page.size = size
+      this.getList()
+    },
+    handleCurrentChange(current) {
+      this.page.current = current
+      this.getList()
+    },
+    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()
+      })
+    },
+    handleEdit(row) {
+      this.dialogTitle = '编辑知识'
+      this.form = {
+        id: row.id,
+        title: row.title,
+        question: row.question,
+        answer: row.answer,
+        industryType: row.industryType
+      }
+      this.dialogVisible = true
+    },
+    handleView(row) {
+      this.currentItem = row
+      this.viewDialogVisible = true
+    },
+    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.getList()
+      } 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.getList()
+      } 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.getList()
+      } 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.getList()
+      } 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>

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

@@ -0,0 +1,298 @@
+<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'
+
+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: ''
+      },
+      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() {
+      // TODO: 从工作流模板API加载模板列表
+      this.templateOptions = []
+    },
+    handleSearch() {
+      this.getList()
+    },
+    handleReset() {
+      this.searchForm = {
+        tagCode: '',
+        templateId: null
+      }
+      this.handleSearch()
+    },
+    handleAdd() {
+      this.dialogTitle = '新增绑定'
+      this.form = {
+        id: null,
+        tagCode: '',
+        tagName: '',
+        templateId: null,
+        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,
+        priority: row.priority,
+        matchCondition: row.matchCondition
+      }
+      this.dialogVisible = true
+    },
+    async handleSubmit() {
+      try {
+        await this.$refs.formRef.validate()
+        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>
+

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

@@ -0,0 +1,271 @@
+<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 === 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: '',
+        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>

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

@@ -0,0 +1,401 @@
+<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="80" />
+        <el-table-column prop="createTime" label="创建时间" width="160" />
+        <el-table-column label="操作" width="220" fixed="right">
+          <template slot-scope="scope">
+            <el-button type="text" @click="handlePreview(scope.row)">预览</el-button>
+            <el-button type="text" @click="handleEditTemplate(scope.row)">编辑</el-button>
+            <el-button type="text" @click="handleVisual(scope.row)">流程图</el-button>
+            <el-button type="text" style="color:#f56c6c" @click="handleDeleteTemplate(scope.row)">删除</el-button>
+          </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,
+  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,
+      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('/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 || '删除失败')
+        }
+      }
+    }
+  }
+}
+</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;
+  }
+}
+</style>

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

@@ -0,0 +1,1293 @@
+<template>
+  <div class="workflow-canvas-editor">
+    <!-- 顶部工具栏 -->
+    <div class="canvas-toolbar">
+      <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>
+      </div>
+      <div class="toolbar-right">
+        <el-radio-group v-model="viewMode" size="small" style="margin-right: 12px">
+          <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 type="primary" size="small" :loading="saving" @click="saveCanvas">保存</el-button>
+      </div>
+    </div>
+
+    <!-- 画布模式 -->
+    <div v-if="viewMode === 'canvas'" 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="true"
+              @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>
+            <pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
+              <path d="M 20 0 L 0 0 0 20" fill="none" stroke="#e0e0e0" stroke-width="0.5"/>
+            </pattern>
+            <marker
+              id="arrowhead"
+              markerWidth="10"
+              markerHeight="10"
+              refX="9"
+              refY="5"
+              orient="auto"
+            >
+              <path d="M0,1 L0,9 L9,5 z" fill="#999" />
+            </marker>
+            <marker
+              id="arrowhead-selected"
+              markerWidth="10"
+              markerHeight="10"
+              refX="9"
+              refY="5"
+              orient="auto"
+            >
+              <path d="M0,1 L0,9 L9,5 z" fill="#409eff" />
+            </marker>
+          </defs>
+
+          <!-- 网格背景 -->
+          <rect :width="canvasSize.width" :height="canvasSize.height" fill="url(#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 || '#999'"
+                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="12"
+                fill="#666"
+              >{{ edge.edgeLabel }}</text>
+            </g>
+
+            <!-- 正在绘制的临时连线 -->
+            <path
+              v-if="drawingEdge"
+              :d="getDrawingEdgePath()"
+              stroke="#409eff"
+              stroke-width="2"
+              stroke-dasharray="5,5"
+              fill="none"
+              marker-end="url(#arrowhead)"
+            />
+          </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)"
+            >
+              <!-- 节点背景 - 主要交互区域 -->
+              <rect
+                x="0"
+                y="0"
+                :width="node.width || 200"
+                :height="node.height || 80"
+                rx="8"
+                :fill="getNodeColor(node.nodeType)"
+                :stroke="selectedNode === node ? '#409eff' : '#dcdfe6'"
+                stroke-width="2"
+                class="node-rect"
+                @mousedown="onNodeMouseDown($event, node)"
+                style="cursor: move; pointer-events: all"
+              />
+
+              <!-- 拖拽指示器 -->
+              <text
+                :x="(node.width || 200) - 15"
+                :y="15"
+                font-size="10"
+                fill="#c0c4cc"
+                class="drag-indicator"
+                style="cursor: move"
+              >⋮⋮</text>
+
+              <!-- 节点类型图标 -->
+              <text
+                x="15"
+                y="25"
+                font-size="18"
+                :fill="getNodeIconColor(node.nodeType)"
+              >{{ getNodeIcon(node.nodeType) }}</text>
+
+              <!-- 节点名称 -->
+              <text
+                x="40"
+                y="25"
+                font-size="14"
+                font-weight="bold"
+                fill="#303133"
+              >{{ node.nodeName }}</text>
+
+              <!-- 节点类型标签 -->
+              <text
+                x="40"
+                y="45"
+                font-size="11"
+                fill="#909399"
+              >{{ getNodeTypeName(node.nodeType) }}</text>
+
+              <!-- 连接点 - 上 -->
+              <circle
+                :cx="(node.width || 200) / 2"
+                cy="0"
+                r="6"
+                fill="#fff"
+                stroke="#409eff"
+                stroke-width="2"
+                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="6"
+                fill="#fff"
+                stroke="#409eff"
+                stroke-width="2"
+                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="6"
+                fill="#fff"
+                stroke="#409eff"
+                stroke-width="2"
+                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="6"
+                fill="#fff"
+                stroke="#409eff"
+                stroke-width="2"
+                class="port port-right"
+                @mousedown.stop="startDrawEdge($event, node, 'right')"
+                style="cursor: crosshair; pointer-events: all"
+              />
+            </g>
+          </g>
+        </svg>
+      </div>
+
+      <!-- 右侧属性面板 -->
+      <div class="property-panel" v-if="selectedNode || selectedEdge">
+        <div class="panel-title">
+          {{ selectedNode ? '节点属性' : '连线属性' }}
+          <el-button size="mini" icon="el-icon-close" @click="clearSelection" style="float: right"></el-button>
+        </div>
+
+        <!-- 节点属性表单 -->
+        <el-form v-if="selectedNode" label-width="80px" size="small" style="padding: 12px">
+          <el-form-item label="节点名称">
+            <el-input v-model="selectedNode.nodeName" />
+          </el-form-item>
+          <el-form-item label="节点类型">
+            <el-select v-model="selectedNode.nodeType" style="width: 100%">
+              <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-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" />
+          </el-form-item>
+          <el-form-item label="条件表达式" v-if="selectedNode.nodeType === 3">
+            <el-input v-model="selectedNode.conditionExpr" type="textarea" :rows="3" />
+          </el-form-item>
+          <el-form-item label="节点配置">
+            <el-input v-model="selectedNode.nodeConfig" type="textarea" :rows="4" placeholder="JSON格式" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="danger" size="small" @click="deleteSelectedNode">删除节点</el-button>
+          </el-form-item>
+        </el-form>
+
+        <!-- 连线属性表单 -->
+        <el-form v-if="selectedEdge" label-width="80px" size="small" style="padding: 12px">
+          <el-form-item label="连线标签">
+            <el-input v-model="selectedEdge.edgeLabel" />
+          </el-form-item>
+          <el-form-item label="连线颜色">
+            <el-color-picker v-model="selectedEdge.edgeColor" />
+          </el-form-item>
+          <el-form-item label="条件表达式">
+            <el-input v-model="selectedEdge.conditionExpr" type="textarea" :rows="3" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="danger" size="small" @click="deleteSelectedEdge">删除连线</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+
+    <!-- 列表模式 -->
+    <div v-if="viewMode === 'list'" class="list-workspace" v-loading="loading">
+      <el-card shadow="never">
+        <div slot="header" class="card-header">
+          <span>列表视图 - {{ templateData.templateName || '加载中...' }}</span>
+        </div>
+        <div v-if="editorForm.nodes && editorForm.nodes.length > 0">
+        <workflow-solution-editor v-model="editorForm" :readonly="false" :compact="true" />
+        </div>
+        <el-empty v-else description="暂无节点数据" />
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script>
+import WorkflowSolutionEditor from './WorkflowSolutionEditor.vue'
+import { getWorkflowTemplateDetail, saveWorkflowCanvas } from '@/api/company/workflowLobster'
+
+export default {
+  name: 'WorkflowCanvasEditor',
+  components: { WorkflowSolutionEditor },
+  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,避免频繁更新
+      hasDragged: false, // 标记是否发生了拖动,用于区分click和drag
+      // 连线绘制
+      drawingEdge: null,
+      edgeStartNode: null,
+      edgeStartPort: null,
+      mousePos: { x: 0, y: 0 },
+      // 选中状态
+      selectedNode: null,
+      selectedEdge: null,
+      // 网格配置
+      gridSize: 20, // 网格大小,用于对齐
+      // 节点类型配置
+      nodeCategories: [
+        {
+          key: 'basic',
+          name: '基础节点',
+          types: [
+            { type: 1, label: '开始节点', icon: 'el-icon-video-play', color: '#67c23a' },
+            { type: 5, label: '结束节点', icon: 'el-icon-video-pause', color: '#f56c6c' }
+          ]
+        },
+        {
+          key: 'action',
+          name: '操作节点',
+          types: [
+            { type: 2, label: '消息节点', icon: 'el-icon-message', color: '#409eff' },
+            { type: 4, label: '等待节点', icon: 'el-icon-time', color: '#e6a23c' },
+            { type: 6, label: 'API节点', icon: 'el-icon-link', color: '#909399' }
+          ]
+        },
+        {
+          key: 'logic',
+          name: '逻辑节点',
+          types: [
+            { type: 3, label: '判断节点', icon: 'el-icon-question', color: '#606266' }
+          ]
+        }
+      ]
+    }
+  },
+  created() {
+    this.templateId = this.$route.params.id
+    this.fetchTemplateDetail()
+  },
+  mounted() {
+    // 自动聚焦到画布以接收键盘事件
+    this.$nextTick(() => {
+      if (this.$refs.canvasContainer) {
+        this.$refs.canvasContainer.focus()
+      }
+    })
+  },
+  beforeDestroy() {
+    // 清理事件监听器,防止内存泄漏
+    document.removeEventListener('mousemove', this.onNodeDragMove)
+    document.removeEventListener('mouseup', this.onNodeDragEnd)
+    document.removeEventListener('mousemove', this.onEdgeDrawMove)
+    document.removeEventListener('mouseup', this.onEdgeDrawEnd)
+  },
+  methods: {
+    async fetchTemplateDetail() {
+      this.loading = true
+      try {
+        const res = await getWorkflowTemplateDetail(this.templateId)
+        const data = res && res.data ? res.data : {}
+        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 = 80   // 水平间距
+      const verticalGap = 100    // 垂直间距(行间距)
+      const startX = 100         // 起始X坐标
+      const startY = 100         // 起始Y坐标
+      const nodesPerRow = 5      // 每行节点数
+      
+      // 按照连接关系和sortNo排序节点
+      const sortedNodes = this.sortNodesForLayout()
+      
+      // 按照每行5个节点进行布局
+      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()
+    },
+    // 缩放控制
+    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))
+    },
+    // 节点类型辅助方法
+    getNodeColor(type) {
+      const colors = {
+        1: '#f0f9ff',
+        2: '#ecf5ff',
+        3: '#fdf6ec',
+        4: '#f5f7fa',
+        5: '#fef0f0',
+        6: '#f4f4f5'
+      }
+      return colors[type] || '#fff'
+    },
+    getNodeIconColor(type) {
+      const colors = {
+        1: '#67c23a',
+        2: '#409eff',
+        3: '#e6a23c',
+        4: '#909399',
+        5: '#f56c6c',
+        6: '#606266'
+      }
+      return colors[type] || '#909399'
+    },
+    getNodeIcon(type) {
+      const icons = {
+        1: '\ud83d\udfe2',
+        2: '\ud83d\udcac',
+        3: '\u2753',
+        4: '\u23f1',
+        5: '\ud83d\udd34',
+        6: '\ud83d\udd17'
+      }
+      return icons[type] || '\ud83d\udccc'
+    },
+    getNodeTypeName(type) {
+      const names = {
+        1: '开始节点',
+        2: '消息节点',
+        3: '判断节点',
+        4: '等待节点',
+        5: '结束节点',
+        6: 'API调用节点'
+      }
+      return names[type] || '未知类型'
+    },
+    // 拖拽添加节点
+    onDragStart(event, nodeType) {
+      event.dataTransfer.setData('nodeType', JSON.stringify(nodeType))
+      event.dataTransfer.effectAllowed = 'copy'
+    },
+    onDrop(event) {
+      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: '',
+        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) {
+      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
+      }
+      
+      // 添加全局事件监听,使用passive提升性能
+      document.addEventListener('mousemove', this.onNodeDragMove, { passive: true })
+      document.addEventListener('mouseup', this.onNodeDragEnd)
+      document.body.style.cursor = 'move'
+      document.body.style.userSelect = 'none'
+    },
+    
+    // 节点拖动中 - 直接更新,让节点紧跟光标
+    onNodeDragMove(event) {
+      if (!this.isDraggingNode || !this.draggingNode) return
+      
+      const canvasRect = this.canvasRect
+      
+      // 计算新的节点位置(光标位置 - 偏移量)
+      let newX = (event.clientX - canvasRect.left - this.canvasOffset.x) / this.scale - this.dragNodeOffset.x
+      let newY = (event.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() {
+      // 拖动结束时进行网格对齐
+      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
+      
+      // 移除全局事件监听
+      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.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) {
+      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.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.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(#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: '#999',
+        conditionExpr: '',
+        sortNo: this.edges.length + 1,
+        delFlag: 0
+      }
+      this.edges.push(newEdge)
+    },
+    // 保存画布
+    async saveCanvas() {
+      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: #f5f7fa;
+  overflow: hidden;
+
+  .canvas-toolbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 12px 16px;
+    background: #fff;
+    border-bottom: 1px solid #e4e7ed;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+
+    .toolbar-left {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+
+      .template-name {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+      }
+    }
+
+    .toolbar-right {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+  }
+
+  .canvas-workspace {
+    flex: 1;
+    display: flex;
+    overflow: hidden;
+
+    .node-panel {
+      width: 240px;
+      background: #fff;
+      border-right: 1px solid #e4e7ed;
+      padding: 16px;
+      overflow-y: auto;
+
+      .panel-title {
+        font-size: 14px;
+        font-weight: 600;
+        margin-bottom: 16px;
+        color: #303133;
+      }
+
+      .node-category {
+        margin-bottom: 20px;
+
+        .category-title {
+          font-size: 12px;
+          color: #909399;
+          margin-bottom: 8px;
+        }
+
+        .node-list {
+          display: flex;
+          flex-direction: column;
+          gap: 8px;
+
+          .node-item {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            padding: 10px 12px;
+            background: #f5f7fa;
+            border: 1px solid #e4e7ed;
+            border-radius: 4px;
+            cursor: move;
+            transition: all 0.3s;
+
+            &:hover {
+              background: #ecf5ff;
+              border-color: #409eff;
+              box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
+            }
+
+            i {
+              font-size: 16px;
+            }
+
+            span {
+              font-size: 13px;
+              color: #606266;
+            }
+          }
+        }
+      }
+    }
+
+    .canvas-container {
+      flex: 1;
+      position: relative;
+      overflow: hidden;
+      background: #fafafa;
+      outline: none;
+
+      .canvas-svg {
+        min-width: 100%;
+        min-height: 100%;
+        cursor: grab;
+        user-select: none; // 防止拖动时选中文字
+        -webkit-user-select: none;
+      
+        &.dragging-canvas {
+          cursor: grabbing;
+        }
+      
+        .edge-group {
+          // 启用GPU加速
+          will-change: transform;
+          
+          &.selected {
+            .edge-path {
+              stroke: #409eff !important;
+              stroke-width: 3;
+              filter: drop-shadow(0 0 4px rgba(64, 158, 255, 0.5));
+            }
+          }
+      
+          &:hover:not(.selected) {
+            .edge-path {
+              stroke-width: 3;
+              filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.2));
+            }
+          }
+      
+          .edge-interaction {
+            &:hover {
+              stroke: rgba(64, 158, 255, 0.1);
+            }
+          }
+        }
+
+        .node-group {
+          cursor: move;
+          pointer-events: all; // 确保能接收鼠标事件
+          // 禁用位置的transition,确保拖动时节点紧跟光标
+          transition: filter 0.2s, opacity 0.2s;
+          
+          &.selected {
+            .node-rect {
+              stroke: #409eff !important;
+              stroke-width: 3;
+              filter: drop-shadow(0 2px 12px rgba(64, 158, 255, 0.4));
+            }
+          }
+
+          &.dragging {
+            // 拖动时禁用所有过渡动画,确保实时响应
+            transition: none !important;
+            .node-rect {
+              stroke: #409eff !important;
+              stroke-width: 3;
+            }
+            opacity: 0.95;
+          }
+
+          &:hover:not(.selected):not(.dragging) {
+            .node-rect {
+              filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15));
+            }
+          }
+
+          .drag-indicator {
+            opacity: 0;
+            transition: opacity 0.2s;
+            user-select: none;
+          }
+
+          &:hover .drag-indicator {
+            opacity: 1;
+          }
+
+          .port {
+            opacity: 0.7; // 默认就显示,透明度70%
+            transition: all 0.2s;
+            cursor: crosshair;
+
+            &:hover {
+              r: 9;
+              fill: #409eff;
+              stroke-width: 3;
+              opacity: 1;
+              filter: drop-shadow(0 0 8px rgba(64, 158, 255, 0.8));
+            }
+          }
+        }
+      }
+    }
+
+    .property-panel {
+      width: 320px;
+      background: #fff;
+      border-left: 1px solid #e4e7ed;
+      overflow-y: auto;
+
+      .panel-title {
+        padding: 12px 16px;
+        font-size: 14px;
+        font-weight: 600;
+        border-bottom: 1px solid #e4e7ed;
+        background: #fafafa;
+      }
+    }
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .list-workspace {
+    flex: 1;
+    overflow-y: auto;
+    padding: 12px;
+  }
+}
+</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;

+ 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
     },

+ 142 - 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>
@@ -386,11 +386,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 +401,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 +522,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 +622,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 +640,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>
@@ -717,7 +795,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">
@@ -956,9 +1034,10 @@ 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: [],
@@ -1188,6 +1267,14 @@ export default {
       rules: {
       },
       userId:null,
+      aiAnalyze: {
+        title: "AI 分析",
+        open: false,
+        userId: null,
+        externalUserId: null,
+        corpId: null,
+        customerRow: null,
+      },
     };
   },
   created() {
@@ -1240,6 +1327,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;
@@ -1723,7 +1845,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 +1854,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({

+ 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;