boss 19 uur geleden
bovenliggende
commit
78796ca44e

+ 2 - 1
package.json

@@ -75,7 +75,8 @@
     "vue-router": "3.4.9",
     "vuedraggable": "^2.24.3",
     "vuex": "3.6.0",
-    "wangeditor": "^4.7.5"
+    "wangeditor": "^4.7.5",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",

+ 44 - 0
src/api/company/companyVoiceApi.js

@@ -57,4 +57,48 @@ export function exportCompanyVoiceApi(query) {
     method: 'get',
     params: query
   })
+}
+
+// ========== 通话接口-租户分配 ==========
+
+// 查询接口已分配的租户列表
+export function getAssignedTenants(apiId) {
+  return request({
+    url: '/admin/voice-api/tenants/' + apiId,
+    method: 'get'
+  })
+}
+
+// 查询租户已分配的接口列表
+export function getTenantApis(companyId) {
+  return request({
+    url: '/admin/voice-api/apis/' + companyId,
+    method: 'get'
+  })
+}
+
+// 分配接口给租户(批量)
+export function assignTenants(apiId, companyIds) {
+  return request({
+    url: '/admin/voice-api/assignTenants',
+    method: 'post',
+    data: { apiId, companyIds }
+  })
+}
+
+// 取消分配
+export function unassignTenant(apiId, companyId) {
+  return request({
+    url: '/admin/voice-api/unassignTenant',
+    method: 'delete',
+    params: { apiId, companyId }
+  })
+}
+
+// 查询接口已分配租户数量
+export function getTenantCount(apiId) {
+  return request({
+    url: '/admin/voice-api/tenantCount/' + apiId,
+    method: 'get'
+  })
 }

+ 95 - 0
src/api/system/smsApi.js

@@ -0,0 +1,95 @@
+import request from '@/utils/request'
+
+// 查询短信接口列表
+export function listSmsApi(query) {
+  return request({
+    url: '/admin/smsApi/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询短信接口详情
+export function getSmsApi(apiId) {
+  return request({
+    url: '/admin/smsApi/' + apiId,
+    method: 'get'
+  })
+}
+
+// 新增短信接口
+export function addSmsApi(data) {
+  return request({
+    url: '/admin/smsApi',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改短信接口
+export function updateSmsApi(data) {
+  return request({
+    url: '/admin/smsApi',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除短信接口
+export function delSmsApi(apiId) {
+  return request({
+    url: '/admin/smsApi/' + apiId,
+    method: 'delete'
+  })
+}
+
+// 查询租户绑定列表
+export function listSmsApiTenant(query) {
+  return request({
+    url: '/admin/smsApiTenant/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询租户绑定详情
+export function getSmsApiTenant(id) {
+  return request({
+    url: '/admin/smsApiTenant/' + id,
+    method: 'get'
+  })
+}
+
+// 查询租户已绑定的接口
+export function getSmsApiTenantByCompany(companyId) {
+  return request({
+    url: '/admin/smsApiTenant/byCompany/' + companyId,
+    method: 'get'
+  })
+}
+
+// 新增租户绑定
+export function addSmsApiTenant(data) {
+  return request({
+    url: '/admin/smsApiTenant',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改租户绑定
+export function updateSmsApiTenant(data) {
+  return request({
+    url: '/admin/smsApiTenant',
+    method: 'put',
+    data: data
+  })
+}
+
+// 解除租户绑定
+export function delSmsApiTenant(id) {
+  return request({
+    url: '/admin/smsApiTenant/' + id,
+    method: 'delete'
+  })
+}

+ 1 - 1
src/api/workflow/lobster.js

@@ -122,7 +122,7 @@ export function addCorpusDialog(data) {
 }
 
 export function batchImportCorpus(data) {
-  return request({ url: '/workflow/lobster/sales-corpus/batch-import', method: 'post', data })
+  return request({ url: '/workflow/lobster/sales-corpus/batch-import', method: 'post', data, timeout: 60000 })
 }
 
 export function analyzeCorpus() {

+ 88 - 0
src/components/IconSelect/elIcons.js

@@ -0,0 +1,88 @@
+// Element UI 图标列表(用于菜单管理图标选择器)
+const elIcons = [
+  'el-icon-s-home',
+  'el-icon-s-grid',
+  'el-icon-s-tools',
+  'el-icon-s-check',
+  'el-icon-s-data',
+  'el-icon-s-custom',
+  'el-icon-s-operation',
+  'el-icon-s-order',
+  'el-icon-s-goods',
+  'el-icon-s-finance',
+  'el-icon-s-cooperation',
+  'el-icon-s-platform',
+  'el-icon-s-management',
+  'el-icon-setting',
+  'el-icon-user',
+  'el-icon-phone',
+  'el-icon-phone-outline',
+  'el-icon-connection',
+  'el-icon-chat-dot-round',
+  'el-icon-chat-dot-square',
+  'el-icon-chat-line-round',
+  'el-icon-message',
+  'el-icon-message-solid',
+  'el-icon-bell',
+  'el-icon-warning',
+  'el-icon-warning-outline',
+  'el-icon-star-off',
+  'el-icon-star-on',
+  'el-icon-key',
+  'el-icon-monitor',
+  'el-icon-cpu',
+  'el-icon-video-camera',
+  'el-icon-video-camera-solid',
+  'el-icon-video-play',
+  'el-icon-video-pause',
+  'el-icon-film',
+  'el-icon-picture',
+  'el-icon-document',
+  'el-icon-document-add',
+  'el-icon-notebook-2',
+  'el-icon-folder',
+  'el-icon-folder-opened',
+  'el-icon-data-line',
+  'el-icon-data-analysis',
+  'el-icon-data-board',
+  'el-icon-tickets',
+  'el-icon-shopping-bag-2',
+  'el-icon-shopping-cart-2',
+  'el-icon-box',
+  'el-icon-upload',
+  'el-icon-download',
+  'el-icon-link',
+  'el-icon-coin',
+  'el-icon-wallet',
+  'el-icon-money',
+  'el-icon-bank-card',
+  'el-icon-office-building',
+  'el-icon-school',
+  'el-icon-house',
+  'el-icon-trophy',
+  'el-icon-magic-stick',
+  'el-icon-finished',
+  'el-icon-time',
+  'el-icon-lock',
+  'el-icon-unlock',
+  'el-icon-search',
+  'el-icon-share',
+  'el-icon-collection',
+  'el-icon-edit',
+  'el-icon-delete',
+  'el-icon-plus',
+  'el-icon-minus',
+  'el-icon-check',
+  'el-icon-close',
+  'el-icon-refresh',
+  'el-icon-coordinate',
+  'el-icon-camera',
+  'el-icon-s-release',
+  'el-icon-s-shop',
+  'el-icon-s-marketing',
+  'el-icon-s-flag',
+  'el-icon-s-comment',
+  'el-icon-s-ticket'
+]
+
+export default elIcons

+ 49 - 18
src/components/IconSelect/index.vue

@@ -1,34 +1,63 @@
 <!-- @author zhengjie -->
 <template>
   <div class="icon-body">
-    <el-input v-model="name" style="position: relative;" clearable placeholder="请输入图标名称" @clear="filterIcons" @input.native="filterIcons">
-      <i slot="suffix" class="el-icon-search el-input__icon" />
-    </el-input>
-    <div class="icon-list">
-      <div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
-        <svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
-        <span>{{ item }}</span>
-      </div>
-    </div>
+    <el-tabs v-model="activeTab" size="mini">
+      <el-tab-pane label="Element图标" name="el-icon">
+        <el-input v-model="name" style="position: relative;" clearable placeholder="请输入图标名称" @clear="filterIcons" @input.native="filterIcons" size="mini">
+          <i slot="suffix" class="el-icon-search el-input__icon" />
+        </el-input>
+        <div class="icon-list">
+          <div v-for="(item, index) in filteredElIcons" :key="'el-'+index" @click="selectedIcon(item)">
+            <i :class="item" style="font-size: 16px; width: 16px; text-align: center;" />
+            <span>{{ item.replace('el-icon-', '') }}</span>
+          </div>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="SVG图标" name="svg-icon">
+        <el-input v-model="name" style="position: relative;" clearable placeholder="请输入图标名称" @clear="filterIcons" @input.native="filterIcons" size="mini">
+          <i slot="suffix" class="el-icon-search el-input__icon" />
+        </el-input>
+        <div class="icon-list">
+          <div v-for="(item, index) in filteredSvgIcons" :key="'svg-'+index" @click="selectedIcon(item)">
+            <svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
+            <span>{{ item }}</span>
+          </div>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
   </div>
 </template>
 
 <script>
-import icons from './requireIcons'
+import svgIcons from './requireIcons'
+import elIcons from './elIcons'
 export default {
   name: 'IconSelect',
   data() {
     return {
       name: '',
-      iconList: icons
+      activeTab: 'el-icon',
+      svgIconList: svgIcons,
+      elIconList: elIcons
     }
   },
-  methods: {
-    filterIcons() {
-      this.iconList = icons
+  computed: {
+    filteredElIcons() {
+      if (this.name) {
+        return this.elIconList.filter(item => item.includes(this.name))
+      }
+      return this.elIconList
+    },
+    filteredSvgIcons() {
       if (this.name) {
-        this.iconList = this.iconList.filter(item => item.includes(this.name))
+        return this.svgIconList.filter(item => item.includes(this.name))
       }
+      return this.svgIconList
+    }
+  },
+  methods: {
+    filterIcons() {
+      // 过滤逻辑已移至 computed
     },
     selectedIcon(name) {
       this.$emit('selected', name)
@@ -36,7 +65,7 @@ export default {
     },
     reset() {
       this.name = ''
-      this.iconList = icons
+      this.activeTab = 'el-icon'
     }
   }
 }
@@ -47,14 +76,14 @@ export default {
     width: 100%;
     padding: 10px;
     .icon-list {
-      height: 200px;
+      height: 180px;
       overflow-y: scroll;
       div {
         height: 30px;
         line-height: 30px;
         margin-bottom: -5px;
         cursor: pointer;
-        width: 33%;
+        width: 50%;
         float: left;
       }
       span {
@@ -62,6 +91,8 @@ export default {
         vertical-align: -0.15em;
         fill: currentColor;
         overflow: hidden;
+        font-size: 12px;
+        margin-left: 4px;
       }
     }
   }

+ 4 - 1
src/layout/AdminLayout.vue

@@ -163,7 +163,10 @@ export default {
     // 从Vuex Store动态路由中读取菜单结构(替代硬编码menuData)
     menuData() {
       const routes = this.$store.state.permission.sidebarRouters || []
-      const adminRoute = routes.find(r => r.path === '/admin')
+      // 优先查找非 hidden 的 /admin 路由(动态路由含完整菜单),
+      // 避免 constantRoutes 中的兜底路由(hidden:true,子路由不全)被优先匹配
+      const adminRoute = routes.find(r => r.path === '/admin' && !r.hidden)
+        || routes.find(r => r.path === '/admin')
       if (!adminRoute || !adminRoute.children) return []
 
       return adminRoute.children

+ 5 - 1
src/layout/components/Sidebar/Item.vue

@@ -16,7 +16,11 @@ export default {
     const { icon, title } = context.props
     const vnodes = []
 
-    vnodes.push(<svg-icon icon-class={icon || 'tree-table'}/>)
+    if (icon && icon.startsWith('el-icon-')) {
+      vnodes.push(<i class={icon}/>)
+    } else {
+      vnodes.push(<svg-icon icon-class={icon || 'tree-table'}/>)
+    }
 
     if (title) {
       vnodes.push(<span slot='title'>{(title)}</span>)

+ 11 - 2
src/router/index.js

@@ -245,8 +245,17 @@ export const constantRoutes = [
   ]
   },
   // ======== 龙虾引擎 (Lobster Workflow Engine) ========
-  // 已迁移到 sys_menu 数据库管理,不再硬编码到 constantRoutes
-  // 子路由在 Admin > Lobster-mgmt 下通过数据库驱动
+  // 兜底路由:当 require([]) 动态加载龙虾视图遇到 webpack 上下文遗漏时,
+  // 静态 import() 确保编译产出对应 chunk;hidden:true 不参与菜单渲染
+  {
+    path: '/admin',
+    component: () => import('@/layout/AdminLayout'),
+    hidden: true,
+    children: [
+      { path: 'workflowGenerate', component: () => import('@/views/lobster/workflow-generate/index'), name: 'AdminWorkflowGenerate', meta: { title: 'AI生成工作流' } },
+      { path: 'salesCorpus', component: () => import('@/views/lobster/sales-corpus/index'), name: 'AdminSalesCorpus', meta: { title: '销冠语料学习' } }
+    ]
+  },
 ]
 
 const originalPush = Router.prototype.push

+ 3 - 11
src/views/admin/menu.js

@@ -44,7 +44,6 @@ const adminRoutes = {
     // 7. 外呼管理
     { path: 'voice', component: () => import('@/views/admin/voice/index'), name: 'AdminVoice', meta: { title: '外呼管理' } },
     { path: 'voiceApi', component: () => import('@/views/admin/voiceApi/index'), name: 'AdminVoiceApi', meta: { title: '通话接口管理' } },
-    { path: 'voiceNumber', component: () => import('@/views/admin/voiceNumber/index'), name: 'AdminVoiceNumber', meta: { title: '号码管理' } },
     { path: 'voicePackage', component: () => import('@/views/admin/voicePackage/index'), name: 'AdminVoicePackage', meta: { title: '通话套餐管理' } },
     { path: 'voiceSeat', component: () => import('@/views/admin/voiceSeat/index'), name: 'AdminVoiceSeat', meta: { title: '坐席管理' } },
     { path: 'voiceBlacklist', component: () => import('@/views/admin/voiceBlacklist/index'), name: 'AdminVoiceBlacklist', meta: { title: '黑名单管理' } },
@@ -53,6 +52,8 @@ const adminRoutes = {
 
     // 8. 短信管理
     { path: 'sms', component: () => import('@/views/admin/sms/index'), name: 'AdminSms', meta: { title: '短信管理' } },
+    { path: 'smsApi', component: () => import('@/views/admin/smsApi/index'), name: 'AdminSmsApi', meta: { title: '短信接口' } },
+    { path: 'smsApiTenant', component: () => import('@/views/admin/smsApiTenant/index'), name: 'AdminSmsApiTenant', meta: { title: '短信接口绑定' } },
     { path: 'smsPackage', component: () => import('@/views/admin/smsPackage/index'), name: 'AdminSmsPackage', meta: { title: '短信套餐' } },
     { path: 'smsOrder', component: () => import('@/views/admin/smsOrder/index'), name: 'AdminSmsOrder', meta: { title: '短信订单' } },
 
@@ -71,7 +72,6 @@ const adminRoutes = {
 
     // 11. 内容审计
     { path: 'videoResource', component: () => import('@/views/admin/videoResource/index'), name: 'AdminVideoResource', meta: { title: '视频资源' } },
-    { path: 'course', component: () => import('@/views/admin/course/index'), name: 'AdminCourse', meta: { title: '公域课程管理' } },
     { path: 'live', component: () => import('@/views/admin/live/index'), name: 'AdminLive', meta: { title: '直播间' } },
     { path: 'liveVideo', component: () => import('@/views/admin/liveVideo/index'), name: 'AdminLiveVideo', meta: { title: '直播视频' } },
     { path: 'product', component: () => import('@/views/admin/product/index'), name: 'AdminProduct', meta: { title: '商品管理' } },
@@ -80,7 +80,6 @@ const adminRoutes = {
 
     // 12. AI模型管理(统一配置)
     { path: 'aiModel', component: () => import('@/views/admin/aiModel/index'), name: 'AdminAiModel', meta: { title: 'AI模型配置' } },
-    { path: 'aiProvider', component: () => import('@/views/admin/aiProvider/index'), name: 'AdminAiProvider', meta: { title: '大模型管理(旧)' } },
 
     // 13. 其他管理
     { path: 'ipadServer', component: () => import('@/views/admin/ipadServer/index'), name: 'AdminIpadServer', meta: { title: 'Ipad服务器' } },
@@ -89,17 +88,10 @@ const adminRoutes = {
     // 14. Lobster 引擎(挂在 /admin/lobster-mgmt 下,通过 sys_menu 数据库驱动)
     { path: 'workflowGenerate', component: () => import('@/views/lobster/workflow-generate/index'), name: 'AdminWorkflowGenerate', meta: { title: 'AI生成工作流' } },
     { path: 'salesCorpus', component: () => import('@/views/lobster/sales-corpus/index'), name: 'AdminSalesCorpus', meta: { title: '销冠语料学习' } },
-    { path: 'workflowCanvas', component: () => import('@/views/lobster/workflow-canvas/index'), name: 'AdminWorkflowCanvas', meta: { title: '工作流画布' } },
-    { path: 'workflowTemplate', component: () => import('@/views/lobster/template/index'), name: 'AdminWorkflowTemplate', meta: { title: '工作流模板库' } },
     { path: 'instance', component: () => import('@/views/lobster/instance/index'), name: 'AdminLobsterInstance', meta: { title: '实例监控' } },
     { path: 'optimization', component: () => import('@/views/lobster/optimization/index'), name: 'AdminLobsterOptimization', meta: { title: 'AI优化建议' } },
     { path: 'prompt', component: () => import('@/views/lobster/prompt/index'), name: 'AdminLobsterPrompt', meta: { title: '提示词管理' } },
-    { path: 'apiRegistry', component: () => import('@/views/lobster/api-registry/index'), name: 'AdminLobsterApiRegistry', meta: { title: '接口注册中心' } },
-    { path: 'deadLetter', component: () => import('@/views/lobster/dead-letter/index'), name: 'AdminLobsterDeadLetter', meta: { title: '死信队列' } },
-    { path: 'eventAudit', component: () => import('@/views/lobster/event-audit/index'), name: 'AdminLobsterEventAudit', meta: { title: '节点审核' } },
-    { path: 'chatAggregate', component: () => import('@/views/lobster/chat-aggregate/index'), name: 'AdminLobsterChatAggregate', meta: { title: '聚合聊天' } },
-    { path: 'lobsterModelConfig', component: () => import('@/views/lobster/model-config/index'), name: 'AdminLobsterModelConfig', meta: { title: '模型配置' } },
-    { path: 'billing', component: () => import('@/views/lobster/billing/index'), name: 'AdminLobsterBilling', meta: { title: 'Token系数管理' } }
+    { path: 'deadLetter', component: () => import('@/views/lobster/dead-letter/index'), name: 'AdminLobsterDeadLetter', meta: { title: '死信队列' } }
   ]
 }
 

+ 260 - 0
src/views/admin/smsApi/index.vue

@@ -0,0 +1,260 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
+      <el-form-item label="接口名称" prop="apiName">
+        <el-input v-model="queryParams.apiName" placeholder="请输入接口名称" clearable @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="短信类型" prop="smsType">
+        <el-select v-model="queryParams.smsType" placeholder="请选择" clearable>
+          <el-option v-for="item in smsTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="服务商" prop="provider">
+        <el-select v-model="queryParams.provider" placeholder="请选择" clearable>
+          <el-option label="润方" value="rf" />
+          <el-option label="德华" value="dh" />
+        </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-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="apiList" border size="small">
+      <el-table-column label="ID" align="center" prop="apiId" width="60" />
+      <el-table-column label="接口名称" align="center" prop="apiName" min-width="140" />
+      <el-table-column label="短信类型" align="center" prop="smsType" width="140">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.smsType === 1 ? 'success' : scope.row.smsType === 2 ? 'warning' : 'danger'" size="small">
+            {{ smsTypeFormat(scope.row.smsType) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="服务商" align="center" prop="provider" width="80">
+        <template slot-scope="scope">{{ scope.row.provider === 'rf' ? '润方' : '德华' }}</template>
+      </el-table-column>
+      <el-table-column label="账户名" align="center" prop="account" min-width="100" show-overflow-tooltip />
+      <el-table-column label="签名" align="center" prop="sign" width="80" />
+      <el-table-column label="成本价(元/条)" align="center" prop="costPrice" width="120">
+        <template slot-scope="scope">
+          <span style="color:#e6a23c;font-weight:bold">{{ scope.row.costPrice || '-' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="默认" align="center" prop="isDefault" width="60">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.isDefault === 1" type="success" size="mini">是</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" width="70">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="small">{{ scope.row.status === 1 ? '正常' : '禁用' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="160">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="680px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="接口名称" prop="apiName">
+              <el-input v-model="form.apiName" placeholder="如:润方行业验证码通知" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="短信类型" prop="smsType">
+              <el-select v-model="form.smsType" placeholder="请选择" style="width:100%">
+                <el-option v-for="item in smsTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="服务商" prop="provider">
+              <el-select v-model="form.provider" placeholder="请选择" style="width:100%">
+                <el-option label="润方" value="rf" />
+                <el-option label="德华" value="dh" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="成本价(元/条)" prop="costPrice">
+              <el-input-number v-model="form.costPrice" :precision="4" :step="0.001" :min="0" style="width:100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-divider content-position="left">通道配置</el-divider>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="账户名" prop="account">
+              <el-input v-model="form.account" placeholder="请输入账户名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="密码" prop="password">
+              <el-input v-model="form.password" placeholder="请输入密码" show-password />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="短信签名">
+              <el-input v-model="form.sign" placeholder="请输入短信签名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.provider === 'rf'" label="接口地址">
+              <el-input v-model="form.url" placeholder="润方专用" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row v-if="form.provider === 'rf'" :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="扩展码">
+              <el-input v-model="form.code" placeholder="润方专用" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="是否默认">
+              <el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态">
+              <el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="正常" inactive-text="禁用" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listSmsApi, getSmsApi, addSmsApi, updateSmsApi, delSmsApi } from '@/api/system/smsApi'
+
+export default {
+  name: 'AdminSmsApi',
+  data() {
+    return {
+      loading: false,
+      apiList: [],
+      dialogVisible: false,
+      dialogTitle: '',
+      smsTypeOptions: [
+        { value: 1, label: '行业验证码通知短信' },
+        { value: 2, label: '营销短信' },
+        { value: 3, label: '5G消息' }
+      ],
+      queryParams: { apiName: undefined, smsType: undefined, provider: undefined },
+      form: {},
+      rules: {
+        apiName: [{ required: true, message: '接口名称不能为空', trigger: 'blur' }],
+        smsType: [{ required: true, message: '请选择短信类型', trigger: 'change' }],
+        provider: [{ required: true, message: '请选择服务商', trigger: 'change' }],
+        account: [{ required: true, message: '账户名不能为空', trigger: 'blur' }],
+        password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
+        costPrice: [{ required: true, message: '成本价不能为空', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    smsTypeFormat(type) {
+      const map = { 1: '行业验证码通知', 2: '营销短信', 3: '5G消息' }
+      return map[type] || '未知'
+    },
+    getList() {
+      this.loading = true
+      listSmsApi(this.queryParams).then(res => {
+        this.apiList = res.data || []
+      }).finally(() => { this.loading = false })
+    },
+    handleQuery() { this.getList() },
+    resetQuery() {
+      this.queryParams = { apiName: undefined, smsType: undefined, provider: undefined }
+      this.handleQuery()
+    },
+    resetForm() {
+      this.form = {
+        apiId: undefined, apiName: '', smsType: 1, provider: 'rf',
+        account: '', password: '', url: '', code: '', sign: '',
+        costPrice: 0, isDefault: 0, status: 1, remark: ''
+      }
+    },
+    handleAdd() {
+      this.resetForm()
+      this.dialogTitle = '新增短信接口'
+      this.dialogVisible = true
+    },
+    handleUpdate(row) {
+      this.resetForm()
+      getSmsApi(row.apiId).then(res => {
+        this.form = res.data
+        this.dialogTitle = '修改短信接口'
+        this.dialogVisible = true
+      })
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        if (this.form.apiId) {
+          updateSmsApi(this.form).then(() => {
+            this.$message.success('修改成功')
+            this.dialogVisible = false
+            this.getList()
+          })
+        } else {
+          addSmsApi(this.form).then(() => {
+            this.$message.success('新增成功')
+            this.dialogVisible = false
+            this.getList()
+          })
+        }
+      })
+    },
+    handleDelete(row) {
+      this.$confirm('确认删除接口"' + row.apiName + '"?删除后租户绑定也将同步解除。', '提示', {
+        confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
+      }).then(() => {
+        delSmsApi(row.apiId).then(() => {
+          this.$message.success('删除成功')
+          this.getList()
+        })
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+</style>

+ 249 - 0
src/views/admin/smsApiTenant/index.vue

@@ -0,0 +1,249 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
+      <el-form-item label="租户" prop="companyId">
+        <el-select v-model="queryParams.companyId" placeholder="选择租户" clearable filterable>
+          <el-option v-for="c in companyList" :key="c.companyId" :label="c.companyName" :value="c.companyId" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="短信类型" prop="smsType">
+        <el-select v-model="queryParams.smsType" placeholder="请选择" clearable>
+          <el-option v-for="item in smsTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+        </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-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="bindList" border size="small">
+      <el-table-column label="ID" align="center" prop="id" width="60" />
+      <el-table-column label="租户名称" align="center" prop="companyName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="接口名称" align="center" prop="apiName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="短信类型" align="center" prop="smsType" width="140">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.smsType === 1 ? 'success' : scope.row.smsType === 2 ? 'warning' : 'danger'" size="small">
+            {{ smsTypeFormat(scope.row.smsType) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="服务商" align="center" prop="provider" width="80">
+        <template slot-scope="scope">{{ scope.row.provider === 'rf' ? '润方' : '德华' }}</template>
+      </el-table-column>
+      <el-table-column label="成本价" align="center" prop="costPrice" width="100">
+        <template slot-scope="scope">
+          <span style="color:#909399">{{ scope.row.costPrice || '-' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="租户售价(元/条)" align="center" prop="price" width="130">
+        <template slot-scope="scope">
+          <span style="color:#e6a23c;font-weight:bold">{{ scope.row.price }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="利润(元/条)" align="center" width="110">
+        <template slot-scope="scope">
+          <span style="color:#67c23a;font-weight:bold">{{ calcProfit(scope.row) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" width="70">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="small">{{ scope.row.status === 1 ? '启用' : '禁用' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="160">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">调价</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(scope.row)">解除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增绑定弹窗 -->
+    <el-dialog title="新增短信接口-租户绑定" :visible.sync="addDialogVisible" width="500px" append-to-body>
+      <el-form ref="addForm" :model="addForm" :rules="addRules" label-width="100px" size="small">
+        <el-form-item label="选择接口" prop="apiId">
+          <el-select v-model="addForm.apiId" placeholder="请选择短信接口" style="width:100%" @change="onApiChange">
+            <el-option v-for="api in apiOptions" :key="api.apiId" :label="api.apiName + ' (' + smsTypeFormat(api.smsType) + ')'" :value="api.apiId" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="选择租户" prop="companyId">
+          <el-select v-model="addForm.companyId" placeholder="请选择租户" filterable style="width:100%">
+            <el-option v-for="c in companyList" :key="c.companyId" :label="c.companyName" :value="c.companyId" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="租户售价" prop="price">
+          <el-input-number v-model="addForm.price" :precision="4" :step="0.001" :min="0" style="width:100%" />
+          <span style="color:#909399;font-size:12px;margin-left:8px">元/条</span>
+        </el-form-item>
+        <el-form-item v-if="selectedApiCost" label="">
+          <span style="color:#909399;font-size:12px">该接口成本价: {{ selectedApiCost }} 元/条,建议售价 >= 成本价</span>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button type="primary" @click="submitAdd">确 定</el-button>
+        <el-button @click="addDialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 调价弹窗 -->
+    <el-dialog title="调整租户售价" :visible.sync="editDialogVisible" width="450px" append-to-body>
+      <el-form ref="editForm" :model="editForm" label-width="100px" size="small">
+        <el-form-item label="租户">
+          <span>{{ editForm.companyName }}</span>
+        </el-form-item>
+        <el-form-item label="接口">
+          <span>{{ editForm.apiName }}</span>
+        </el-form-item>
+        <el-form-item label="成本价">
+          <span style="color:#909399">{{ editForm.costPrice }} 元/条</span>
+        </el-form-item>
+        <el-form-item label="租户售价" prop="price">
+          <el-input-number v-model="editForm.price" :precision="4" :step="0.001" :min="0" style="width:100%" />
+          <span style="color:#909399;font-size:12px;margin-left:8px">元/条</span>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-switch v-model="editForm.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button type="primary" @click="submitEdit">确 定</el-button>
+        <el-button @click="editDialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listSmsApiTenant, addSmsApiTenant, updateSmsApiTenant, delSmsApiTenant, listSmsApi } from '@/api/system/smsApi'
+import { listAllCompanies } from '@/api/admin/sysCompany'
+
+export default {
+  name: 'AdminSmsApiTenant',
+  data() {
+    return {
+      loading: false,
+      bindList: [],
+      companyList: [],
+      apiOptions: [],
+      smsTypeOptions: [
+        { value: 1, label: '行业验证码通知短信' },
+        { value: 2, label: '营销短信' },
+        { value: 3, label: '5G消息' }
+      ],
+      queryParams: { companyId: undefined, smsType: undefined },
+      addDialogVisible: false,
+      editDialogVisible: false,
+      addForm: { apiId: undefined, companyId: undefined, price: 0 },
+      editForm: { id: undefined, price: 0, status: 1, companyName: '', apiName: '', costPrice: 0 },
+      addRules: {
+        apiId: [{ required: true, message: '请选择接口', trigger: 'change' }],
+        companyId: [{ required: true, message: '请选择租户', trigger: 'change' }],
+        price: [{ required: true, message: '请填写售价', trigger: 'blur' }]
+      }
+    }
+  },
+  computed: {
+    selectedApiCost() {
+      const api = this.apiOptions.find(a => a.apiId === this.addForm.apiId)
+      return api && api.costPrice ? api.costPrice : ''
+    }
+  },
+  created() {
+    this.getList()
+    this.loadCompanyList()
+    this.loadApiOptions()
+  },
+  methods: {
+    smsTypeFormat(type) {
+      const map = { 1: '行业验证码通知', 2: '营销短信', 3: '5G消息' }
+      return map[type] || '未知'
+    },
+    calcProfit(row) {
+      if (row.price != null && row.costPrice != null) {
+        return (row.price - row.costPrice).toFixed(4)
+      }
+      return '-'
+    },
+    getList() {
+      this.loading = true
+      listSmsApiTenant(this.queryParams).then(res => {
+        this.bindList = res.data || []
+      }).finally(() => { this.loading = false })
+    },
+    loadCompanyList() {
+      listAllCompanies({ pageNum: 1, pageSize: 1000 }).then(res => {
+        this.companyList = res.rows || res.data || []
+      })
+    },
+    loadApiOptions() {
+      listSmsApi({ status: 1 }).then(res => {
+        this.apiOptions = res.data || []
+      })
+    },
+    onApiChange(apiId) {
+      // 当选择接口时,自动填充默认售价
+      const api = this.apiOptions.find(a => a.apiId === apiId)
+      if (api && api.costPrice) {
+        this.addForm.price = api.costPrice
+      }
+    },
+    handleQuery() { this.getList() },
+    resetQuery() {
+      this.queryParams = { companyId: undefined, smsType: undefined }
+      this.handleQuery()
+    },
+    handleAdd() {
+      this.addForm = { apiId: undefined, companyId: undefined, price: 0 }
+      this.addDialogVisible = true
+    },
+    submitAdd() {
+      this.$refs.addForm.validate(valid => {
+        if (!valid) return
+        addSmsApiTenant({ ...this.addForm, status: 1 }).then(() => {
+          this.$message.success('绑定成功')
+          this.addDialogVisible = false
+          this.getList()
+        })
+      })
+    },
+    handleUpdate(row) {
+      this.editForm = {
+        id: row.id,
+        price: row.price,
+        status: row.status,
+        companyName: row.companyName,
+        apiName: row.apiName,
+        costPrice: row.costPrice
+      }
+      this.editDialogVisible = true
+    },
+    submitEdit() {
+      updateSmsApiTenant({ id: this.editForm.id, price: this.editForm.price, status: this.editForm.status }).then(() => {
+        this.$message.success('修改成功')
+        this.editDialogVisible = false
+        this.getList()
+      })
+    },
+    handleDelete(row) {
+      this.$confirm('确认解除租户"' + row.companyName + '"与接口"' + row.apiName + '"的绑定?', '提示', {
+        confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
+      }).then(() => {
+        delSmsApiTenant(row.id).then(() => {
+          this.$message.success('解除绑定成功')
+          this.getList()
+        })
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+</style>

+ 9 - 2
src/views/admin/sysCompany/index.vue

@@ -234,7 +234,6 @@
           ref="menuTree"
           :data="menuDialog.treeData"
           show-checkbox
-          check-strictly
           node-key="id"
           :default-checked-keys="menuDialog.checkedKeys"
           :props="{ label: 'label', children: 'children' }"
@@ -512,6 +511,13 @@ export default {
           walk(menus)
           this.menuDialog.checkedKeys = checkedIds
           this.menuDialog.allMenuIds = allIds
+          // 等tree渲染后捕获级联展开的实际勾选状态作为基准
+          this.$nextTick(() => {
+            const tree = this.$refs.menuTree
+            if (tree) {
+              this.menuDialog.checkedKeys = tree.getCheckedKeys().concat(tree.getHalfCheckedKeys())
+            }
+          })
         } else {
           this.$message.error(res.msg || '加载菜单失败')
         }
@@ -525,7 +531,8 @@ export default {
     submitMenuEdit() {
       const tree = this.$refs.menuTree
       if (!tree) return
-      const currentChecked = tree.getCheckedKeys()
+      // 级联模式:纳入全选+半选节点作为当前勾选状态
+      const currentChecked = tree.getCheckedKeys().concat(tree.getHalfCheckedKeys())
       const originalChecked = this.menuDialog.checkedKeys
       // selected = 新勾选的(当前有,原来没有)
       const selected = currentChecked.filter(id => !originalChecked.includes(id))

+ 291 - 69
src/views/admin/voiceApi/index.vue

@@ -2,14 +2,27 @@
   <div class="app-container">
     <el-card shadow="never" class="mb16 filter-card">
       <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
-        <el-form-item label="租户名称" prop="companyName">
+        <el-form-item label="接口名称" prop="apiName">
           <el-input
-            v-model="queryParams.companyName"
-            placeholder="请输入租户名称"
+            v-model="queryParams.apiName"
+            placeholder="请输入接口名称"
             clearable
             @keyup.enter.native="handleQuery"
           />
         </el-form-item>
+        <el-form-item label="接口类型" prop="apiType">
+          <el-select v-model="queryParams.apiType" placeholder="请选择类型" clearable size="small">
+            <el-option label="SIP" value="0" />
+            <el-option label="网关" value="1" />
+            <el-option label="API" value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+            <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>
@@ -18,18 +31,25 @@
     </el-card>
 
     <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
     <el-table v-loading="loading" :data="dataList" border size="small" style="width:100%">
       <el-table-column label="接口ID" align="center" prop="apiId" min-width="70" />
-      <el-table-column label="所属租户" align="center" prop="companyName" min-width="120" show-overflow-tooltip />
-      <el-table-column label="接口类型" align="center" prop="apiType" min-width="100">
+      <el-table-column label="接口类型" align="center" prop="apiType" min-width="80">
         <template slot-scope="scope">
           <span>{{ getApiTypeLabel(scope.row.apiType) }}</span>
         </template>
       </el-table-column>
       <el-table-column label="接口名称" align="center" prop="apiName" min-width="130" show-overflow-tooltip />
+      <el-table-column label="已分配租户" align="center" min-width="100">
+        <template slot-scope="scope">
+          <el-button type="text" @click="handleViewTenants(scope.row)">{{ scope.row.tenantCount || 0 }}个租户</el-button>
+        </template>
+      </el-table-column>
       <el-table-column label="状态" align="center" prop="status" min-width="80">
         <template slot-scope="scope">
           <el-tag v-if="scope.row.status === 0 || scope.row.status === '0'" type="danger">禁用</el-tag>
@@ -37,14 +57,11 @@
         </template>
       </el-table-column>
       <el-table-column label="创建时间" align="center" prop="createTime" min-width="150" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="240">
         <template slot-scope="scope">
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-view"
-            @click="handleDetail(scope.row)"
-          >详情</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">编辑</el-button>
+          <el-button size="mini" type="text" icon="el-icon-user" @click="handleAssignTenant(scope.row)">分配租户</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -57,54 +74,105 @@
       @pagination="getList"
     />
 
-    <!-- 详情弹窗 -->
-    <el-dialog title="接口详情" :visible.sync="detailOpen" width="600px" append-to-body>
-      <el-form :model="detailForm" label-width="100px" size="small">
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="接口ID">
-              <span>{{ detailForm.apiId }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="所属租户">
-              <span>{{ detailForm.companyName }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="接口类型">
-              <span>{{ getApiTypeLabel(detailForm.apiType) }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="接口名称">
-              <span>{{ detailForm.apiName }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="状态">
-              <el-tag v-if="detailForm.status === 0 || detailForm.status === '0'" type="danger">禁用</el-tag>
-              <el-tag v-else type="success">启用</el-tag>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="创建时间">
-              <span>{{ detailForm.createTime }}</span>
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="备注">
-              <span>{{ detailForm.remark || '-' }}</span>
-            </el-form-item>
-          </el-col>
-        </el-row>
+    <!-- 新增/编辑接口弹窗 -->
+    <el-dialog :title="formTitle" :visible.sync="formOpen" width="650px" append-to-body>
+      <el-form ref="apiForm" :model="apiForm" :rules="apiRules" label-width="120px" size="small">
+        <el-form-item label="接口名称" prop="apiName">
+          <el-input v-model="apiForm.apiName" placeholder="请输入接口名称" />
+        </el-form-item>
+        <el-form-item label="接口类型" prop="apiType">
+          <el-select v-model="apiForm.apiType" placeholder="请选择类型" clearable size="small" style="width:100%">
+            <el-option label="SIP" value="0" />
+            <el-option label="网关" value="1" />
+            <el-option label="API" value="2" />
+          </el-select>
+        </el-form-item>
+        <template v-if="apiForm.apiType === '1' || apiForm.apiType === '2'">
+          <el-form-item label="帐号">
+            <el-input v-model="apiForm.apiJsonObj.account" placeholder="请输入帐号" />
+          </el-form-item>
+          <el-form-item label="密码">
+            <el-input v-model="apiForm.apiJsonObj.password" placeholder="请输入密码" />
+          </el-form-item>
+          <el-form-item label="接口地址">
+            <el-input v-model="apiForm.apiJsonObj.url" placeholder="请输入接口地址" />
+          </el-form-item>
+        </template>
+        <template v-if="apiForm.apiType === '2'">
+          <el-form-item label="话术跳转地址">
+            <el-input v-model="apiForm.apiJsonObj.dialogUrl" placeholder="请输入话术跳转地址" />
+          </el-form-item>
+        </template>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="apiForm.status">
+            <el-radio label="1">启用</el-radio>
+            <el-radio label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input type="textarea" v-model="apiForm.remark" placeholder="请输入备注" />
+        </el-form-item>
       </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitApiForm" :loading="formSubmitting">确 定</el-button>
+        <el-button @click="formOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 分配租户弹窗 -->
+    <el-dialog title="分配租户" :visible.sync="assignOpen" width="700px" append-to-body>
+      <div style="margin-bottom:12px">
+        <span>接口:<strong>{{ assignApi.apiName }}</strong>(ID: {{ assignApi.apiId }})</span>
+      </div>
+      <!-- 已分配租户列表 -->
+      <el-table :data="assignedTenants" border size="small" style="width:100%;margin-bottom:12px" v-loading="assignLoading">
+        <el-table-column label="租户ID" align="center" prop="companyId" width="80" />
+        <el-table-column label="租户名称" align="center" prop="companyName" />
+        <el-table-column label="状态" align="center" prop="status" width="80">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{ scope.row.status === 1 ? '启用' : '禁用' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="100">
+          <template slot-scope="scope">
+            <el-button size="mini" type="text" style="color:#F56C6C" @click="handleUnassign(scope.row)">取消分配</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 添加租户 -->
+      <el-divider>添加租户</el-divider>
+      <el-select v-model="selectedCompanyIds" multiple filterable remote reserve-keyword
+        placeholder="输入租户名称搜索" :remote-method="searchCompanies" :loading="companySearchLoading"
+        style="width:100%" size="small">
+        <el-option v-for="item in companyOptions" :key="item.companyId" :label="item.companyName" :value="item.companyId" />
+      </el-select>
+      <div style="margin-top:12px;text-align:right">
+        <el-button type="primary" size="small" @click="submitAssign" :loading="assignSubmitting" :disabled="selectedCompanyIds.length === 0">确认分配</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 查看已分配租户弹窗 -->
+    <el-dialog title="已分配租户" :visible.sync="tenantListOpen" width="600px" append-to-body>
+      <el-table :data="viewTenants" border size="small" style="width:100%" v-loading="viewTenantsLoading">
+        <el-table-column label="租户ID" align="center" prop="companyId" width="80" />
+        <el-table-column label="租户名称" align="center" prop="companyName" />
+        <el-table-column label="状态" align="center" prop="status" width="80">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{ scope.row.status === 1 ? '启用' : '禁用' }}</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
     </el-dialog>
   </div>
 </template>
 
 <script>
-import { listCompanyVoiceApi, getCompanyVoiceApi } from '@/api/company/companyVoiceApi'
+import {
+  listCompanyVoiceApi, getCompanyVoiceApi,
+  addCompanyVoiceApi, updateCompanyVoiceApi, delCompanyVoiceApi,
+  getAssignedTenants, assignTenants, unassignTenant, getTenantCount
+} from '@/api/company/companyVoiceApi'
+import { listAllCompanies } from '@/api/admin/sysCompany'
 
 export default {
   name: 'AdminVoiceApi',
@@ -117,15 +185,41 @@ export default {
       queryParams: {
         pageNum: 1,
         pageSize: 10,
-        companyName: null
+        apiName: null,
+        apiType: null,
+        status: null
       },
-      detailOpen: false,
-      detailForm: {},
-      apiTypeMap: {
-        0: 'SIP',
-        1: '网关',
-        2: 'API'
-      }
+      apiTypeMap: { '0': 'SIP', '1': '网关', '2': 'API' },
+      // 新增/编辑表单
+      formOpen: false,
+      formTitle: '',
+      formSubmitting: false,
+      apiForm: {
+        apiId: null,
+        apiName: null,
+        apiType: '1',
+        apiJsonObj: {},
+        status: '1',
+        remark: null
+      },
+      apiRules: {
+        apiName: [{ required: true, message: '接口名称不能为空', trigger: 'blur' }],
+        apiType: [{ required: true, message: '接口类型不能为空', trigger: 'change' }],
+        status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+      },
+      // 分配租户
+      assignOpen: false,
+      assignApi: {},
+      assignLoading: false,
+      assignSubmitting: false,
+      assignedTenants: [],
+      selectedCompanyIds: [],
+      companyOptions: [],
+      companySearchLoading: false,
+      // 查看租户列表
+      tenantListOpen: false,
+      viewTenants: [],
+      viewTenantsLoading: false
     }
   },
   created() {
@@ -135,9 +229,17 @@ export default {
     getList() {
       this.loading = true
       listCompanyVoiceApi(this.queryParams).then(response => {
-        this.dataList = response.rows
-        this.total = response.total
+        this.dataList = response.rows || []
+        this.total = response.total || 0
         this.loading = false
+        // 加载每个接口的租户数量
+        this.dataList.forEach(row => {
+          getTenantCount(row.apiId).then(res => {
+            this.$set(row, 'tenantCount', res.data || 0)
+          }).catch(() => {
+            this.$set(row, 'tenantCount', 0)
+          })
+        })
       })
     },
     handleQuery() {
@@ -148,14 +250,134 @@ export default {
       this.resetForm('queryForm')
       this.handleQuery()
     },
-    handleDetail(row) {
+    getApiTypeLabel(type) {
+      return this.apiTypeMap[String(type)] || '未知'
+    },
+    // 新增
+    handleAdd() {
+      this.resetApiForm()
+      this.formTitle = '新增通话接口'
+      this.formOpen = true
+    },
+    // 编辑
+    handleUpdate(row) {
+      this.resetApiForm()
       getCompanyVoiceApi(row.apiId).then(response => {
-        this.detailForm = response.data
-        this.detailOpen = true
+        const data = response.data
+        this.apiForm = {
+          apiId: data.apiId,
+          apiName: data.apiName,
+          apiType: String(data.apiType),
+          apiJsonObj: data.apiJson ? JSON.parse(data.apiJson) : {},
+          status: String(data.status),
+          remark: data.remark
+        }
+        this.formTitle = '编辑通话接口'
+        this.formOpen = true
       })
     },
-    getApiTypeLabel(type) {
-      return this.apiTypeMap[type] || '未知'
+    resetApiForm() {
+      this.apiForm = {
+        apiId: null,
+        apiName: null,
+        apiType: '1',
+        apiJsonObj: {},
+        status: '1',
+        remark: null
+      }
+      this.$nextTick(() => {
+        if (this.$refs.apiForm) this.$refs.apiForm.clearValidate()
+      })
+    },
+    submitApiForm() {
+      this.$refs.apiForm.validate(valid => {
+        if (!valid) return
+        this.formSubmitting = true
+        const data = { ...this.apiForm }
+        data.apiJson = JSON.stringify(data.apiJsonObj)
+        delete data.apiJsonObj
+        if (data.apiId) {
+          updateCompanyVoiceApi(data).then(() => {
+            this.$message.success('修改成功')
+            this.formOpen = false
+            this.getList()
+          }).finally(() => { this.formSubmitting = false })
+        } else {
+          addCompanyVoiceApi(data).then(() => {
+            this.$message.success('新增成功')
+            this.formOpen = false
+            this.getList()
+          }).finally(() => { this.formSubmitting = false })
+        }
+      })
+    },
+    // 删除
+    handleDelete(row) {
+      this.$confirm('是否确认删除接口"' + row.apiName + '"?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return delCompanyVoiceApi(row.apiId)
+      }).then(() => {
+        this.$message.success('删除成功')
+        this.getList()
+      }).catch(() => {})
+    },
+    // 分配租户
+    handleAssignTenant(row) {
+      this.assignApi = row
+      this.assignOpen = true
+      this.selectedCompanyIds = []
+      this.companyOptions = []
+      this.loadAssignedTenants()
+    },
+    loadAssignedTenants() {
+      this.assignLoading = true
+      getAssignedTenants(this.assignApi.apiId).then(response => {
+        this.assignedTenants = response.data || []
+      }).finally(() => { this.assignLoading = false })
+    },
+    searchCompanies(query) {
+      if (query.length < 1) return
+      this.companySearchLoading = true
+      listAllCompanies({ companyName: query, pageNum: 1, pageSize: 20 }).then(response => {
+        this.companyOptions = (response.rows || []).map(c => ({
+          companyId: c.companyId || c.id,
+          companyName: c.companyName || c.tenantName
+        }))
+      }).finally(() => { this.companySearchLoading = false })
+    },
+    submitAssign() {
+      if (this.selectedCompanyIds.length === 0) return
+      this.assignSubmitting = true
+      assignTenants(this.assignApi.apiId, this.selectedCompanyIds).then(() => {
+        this.$message.success('分配成功')
+        this.selectedCompanyIds = []
+        this.loadAssignedTenants()
+        this.getList()
+      }).finally(() => { this.assignSubmitting = false })
+    },
+    handleUnassign(row) {
+      this.$confirm('是否取消该租户的接口分配?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return unassignTenant(row.apiId, row.companyId)
+      }).then(() => {
+        this.$message.success('已取消分配')
+        this.loadAssignedTenants()
+        this.getList()
+      }).catch(() => {})
+    },
+    // 查看已分配租户
+    handleViewTenants(row) {
+      this.tenantListOpen = true
+      this.viewTenantsLoading = true
+      getAssignedTenants(row.apiId).then(response => {
+        this.viewTenants = response.data || []
+      }).finally(() => { this.viewTenantsLoading = false })
     }
   }
 }

+ 28 - 5
src/views/lobster/dead-letter/index.vue

@@ -1,5 +1,24 @@
 <template>
   <div class="app-container">
+    <!-- 搜索栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="租户" prop="tenantId">
+        <el-select v-model="queryParams.tenantId" placeholder="选择租户" clearable filterable size="small" style="width: 200px">
+          <el-option v-for="item in companyList" :key="item.companyId" :label="item.companyName" :value="item.companyId" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="渠道类型" prop="channelType">
+        <el-select v-model="queryParams.channelType" placeholder="请选择渠道" clearable size="small">
+          <el-option label="企微" value="qw" />
+          <el-option label="个微" value="wx" />
+          <el-option label="短信" value="sms" />
+        </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-row :gutter="10" class="mb8">
       <el-col :span="1.5"><el-button type="danger" plain icon="el-icon-refresh-right" size="mini" @click="handleRetryAll" v-hasPermi="['workflow:lobster:edit']">批量重发</el-button></el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
@@ -9,6 +28,7 @@
       <el-col :span="6"><el-card shadow="hover"><div class="stat-card"><div class="stat-value" style="color:#F56C6C">{{ stats.deadCount || 0 }}</div><div class="stat-label">死信数</div></div></el-card></el-col>
     </el-row>
     <el-table border v-loading="loading" :data="deadList">
+      <el-table-column label="租户" align="center" prop="tenant_name" width="140" />
       <el-table-column label="实例ID" align="center" prop="instanceId" width="80" />
       <el-table-column label="渠道类型" align="center" prop="channelType" width="100" />
       <el-table-column label="消息内容" align="center" prop="content" show-overflow-tooltip />
@@ -24,16 +44,19 @@ import { listDeadLetters, retryAllDeadLetters, getDeadLetterStats } from '@/api/
 export default {
   name: 'LobsterDeadLetter',
   data() {
-    return { loading: false, showSearch: true, deadList: [], stats: {} }
+    return { loading: false, showSearch: true, deadList: [], stats: {}, companyList: [],
+      queryParams: { tenantId: null, channelType: null }
+    }
   },
-  created() { this.getCompanyList()
-    this.getList(); this.getStats() },
+  created() { this.getCompanyList(); this.getList(); this.getStats() },
   methods: {
     getCompanyList() {
       listAllCompanies().then(res => { this.companyList = res.data || [] })
     },
-    getList() { this.loading = true; listDeadLetters().then(res => { let d = res.data || {}; this.deadList = d.items || []; this.loading = false }).catch(() => { this.loading = false }) },
-    getStats() { getDeadLetterStats().then(res => { this.stats = res.data || {} }) },
+    getList() { this.loading = true; listDeadLetters(this.queryParams).then(res => { let d = res.data || {}; this.deadList = d.items || []; this.loading = false }).catch(() => { this.loading = false }) },
+    getStats() { getDeadLetterStats(this.queryParams).then(res => { this.stats = res.data || {} }) },
+    handleQuery() { this.getList(); this.getStats() },
+    resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
     handleRetryAll() { this.$confirm('确定批量重发所有死信消息吗?', '提示', { type: 'warning' }).then(() => { retryAllDeadLetters().then(res => { this.$message.success(res.data?.message || '重发完成'); this.getList(); this.getStats() }) }) }
   }
 }

+ 1 - 5
src/views/lobster/instance/index.vue

@@ -78,6 +78,7 @@
     <!-- 表格 -->
     <el-table border v-loading="loading" :data="list">
       <el-table-column label="实例ID" align="center" prop="id" width="80" />
+      <el-table-column label="租户" align="center" prop="tenant_name" width="140" />
       <el-table-column label="工作流ID" align="center" prop="workflowId" width="80" />
       <el-table-column label="联系人" align="center" prop="contactId" width="100" />
       <el-table-column label="当前节点" align="center" prop="currentNodeName" />
@@ -191,9 +192,6 @@ export default {
     this.getList()
   },
   methods: {
-    getCompanyList() {
-      listAllCompanies().then(res => { this.companyList = res.data || [] })
-    },
     getList() {
       this.loading = true
       listInstances(this.queryParams).then(res => {
@@ -272,6 +270,4 @@ export default {
 .stat-card { text-align: center; padding: 10px 0; }
 .stat-value { font-size: 24px; font-weight: bold; color: #409EFF; }
 .stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
-</style>bel { font-size: 12px; color: #909399; margin-top: 4px; }
-</style>bel { font-size: 12px; color: #909399; margin-top: 4px; }
 </style>

+ 1 - 0
src/views/lobster/optimization/index.vue

@@ -27,6 +27,7 @@
     </el-row>
     <el-table border v-loading="loading" :data="list" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="租户" align="center" prop="tenant_name" width="140" />
       <el-table-column label="优化ID" align="center" prop="optimizationId" width="80" />
       <el-table-column label="优化原因" align="center" prop="optimizationReason" show-overflow-tooltip />
       <el-table-column label="原始话术" align="center" prop="originalContent" show-overflow-tooltip />

+ 1 - 0
src/views/lobster/prompt/index.vue

@@ -30,6 +30,7 @@
 
     <el-table border v-loading="loading" :data="list">
       <el-table-column label="ID" align="center" prop="id" width="60" />
+      <el-table-column label="租户" align="center" prop="tenant_name" width="140" />
       <el-table-column label="提示词名称" align="center" prop="prompt_name" />
       <el-table-column label="提示词标识" align="center" prop="prompt_key" />
       <el-table-column label="分类" align="center" prop="prompt_category" width="100" />

+ 267 - 27
src/views/lobster/sales-corpus/index.vue

@@ -25,7 +25,7 @@
       </el-form-item>
       <el-form-item label="状态" prop="status">
         <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
-          <el-option label="已分析" value="analyzed" /><el-option label="待分析" value="pending" /><el-option label="已学习" value="learned" />
+          <el-option label="已分析" value="analyzed" /><el-option label="待分析" value="raw" /><el-option label="已学习" value="applied" />
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -34,63 +34,303 @@
       </el-form-item>
     </el-form>
     <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5"><el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddDialog">录入对话</el-button></el-col>
+      <el-col :span="1.5"><el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleImportDialog">批量导入</el-button></el-col>
       <el-col :span="1.5"><el-button type="success" plain icon="el-icon-data-analysis" size="mini" @click="handleAnalyze">AI分析</el-button></el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
     <el-table border v-loading="loading" :data="list">
       <el-table-column label="ID" align="center" prop="id" width="60" />
-      <el-table-column label="销冠姓名" align="center" prop="salespersonName" width="100" />
-      <el-table-column label="客户问题" align="center" prop="customerQuestion" show-overflow-tooltip />
-      <el-table-column label="销冠回答" align="center" prop="salesAnswer" show-overflow-tooltip />
+      <el-table-column label="租户" align="center" prop="tenant_name" width="140" />
+      <el-table-column label="销冠姓名" align="center" prop="salesperson_name" width="100" />
+      <el-table-column label="客户问题" align="center" prop="customer_question" show-overflow-tooltip />
+      <el-table-column label="销冠回答" align="center" prop="sales_answer" show-overflow-tooltip />
       <el-table-column label="场景" align="center" prop="scenario" width="100" />
       <el-table-column label="状态" align="center" prop="status" width="80">
         <template slot-scope="scope">
-          <el-tag :type="scope.row.status==='learned'?'success':scope.row.status==='analyzed'?'warning':'info'" size="small">{{ scope.row.status }}</el-tag>
+          <el-tag :type="scope.row.status==='applied'?'success':scope.row.status==='analyzed'?'warning':'info'" size="small">
+            {{ scope.row.status === 'raw' ? '待分析' : scope.row.status === 'analyzed' ? '已分析' : scope.row.status === 'applied' ? '已学习' : scope.row.status }}
+          </el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+      <el-table-column label="创建时间" align="center" prop="create_time" width="160" />
     </el-table>
     <pagination v-show="total>0" :total="total" :page.sync="queryParams.page" :limit.sync="queryParams.size" @pagination="getList" />
-    <!-- 录入对话弹窗 -->
-    <el-dialog title="录入销冠对话" :visible.sync="addVisible" width="600px" append-to-body>
-      <el-form ref="addForm" :model="addForm" :rules="addRules" label-width="100px">
-        <el-form-item label="销冠姓名" prop="salespersonName"><el-input v-model="addForm.salespersonName" placeholder="请输入销冠姓名" /></el-form-item>
-        <el-form-item label="场景" prop="scenario">
-          <el-select v-model="addForm.scenario" placeholder="请选择场景"><el-option v-for="s in scenarios" :key="s.code" :label="s.name" :value="s.code" /></el-select>
+
+    <!-- ========== 批量导入弹窗 ========== -->
+    <el-dialog title="批量导入销冠对话" :visible.sync="importVisible" width="800px" append-to-body :close-on-click-modal="false">
+      <el-tabs v-model="importTab" @tab-click="resetPreview">
+        <!-- Tab1: Excel导入 -->
+        <el-tab-pane label="Excel导入" name="excel">
+          <el-alert type="info" :closable="false" show-icon style="margin-bottom:15px">
+            <template slot="title">Excel格式:第1列=客户问题,第2列=销冠回答,一行一组对话</template>
+          </el-alert>
+          <el-upload
+            ref="excelUpload"
+            action=""
+            :auto-upload="false"
+            :limit="1"
+            :on-change="handleExcelChange"
+            :on-remove="resetPreview"
+            accept=".xlsx,.xls"
+            drag
+          >
+            <i class="el-icon-upload"></i>
+            <div class="el-upload__text">将Excel文件拖到此处,或<em>点击上传</em></div>
+            <div slot="tip" class="el-upload__tip">仅支持 .xlsx / .xls 文件,第1列客户问题,第2列销冠回答</div>
+          </el-upload>
+        </el-tab-pane>
+
+        <!-- Tab2: 文本输入 -->
+        <el-tab-pane label="文本输入" name="text">
+          <el-alert type="info" :closable="false" show-icon style="margin-bottom:15px">
+            <template slot="title">格式:a:客户说了什么(换行)b:销冠说了什么,每组对话之间空一行</template>
+          </el-alert>
+          <el-input
+            v-model="textContent"
+            type="textarea"
+            :rows="12"
+            placeholder="a: 你们这个产品多少钱?&#10;b: 咱们这个产品根据配置不同价格也不一样,您主要想用在哪个场景呢?&#10;&#10;a: 我主要是想用在客服场景&#10;b: 客服场景的话我推荐咱们的专业版,目前有活动价特别划算,我给您详细介绍一下?"
+          />
+          <el-button type="primary" size="small" style="margin-top:10px" @click="parseTextContent">解析文本</el-button>
+        </el-tab-pane>
+      </el-tabs>
+
+      <!-- 公共表单字段 -->
+      <el-form :model="importForm" label-width="100px" style="margin-top:15px">
+        <el-form-item label="销冠姓名">
+          <el-input v-model="importForm.salespersonName" placeholder="请输入销冠姓名(可选)" style="width:300px" />
+        </el-form-item>
+        <el-form-item label="场景">
+          <el-select v-model="importForm.scenario" placeholder="请选择场景(可选)" clearable style="width:300px">
+            <el-option v-for="s in scenarios" :key="s.code" :label="s.name" :value="s.code" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="选择租户" v-if="importForm.companyId === undefined || importForm.companyId !== null">
+          <el-select v-model="importForm.companyId" placeholder="请选择租户" filterable style="width:300px">
+            <el-option v-for="c in companyList" :key="c.companyId" :label="c.companyName" :value="c.companyId" />
+          </el-select>
         </el-form-item>
-        <el-form-item label="客户问题" prop="customerQuestion"><el-input v-model="addForm.customerQuestion" type="textarea" :rows="3" placeholder="请输入客户的问题" /></el-form-item>
-        <el-form-item label="销冠回答" prop="salesAnswer"><el-input v-model="addForm.salesAnswer" type="textarea" :rows="3" placeholder="请输入销冠的回答" /></el-form-item>
       </el-form>
-      <div slot="footer"><el-button @click="addVisible=false">取消</el-button><el-button type="primary" @click="submitAdd">录入</el-button></div>
+
+      <!-- 预览表格 -->
+      <div v-if="previewList.length > 0" style="margin-top:15px">
+        <el-divider>预览数据(共 {{ previewList.length }} 条)</el-divider>
+        <el-table :data="previewList" border max-height="300" size="mini">
+          <el-table-column label="#" type="index" width="50" />
+          <el-table-column label="客户问题" prop="customer" show-overflow-tooltip />
+          <el-table-column label="销冠回答" prop="sales" show-overflow-tooltip />
+        </el-table>
+      </div>
+
+      <div slot="footer">
+        <el-button @click="importVisible = false">取消</el-button>
+        <el-button type="primary" :disabled="previewList.length === 0" :loading="submitLoading" @click="submitImport">
+          确认导入({{ previewList.length }} 条)
+        </el-button>
+      </div>
     </el-dialog>
   </div>
 </template>
 <script>
-import { listSalesCorpus, addCorpusDialog, analyzeCorpus, getCorpusScenarios } from '@/api/workflow/lobster'
+import { listSalesCorpus, batchImportCorpus, analyzeCorpus, getCorpusScenarios } from '@/api/workflow/lobster'
 import { listAllCompanies } from '@/api/admin/sysCompany'
+import * as XLSX from 'xlsx'
+
 export default {
   name: 'SalesCorpus',
   data() {
-    return { loading: false, showSearch: true, list: [], total: 0, companyList: [], scenarios: [], addVisible: false,
-      addForm: { salespersonName: '', scenario: '', customerQuestion: '', salesAnswer: '' },
-      addRules: { customerQuestion: [{ required: true, message: '请输入客户问题', trigger: 'blur' }], salesAnswer: [{ required: true, message: '请输入销冠回答', trigger: 'blur' }] },
-      queryParams: { page: 1, size: 10, scenario: null, status: null, companyId: null } }
+    return {
+      loading: false,
+      showSearch: true,
+      list: [],
+      total: 0,
+      companyList: [],
+      scenarios: [],
+      queryParams: { page: 1, size: 10, scenario: null, status: null, companyId: null },
+      // 导入相关
+      importVisible: false,
+      importTab: 'excel',
+      textContent: '',
+      previewList: [],
+      submitLoading: false,
+      importForm: {
+        salespersonName: '',
+        scenario: '',
+        companyId: null
+      }
+    }
+  },
+  created() {
+    this.getCompanyList()
+    this.getList()
+    this.getScenarios()
   },
-  created() { this.getCompanyList(); this.getList(); this.getScenarios() },
   methods: {
     getCompanyList() {
       listAllCompanies({ pageSize: 9999 }).then(response => {
         this.companyList = response.rows || []
       })
     },
-    getList() { this.loading = true; listSalesCorpus(this.$withTenant(this.queryParams)).then(res => { let d = res.data || {}; this.list = d.list || []; this.total = d.total || 0; this.loading = false }).catch(() => { this.loading = false }) },
-    getScenarios() { getCorpusScenarios().then(res => { this.scenarios = res.data || [] }) },
+    getList() {
+      this.loading = true
+      listSalesCorpus(this.$withTenant(this.queryParams)).then(res => {
+        this.list = res.rows || []
+        this.total = res.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    getScenarios() {
+      getCorpusScenarios().then(res => { this.scenarios = res.data || [] })
+    },
     handleQuery() { this.queryParams.page = 1; this.getList() },
     resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
-    handleAddDialog() { this.addVisible = true },
-    submitAdd() { this.$refs.addForm.validate(v => { if (!v) return; addCorpusDialog(this.$withTenant(this.addForm)).then(() => { this.$message.success('录入成功'); this.addVisible = false; this.getList() }) }) },
-    handleAnalyze() { this.$confirm('确定触发AI语料分析吗?分析可能需要几分钟。', '提示', { type: 'warning' }).then(() => { analyzeCorpus().then(res => { let d = res.data || {}; this.$message.success('分析完成: 总录入' + d.totalEntries + '条, 评分' + d.overallScore) }) }) }
+
+    // 打开导入弹窗
+    handleImportDialog() {
+      this.importVisible = true
+      this.previewList = []
+      this.textContent = ''
+      this.importForm = { salespersonName: '', scenario: '', companyId: null }
+      this.$nextTick(() => {
+        if (this.$refs.excelUpload) this.$refs.excelUpload.clearFiles()
+      })
+    },
+
+    // Excel文件变化时解析
+    handleExcelChange(file) {
+      const reader = new FileReader()
+      reader.onload = (e) => {
+        try {
+          const data = new Uint8Array(e.target.result)
+          const workbook = XLSX.read(data, { type: 'array' })
+          const sheet = workbook.Sheets[workbook.SheetNames[0]]
+          const json = XLSX.utils.sheet_to_json(sheet, { header: 1 })
+          this.previewList = []
+          for (let i = 0; i < json.length; i++) {
+            const row = json[i]
+            if (!row || row.length < 2) continue
+            const customer = String(row[0] || '').trim()
+            const sales = String(row[1] || '').trim()
+            // 跳过表头行(如果第一行看起来像标题)
+            if (i === 0 && (customer === '客户问题' || customer === '客户' || customer === 'customer' || customer === 'Customer')) continue
+            if (customer && sales) {
+              this.previewList.push({ customer, sales })
+            }
+          }
+          if (this.previewList.length === 0) {
+            this.$message.warning('未解析到有效数据,请检查Excel格式(第1列客户问题,第2列销冠回答)')
+          }
+        } catch (err) {
+          this.$message.error('Excel解析失败: ' + err.message)
+        }
+      }
+      reader.readAsArrayBuffer(file.raw)
+    },
+
+    // 解析文本内容 (a:/b: 格式)
+    parseTextContent() {
+      if (!this.textContent.trim()) {
+        this.$message.warning('请先输入对话内容')
+        return
+      }
+      const lines = this.textContent.split('\n')
+      const pairs = []
+      let currentCustomer = ''
+      let currentSales = ''
+      let hasA = false
+      let hasB = false
+
+      for (let i = 0; i < lines.length; i++) {
+        const line = lines[i].trim()
+        if (!line) {
+          // 空行:如果已经有一组完整的a/b,保存
+          if (hasA && hasB) {
+            pairs.push({ customer: currentCustomer.trim(), sales: currentSales.trim() })
+          }
+          currentCustomer = ''
+          currentSales = ''
+          hasA = false
+          hasB = false
+          continue
+        }
+
+        if (line.startsWith('a:') || line.startsWith('A:')) {
+          // 如果之前有未完成的a/b对,先保存
+          if (hasA && hasB) {
+            pairs.push({ customer: currentCustomer.trim(), sales: currentSales.trim() })
+            currentCustomer = ''
+            currentSales = ''
+            hasA = false
+            hasB = false
+          }
+          currentCustomer = line.substring(2).trim()
+          hasA = true
+        } else if (line.startsWith('b:') || line.startsWith('B:')) {
+          currentSales = line.substring(2).trim()
+          hasB = true
+        } else if (hasA && !hasB) {
+          // 客户问题续行
+          currentCustomer += ' ' + line
+        } else if (hasB) {
+          // 销冠回答续行
+          currentSales += ' ' + line
+        }
+      }
+      // 处理最后一组
+      if (hasA && hasB) {
+        pairs.push({ customer: currentCustomer.trim(), sales: currentSales.trim() })
+      }
+
+      if (pairs.length === 0) {
+        this.$message.warning('未解析到有效对话,请检查格式:a:客户说的 b:销冠说的')
+        return
+      }
+
+      this.previewList = pairs
+      this.$message.success('解析成功,共 ' + pairs.length + ' 条对话')
+    },
+
+    // 重置预览
+    resetPreview() {
+      this.previewList = []
+    },
+
+    // 提交导入
+    submitImport() {
+      if (this.previewList.length === 0) {
+        this.$message.warning('没有可导入的数据')
+        return
+      }
+      this.submitLoading = true
+      const data = {
+        salespersonName: this.importForm.salespersonName || '销冠',
+        scenario: this.importForm.scenario || '通用',
+        companyId: this.importForm.companyId,
+        dialogs: this.previewList.map(item => ({
+          customer: item.customer,
+          sales: item.sales
+        }))
+      }
+      batchImportCorpus(data).then(res => {
+        this.$message.success('导入成功!共 ' + (res.data && res.data.count ? res.data.count : this.previewList.length) + ' 条')
+        this.importVisible = false
+        this.previewList = []
+        this.getList()
+      }).catch(err => {
+        this.$message.error('导入失败: ' + (err.msg || err.message || '未知错误'))
+      }).finally(() => {
+        this.submitLoading = false
+      })
+    },
+
+    handleAnalyze() {
+      this.$confirm('确定触发AI语料分析吗?分析可能需要几分钟。', '提示', { type: 'warning' }).then(() => {
+        analyzeCorpus().then(res => {
+          let d = res.data || {}
+          this.$message.success('分析完成: 总录入' + d.totalEntries + '条, 评分' + d.overallScore)
+        })
+      })
+    }
   }
 }
 </script>

+ 1 - 1
src/views/lobster/workflow-generate/index.vue

@@ -55,7 +55,7 @@
     <!-- ===== 实例列表 ===== -->
     <el-table border v-loading="loading" :data="instanceList">
       <el-table-column label="实例ID" prop="instanceId" width="120" align="center" />
-      <el-table-column label="租户ID" prop="companyId" width="80" align="center" />
+      <el-table-column label="租户" align="center" prop="tenant_name" width="140" />
       <el-table-column label="工作流名称" prop="workflowName" show-overflow-tooltip />
       <el-table-column label="当前节点" prop="currentNode" show-overflow-tooltip />
       <el-table-column label="状态" align="center" width="100">

+ 1 - 1
src/views/system/keyword/index.vue

@@ -205,7 +205,7 @@ export default {
   created() {
     this.getList();
     // this.getTypeOptions();
-    this.getCompanyOptions();
+    // this.getCompanyOptions(); // 公司选择UI已注释,无需加载公司列表;fs-company(8006)未启动时会导致500
   },
   methods: {
     /** 查询关键字列表 */

+ 4 - 2
src/views/system/menu/index.vue

@@ -60,7 +60,8 @@
       <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
       <el-table-column prop="icon" label="图标" align="center" width="100">
         <template slot-scope="scope">
-          <svg-icon :icon-class="scope.row.icon" />
+          <i v-if="scope.row.icon && scope.row.icon.startsWith('el-icon-')" :class="scope.row.icon" />
+          <svg-icon v-else :icon-class="scope.row.icon" />
         </template>
       </el-table-column>
       <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
@@ -136,8 +137,9 @@
               >
                 <IconSelect ref="iconSelect" @selected="selected" />
                 <el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
+                  <i v-if="form.icon && form.icon.startsWith('el-icon-')" slot="prefix" :class="form.icon" class="el-input__icon" style="height: 32px;width: 16px;" />
                   <svg-icon
-                    v-if="form.icon"
+                    v-else-if="form.icon"
                     slot="prefix"
                     :icon-class="form.icon"
                     class="el-input__icon"