lmx 3 dni temu
rodzic
commit
79644a10ac

+ 120 - 6
src/api/company/companyVoiceApi.js

@@ -1,5 +1,50 @@
 import request from '@/utils/request'
 
+// ========== 新版外呼接口管理 /admin/companyVoice ==========
+
+/** 分页查询外呼接口列表(新版) */
+export function listCompanyVoiceApiV2(query) {
+  return request({
+    url: '/admin/companyVoice/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/** 获取外呼接口详情(新版) */
+export function getCompanyVoiceApiV2(apiId) {
+  return request({
+    url: '/admin/companyVoice/' + apiId,
+    method: 'get'
+  })
+}
+
+/** 新增外呼接口(新版) */
+export function addCompanyVoiceApiV2(data) {
+  return request({
+    url: '/admin/companyVoice',
+    method: 'post',
+    data: data
+  })
+}
+
+/** 修改外呼接口(新版) */
+export function updateCompanyVoiceApiV2(data) {
+  return request({
+    url: '/admin/companyVoice',
+    method: 'put',
+    data: data
+  })
+}
+
+/** 删除外呼接口(新版,逻辑删除) */
+export function delCompanyVoiceApiV2(apiId) {
+  return request({
+    url: '/admin/companyVoice/' + apiId,
+    method: 'delete'
+  })
+}
+
 // 查询呼叫接口列表(admin专用,走 /admin/ 前缀,路由到 fs-admin 8004)
 export function listCompanyVoiceApi(query) {
   return request({
@@ -70,28 +115,33 @@ export function getAssignedTenants(apiId) {
 }
 
 // 查询租户已分配的接口列表
-export function getTenantApis(companyId) {
+export function getTenantApis(tenantId) {
   return request({
-    url: '/admin/voice-api/apis/' + companyId,
+    url: '/admin/voice-api/apis/' + tenantId,
     method: 'get'
   })
 }
 
 // 分配接口给租户(批量)
-export function assignTenants(apiId, companyIds) {
+export function assignTenants(apiId, tenantIds, extra = {}) {
   return request({
     url: '/admin/voice-api/assignTenants',
     method: 'post',
-    data: { apiId, companyIds }
+    data: {
+      apiId,
+      tenantIds,
+      apiName: extra.apiName,
+      tenants: extra.tenants
+    }
   })
 }
 
 // 取消分配
-export function unassignTenant(apiId, companyId) {
+export function unassignTenant(apiId, tenantId) {
   return request({
     url: '/admin/voice-api/unassignTenant',
     method: 'delete',
-    params: { apiId, companyId }
+    params: { apiId, tenantId }
   })
 }
 
@@ -103,6 +153,70 @@ export function getTenantCount(apiId) {
   })
 }
 
+// ========== 新版外呼接口-租户分配 /admin/companyVoiceApiTenant ==========
+
+/** 分页查询租户-接口绑定列表(新版) */
+export function listVoiceApiTenantV2(query) {
+  return request({
+    url: '/admin/companyVoiceApiTenant/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/** 外呼接口下拉列表(新版,租户绑定页用) */
+export function getVoiceApiListV2() {
+  return request({
+    url: '/admin/companyVoiceApiTenant/apiList',
+    method: 'get'
+  })
+}
+
+/** 分配接口给租户(新版) */
+export function assignTenantsV2(apiId, tenantIds) {
+  return request({
+    url: '/admin/companyVoiceApiTenant/assign',
+    method: 'post',
+    data: { apiId, tenantIds }
+  })
+}
+
+/** 取消分配(新版) */
+export function unassignTenantV2(apiId, tenantId) {
+  return request({
+    url: '/admin/companyVoiceApiTenant/unassign',
+    method: 'delete',
+    params: { apiId, tenantId }
+  })
+}
+
+/** 更新租户定价(新版) */
+export function updateTenantPricingV2(data) {
+  return request({
+    url: '/admin/companyVoiceApiTenant',
+    method: 'put',
+    data: data
+  })
+}
+
+/** 批量更新租户定价(新版) */
+export function batchUpdateTenantPricingV2(data) {
+  return request({
+    url: '/admin/companyVoiceApiTenant/batchPricing',
+    method: 'put',
+    data: data
+  })
+}
+
+/** 批量更新租户状态(新版) */
+export function batchUpdateTenantStatusV2(data) {
+  return request({
+    url: '/admin/companyVoiceApiTenant/batchStatus',
+    method: 'put',
+    data: data
+  })
+}
+
 // ========== 租户定价管理 ==========
 
 // 查询租户-接口定价列表(分页)

+ 205 - 68
src/views/admin/voiceApi/index.vue

@@ -100,18 +100,18 @@
         </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-input v-model="apiForm.account" placeholder="请输入帐号" />
           </el-form-item>
           <el-form-item label="密码">
-            <el-input v-model="apiForm.apiJsonObj.password" placeholder="请输入密码" />
+            <el-input v-model="apiForm.password" placeholder="请输入密码" />
           </el-form-item>
           <el-form-item label="接口地址">
-            <el-input v-model="apiForm.apiJsonObj.url" placeholder="请输入接口地址" />
+            <el-input v-model="apiForm.apiUrl" placeholder="请输入接口地址" />
           </el-form-item>
         </template>
         <template v-if="apiForm.apiType === '2'">
           <el-form-item label="话术跳转地址">
-            <el-input v-model="apiForm.apiJsonObj.dialogUrl" placeholder="请输入话术跳转地址" />
+            <el-input v-model="apiForm.dialogUrl" placeholder="请输入话术跳转地址" />
           </el-form-item>
         </template>
         <el-form-item label="服务商" prop="provider">
@@ -140,42 +140,92 @@
     </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>
+    <el-dialog title="分配租户" :visible.sync="assignOpen" width="700px" append-to-body custom-class="assign-tenant-dialog">
+      <div class="assign-tenant-wrap">
+        <div class="assign-tenant-header">
+          <span>接口:<strong>{{ assignApi.apiName }}</strong>(ID: {{ assignApi.apiId }})</span>
+          <span class="assign-tenant-count">已分配 {{ assignedTenants.length }} 个租户</span>
+        </div>
+        <!-- 已分配租户列表(限高滚动,避免撑开弹窗) -->
+        <div class="assign-tenant-table-wrap">
+          <el-table
+            :data="assignedTenants"
+            border
+            size="small"
+            style="width:100%"
+            max-height="280"
+            v-loading="assignLoading"
+            empty-text="暂无已分配租户"
+          >
+            <el-table-column label="租户ID" align="center" prop="tenantId" width="80" />
+            <el-table-column label="租户编码" align="center" prop="tenantCode" min-width="100" show-overflow-tooltip />
+            <el-table-column label="租户名称" align="center" prop="tenantName" min-width="120" show-overflow-tooltip />
+            <el-table-column label="接口名称" align="center" prop="apiName" min-width="120" show-overflow-tooltip />
+            <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"
+                  :loading="unassigningTenantId === scope.row.tenantId"
+                  :disabled="!!unassigningTenantId && unassigningTenantId !== scope.row.tenantId"
+                  @click="handleUnassign(scope.row)"
+                >取消分配</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <!-- 添加租户(固定在底部,下拉挂载到 body 避免被裁剪) -->
+        <div class="assign-tenant-add">
+          <el-divider content-position="left">添加租户</el-divider>
+          <el-select
+            v-model="selectedTenantIds"
+            multiple
+            filterable
+            remote
+            reserve-keyword
+            collapse-tags
+            popper-append-to-body
+            popper-class="assign-tenant-select-popper"
+            placeholder="输入租户名称搜索"
+            :remote-method="searchTenants"
+            :loading="tenantSearchLoading"
+            :disabled="!!unassigningTenantId"
+            style="width:100%"
+            size="small"
+          >
+            <el-option
+              v-for="item in tenantOptions"
+              :key="item.tenantId"
+              :label="formatTenantOption(item)"
+              :value="item.tenantId"
+            />
+          </el-select>
+        </div>
       </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 slot="footer" class="dialog-footer">
+        <el-button @click="assignOpen = false">取 消</el-button>
+        <el-button
+          type="primary"
+          @click="submitAssign"
+          :loading="assignSubmitting"
+          :disabled="selectedTenantIds.length === 0 || !!unassigningTenantId"
+        >确认分配</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 :data="viewTenants" border size="small" style="width:100%" max-height="420" v-loading="viewTenantsLoading">
+        <el-table-column label="租户ID" align="center" prop="tenantId" width="80" />
+        <el-table-column label="租户编码" align="center" prop="tenantCode" min-width="100" show-overflow-tooltip />
+        <el-table-column label="租户名称" align="center" prop="tenantName" min-width="120" show-overflow-tooltip />
+        <el-table-column label="接口名称" align="center" prop="apiName" min-width="120" show-overflow-tooltip />
         <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>
@@ -188,8 +238,9 @@
 
 <script>
 import {
-  listCompanyVoiceApi, getCompanyVoiceApi,
-  addCompanyVoiceApi, updateCompanyVoiceApi, delCompanyVoiceApi,
+  listCompanyVoiceApiV2, getCompanyVoiceApiV2,
+  addCompanyVoiceApiV2, updateCompanyVoiceApiV2,
+  delCompanyVoiceApiV2,
   getAssignedTenants, assignTenants, unassignTenant, getTenantCount
 } from '@/api/company/companyVoiceApi'
 import { listAllCompanies } from '@/api/admin/sysCompany'
@@ -220,7 +271,10 @@ export default {
         apiName: null,
         apiType: '1',
         provider: 'platform',
-        apiJsonObj: {},
+        account: null,
+        password: null,
+        apiUrl: null,
+        dialogUrl: null,
         costPrice: null,
         status: '1',
         remark: null
@@ -235,10 +289,11 @@ export default {
       assignApi: {},
       assignLoading: false,
       assignSubmitting: false,
+      unassigningTenantId: null,
       assignedTenants: [],
-      selectedCompanyIds: [],
-      companyOptions: [],
-      companySearchLoading: false,
+      selectedTenantIds: [],
+      tenantOptions: [],
+      tenantSearchLoading: false,
       // 查看租户列表
       tenantListOpen: false,
       viewTenants: [],
@@ -251,7 +306,7 @@ export default {
   methods: {
     getList() {
       this.loading = true
-      listCompanyVoiceApi(this.queryParams).then(response => {
+      listCompanyVoiceApiV2(this.queryParams).then(response => {
         this.dataList = response.rows || []
         this.total = response.total || 0
         this.loading = false
@@ -276,6 +331,22 @@ export default {
     getApiTypeLabel(type) {
       return this.apiTypeMap[String(type)] || '未知'
     },
+    formatTenantOption(item) {
+      if (item.tenantCode) {
+        return `${item.tenantName || '-'} (${item.tenantCode})`
+      }
+      return item.tenantName || String(item.tenantId)
+    },
+    buildAssignTenants() {
+      return this.selectedTenantIds.map(tenantId => {
+        const item = this.tenantOptions.find(opt => opt.tenantId === tenantId) || {}
+        return {
+          tenantId,
+          tenantCode: item.tenantCode || null,
+          tenantName: item.tenantName || null
+        }
+      })
+    },
     // 新增
     handleAdd() {
       this.resetApiForm()
@@ -285,14 +356,17 @@ export default {
     // 编辑
     handleUpdate(row) {
       this.resetApiForm()
-      getCompanyVoiceApi(row.apiId).then(response => {
+      getCompanyVoiceApiV2(row.apiId).then(response => {
         const data = response.data
         this.apiForm = {
           apiId: data.apiId,
           apiName: data.apiName,
           apiType: String(data.apiType),
           provider: data.provider || 'platform',
-          apiJsonObj: data.apiJson ? JSON.parse(data.apiJson) : {},
+          account: data.account || null,
+          password: data.password || null,
+          apiUrl: data.apiUrl || null,
+          dialogUrl: data.dialogUrl || null,
           costPrice: data.costPrice,
           status: String(data.status),
           remark: data.remark
@@ -307,7 +381,10 @@ export default {
         apiName: null,
         apiType: '1',
         provider: 'platform',
-        apiJsonObj: {},
+        account: null,
+        password: null,
+        apiUrl: null,
+        dialogUrl: null,
         costPrice: null,
         status: '1',
         remark: null
@@ -321,16 +398,16 @@ export default {
         if (!valid) return
         this.formSubmitting = true
         const data = { ...this.apiForm }
-        data.apiJson = JSON.stringify(data.apiJsonObj)
-        delete data.apiJsonObj
+        data.apiType = data.apiType != null ? parseInt(data.apiType, 10) : null
+        data.status = data.status != null ? parseInt(data.status, 10) : null
         if (data.apiId) {
-          updateCompanyVoiceApi(data).then(() => {
+          updateCompanyVoiceApiV2(data).then(() => {
             this.$message.success('修改成功')
             this.formOpen = false
             this.getList()
           }).finally(() => { this.formSubmitting = false })
         } else {
-          addCompanyVoiceApi(data).then(() => {
+          addCompanyVoiceApiV2(data).then(() => {
             this.$message.success('新增成功')
             this.formOpen = false
             this.getList()
@@ -345,7 +422,7 @@ export default {
         cancelButtonText: '取消',
         type: 'warning'
       }).then(() => {
-        return delCompanyVoiceApi(row.apiId)
+        return delCompanyVoiceApiV2(row.apiId)
       }).then(() => {
         this.$message.success('删除成功')
         this.getList()
@@ -355,32 +432,37 @@ export default {
     handleAssignTenant(row) {
       this.assignApi = row
       this.assignOpen = true
-      this.selectedCompanyIds = []
-      this.companyOptions = []
+      this.selectedTenantIds = []
+      this.tenantOptions = []
+      this.unassigningTenantId = null
       this.loadAssignedTenants()
     },
     loadAssignedTenants() {
       this.assignLoading = true
-      getAssignedTenants(this.assignApi.apiId).then(response => {
+      return getAssignedTenants(this.assignApi.apiId).then(response => {
         this.assignedTenants = response.data || []
       }).finally(() => { this.assignLoading = false })
     },
-    searchCompanies(query) {
+    searchTenants(query) {
       if (query.length < 1) return
-      this.companySearchLoading = true
+      this.tenantSearchLoading = 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
+        this.tenantOptions = (response.rows || []).map(c => ({
+          tenantId: c.id || c.companyId,
+          tenantName: c.tenantName || c.companyName,
+          tenantCode: c.tenantCode || c.companyCode || null
         }))
-      }).finally(() => { this.companySearchLoading = false })
+      }).finally(() => { this.tenantSearchLoading = false })
     },
     submitAssign() {
-      if (this.selectedCompanyIds.length === 0) return
+      if (this.selectedTenantIds.length === 0) return
       this.assignSubmitting = true
-      assignTenants(this.assignApi.apiId, this.selectedCompanyIds).then(() => {
+      assignTenants(this.assignApi.apiId, this.selectedTenantIds, {
+        apiName: this.assignApi.apiName,
+        tenants: this.buildAssignTenants()
+      }).then(() => {
         this.$message.success('分配成功')
-        this.selectedCompanyIds = []
+        this.selectedTenantIds = []
         this.loadAssignedTenants()
         this.getList()
       }).finally(() => { this.assignSubmitting = false })
@@ -389,13 +471,31 @@ export default {
       this.$confirm('是否取消该租户的接口分配?', '警告', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
-        type: 'warning'
-      }).then(() => {
-        return unassignTenant(row.apiId, row.companyId)
-      }).then(() => {
-        this.$message.success('已取消分配')
-        this.loadAssignedTenants()
-        this.getList()
+        type: 'warning',
+        beforeClose: (action, instance, done) => {
+          if (action !== 'confirm') {
+            done()
+            return
+          }
+          instance.confirmButtonLoading = true
+          instance.confirmButtonText = '处理中...'
+          this.unassigningTenantId = row.tenantId
+          this.assignLoading = true
+          unassignTenant(row.apiId, row.tenantId).then(() => {
+            this.$message.success('已取消分配')
+            return this.loadAssignedTenants()
+          }).then(() => {
+            this.getList()
+            done()
+          }).catch(() => {
+            this.assignLoading = false
+            done()
+          }).finally(() => {
+            instance.confirmButtonLoading = false
+            instance.confirmButtonText = '确定'
+            this.unassigningTenantId = null
+          })
+        }
       }).catch(() => {})
     },
     // 查看已分配租户
@@ -414,4 +514,41 @@ export default {
 .mb8 { margin-bottom: 8px; }
 .mb16 { margin-bottom: 16px; }
 .filter-card { padding-bottom: 0; }
+
+.assign-tenant-wrap {
+  display: flex;
+  flex-direction: column;
+  max-height: 65vh;
+}
+.assign-tenant-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+  flex-shrink: 0;
+}
+.assign-tenant-count {
+  color: #909399;
+  font-size: 13px;
+}
+.assign-tenant-table-wrap {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+.assign-tenant-add {
+  flex-shrink: 0;
+  margin-top: 8px;
+  padding-top: 4px;
+  border-top: 1px solid #ebeef5;
+}
+</style>
+
+<style>
+.assign-tenant-select-popper {
+  z-index: 3000 !important;
+}
+.assign-tenant-dialog .el-dialog__body {
+  padding-bottom: 10px;
+}
 </style>

+ 261 - 77
src/views/admin/voiceApiTenant/index.vue

@@ -2,10 +2,10 @@
   <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="companyId">
-          <el-select v-model="queryParams.companyId" placeholder="选择租户" clearable filterable remote
-            reserve-keyword :remote-method="searchCompanies" :loading="companySearchLoading" style="width:200px">
-            <el-option v-for="c in companyOptions" :key="c.companyId" :label="c.companyName" :value="c.companyId" />
+        <el-form-item label="租户" prop="tenantId">
+          <el-select v-model="queryParams.tenantId" placeholder="选择租户" clearable filterable remote
+            reserve-keyword :remote-method="searchTenants" :loading="tenantSearchLoading" style="width:200px">
+            <el-option v-for="c in tenantOptions" :key="c.tenantId" :label="c.tenantName" :value="c.tenantId" />
           </el-select>
         </el-form-item>
         <el-form-item label="接口" prop="apiId">
@@ -30,56 +30,84 @@
       <el-col :span="1.5">
         <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增绑定</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" plain icon="el-icon-edit-outline" size="mini" :disabled="!selectedRows.length" @click="handleBatchPricing">批量定价</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-open" size="mini" :disabled="!selectedRows.length" @click="handleBatchStatus(1)">批量启用</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="info" plain icon="el-icon-turn-off" size="mini" :disabled="!selectedRows.length" @click="handleBatchStatus(0)">批量禁用</el-button>
+      </el-col>
+      <el-col :span="1.5" v-if="selectedRows.length">
+        <span class="selected-tip">已选 {{ selectedRows.length }} 条</span>
+      </el-col>
     </el-row>
 
-    <el-table v-loading="loading" :data="dataList" border size="small" style="width:100%">
-      <el-table-column label="租户" align="center" prop="companyName" min-width="120" show-overflow-tooltip />
-      <el-table-column label="接口名称" align="center" prop="apiName" min-width="120" show-overflow-tooltip />
-      <el-table-column label="服务商" align="center" prop="provider" min-width="80">
+    <el-table
+      v-loading="loading"
+      :data="dataList"
+      border
+      size="small"
+      style="width:100%"
+      class="voice-api-tenant-table"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="45" align="center" />
+      <el-table-column label="租户" align="center" prop="tenantName" min-width="100" show-overflow-tooltip />
+      <el-table-column label="接口名称" align="center" prop="apiName" min-width="100" show-overflow-tooltip />
+      <el-table-column label="服务商" align="center" prop="provider" min-width="76">
         <template slot-scope="scope">
           <el-tag v-if="scope.row.provider === 'card'" type="warning" size="mini">手机卡</el-tag>
           <el-tag v-else type="primary" size="mini">平台</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="接口类型" align="center" prop="apiType" min-width="70">
+      <el-table-column label="类型" align="center" prop="apiType" width="58">
         <template slot-scope="scope">
           <span>{{ apiTypeMap[scope.row.apiType] || scope.row.apiType }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="成本价" align="center" prop="costPrice" min-width="90">
+      <el-table-column label="成本价" align="center" prop="costPrice" min-width="82">
         <template slot-scope="scope">
-          <span>{{ scope.row.costPrice != null ? scope.row.costPrice + ' 元/分钟' : '-' }}</span>
+          <span>{{ scope.row.costPrice != null ? scope.row.costPrice : '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="售价(元/分钟)" align="center" prop="price" min-width="120">
+      <el-table-column label="售价" align="center" prop="salePrice" min-width="82">
         <template slot-scope="scope">
-          <span>{{ scope.row.price != null ? scope.row.price : '未定价' }}</span>
+          <span>{{ scope.row.salePrice != null ? scope.row.salePrice : '未定价' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="优先级" align="center" prop="priority" min-width="70">
+      <el-table-column label="优先级" align="center" prop="priority" width="68">
         <template slot-scope="scope">
           <el-tag :type="scope.row.isPrimary === 1 ? 'success' : 'info'" size="mini">{{ scope.row.priority || '-' }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="主线路" align="center" prop="isPrimary" min-width="70">
+      <el-table-column label="主线路" align="center" prop="isPrimary" width="68">
         <template slot-scope="scope">
           <el-tag v-if="scope.row.isPrimary === 1" type="success" size="mini">是</el-tag>
           <span v-else>-</span>
         </template>
       </el-table-column>
-      <el-table-column label="手动选择" align="center" prop="allowManual" min-width="80">
+      <el-table-column label="手动选择" align="center" prop="selectable" min-width="88">
         <template slot-scope="scope">
-          <el-tag v-if="scope.row.allowManual === 1" type="warning" size="mini">允许</el-tag>
+          <el-tag v-if="isSelectable(scope.row)" type="warning" size="mini">允许</el-tag>
           <span v-else>否</span>
         </template>
       </el-table-column>
-      <el-table-column label="状态" align="center" prop="status" min-width="70">
+      <el-table-column label="状态" align="center" prop="status" width="72" class-name="status-column">
         <template slot-scope="scope">
-          <el-tag v-if="scope.row.status === 1" type="success" size="mini">启用</el-tag>
-          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+          <el-tooltip :content="scope.row.status === 1 ? '启用' : '禁用'" placement="top">
+            <el-switch
+              v-model="scope.row.status"
+              :active-value="1"
+              :inactive-value="0"
+              :disabled="statusUpdatingId === scope.row.id"
+              @change="val => handleStatusChange(scope.row, val)"
+            />
+          </el-tooltip>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
         <template slot-scope="scope">
           <el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditPricing(scope.row)">定价</el-button>
           <el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleUnbind(scope.row)">解除</el-button>
@@ -97,13 +125,13 @@
             <el-option v-for="api in apiOptions" :key="api.apiId" :label="api.apiName" :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-form-item label="选择租户" prop="tenantId">
+          <el-select v-model="addForm.tenantId" placeholder="请选择租户" filterable style="width:100%">
+            <el-option v-for="c in tenantList" :key="c.tenantId" :label="c.tenantName" :value="c.tenantId" />
           </el-select>
         </el-form-item>
-        <el-form-item label="售价(元/分钟)" prop="price">
-          <el-input-number v-model="addForm.price" :precision="4" :step="0.01" :min="0" style="width:100%" />
+        <el-form-item label="售价(元/分钟)" prop="salePrice">
+          <el-input-number v-model="addForm.salePrice" :precision="4" :step="0.01" :min="0" style="width:100%" />
         </el-form-item>
         <el-form-item label="优先级" prop="priority">
           <el-input-number v-model="addForm.priority" :min="1" :max="99" style="width:100%" />
@@ -113,7 +141,7 @@
           <el-switch v-model="addForm.isPrimary" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否" />
         </el-form-item>
         <el-form-item label="允许手动选择">
-          <el-switch v-model="addForm.allowManual" :active-value="1" :inactive-value="0" active-text="允许" inactive-text="禁止" />
+          <el-switch v-model="addForm.selectable" active-value="1" inactive-value="0" active-text="允许" inactive-text="禁止" />
         </el-form-item>
         <el-form-item v-if="selectedApiCost" label="">
           <span style="color:#909399;font-size:12px">该接口成本价: {{ selectedApiCost }} 元/分钟,建议售价 >= 成本价</span>
@@ -125,11 +153,11 @@
       </div>
     </el-dialog>
 
-    <!-- 定价编辑弹窗 -->
+    <!-- 单条定价编辑弹窗 -->
     <el-dialog title="编辑租户定价" :visible.sync="pricingOpen" width="500px" append-to-body>
       <el-form ref="pricingForm" :model="pricingForm" label-width="140px" size="small">
         <el-form-item label="租户">
-          <el-input :value="pricingForm.companyName" disabled />
+          <el-input :value="pricingForm.tenantName" disabled />
         </el-form-item>
         <el-form-item label="接口">
           <el-input :value="pricingForm.apiName" disabled />
@@ -137,8 +165,8 @@
         <el-form-item label="成本价(元/分钟)">
           <el-input :value="pricingForm.costPrice != null ? pricingForm.costPrice : '-'" disabled />
         </el-form-item>
-        <el-form-item label="售价(元/分钟)" prop="price">
-          <el-input-number v-model="pricingForm.price" :precision="4" :min="0" :step="0.01" style="width:100%" />
+        <el-form-item label="售价(元/分钟)" prop="salePrice">
+          <el-input-number v-model="pricingForm.salePrice" :precision="4" :min="0" :step="0.01" style="width:100%" />
         </el-form-item>
         <el-form-item label="优先级" prop="priority">
           <el-input-number v-model="pricingForm.priority" :min="1" :max="99" :step="1" style="width:100%" />
@@ -147,7 +175,7 @@
           <el-switch v-model="pricingForm.isPrimary" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否" />
         </el-form-item>
         <el-form-item label="允许手动选择">
-          <el-switch v-model="pricingForm.allowManual" :active-value="1" :inactive-value="0" active-text="允许" inactive-text="禁止" />
+          <el-switch v-model="pricingForm.selectable" active-value="1" inactive-value="0" active-text="允许" inactive-text="禁止" />
         </el-form-item>
         <el-form-item label="状态">
           <el-switch v-model="pricingForm.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
@@ -158,11 +186,55 @@
         <el-button @click="pricingOpen = false">取 消</el-button>
       </div>
     </el-dialog>
+
+    <!-- 批量定价弹窗 -->
+    <el-dialog title="批量定价" :visible.sync="batchPricingOpen" width="520px" append-to-body>
+      <div class="batch-tip">将对已选 {{ selectedRows.length }} 条记录批量更新,仅填写需要修改的项,留空则保持原值不变。</div>
+      <el-form ref="batchPricingForm" :model="batchPricingForm" label-width="140px" size="small">
+        <el-form-item label="售价(元/分钟)">
+          <el-input-number v-model="batchPricingForm.salePrice" :precision="4" :min="0" :step="0.01" style="width:100%" placeholder="留空不修改" />
+        </el-form-item>
+        <el-form-item label="优先级">
+          <el-input-number v-model="batchPricingForm.priority" :min="1" :max="99" :step="1" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="主线路">
+          <el-checkbox v-model="batchPricingForm.applyIsPrimary">更新主线路</el-checkbox>
+          <el-switch
+            v-model="batchPricingForm.isPrimary"
+            :disabled="!batchPricingForm.applyIsPrimary"
+            :active-value="1"
+            :inactive-value="0"
+            active-text="是"
+            inactive-text="否"
+            style="margin-left:12px"
+          />
+        </el-form-item>
+        <el-form-item label="允许手动选择">
+          <el-checkbox v-model="batchPricingForm.applySelectable">更新手动选择</el-checkbox>
+          <el-switch
+            v-model="batchPricingForm.selectable"
+            :disabled="!batchPricingForm.applySelectable"
+            active-value="1"
+            inactive-value="0"
+            active-text="允许"
+            inactive-text="禁止"
+            style="margin-left:12px"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitBatchPricing" :loading="batchPricingSubmitting">确 定</el-button>
+        <el-button @click="batchPricingOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { listVoiceApiTenant, updateTenantPricing, getVoiceApiList, assignTenants, unassignTenant } from '@/api/company/companyVoiceApi'
+import {
+  listVoiceApiTenantV2, updateTenantPricingV2, getVoiceApiListV2,
+  assignTenantsV2, unassignTenantV2, batchUpdateTenantPricingV2, batchUpdateTenantStatusV2
+} from '@/api/company/companyVoiceApi'
 import { listAllCompanies } from '@/api/admin/sysCompany'
 
 export default {
@@ -172,43 +244,50 @@ export default {
       loading: true,
       total: 0,
       dataList: [],
+      selectedRows: [],
+      statusUpdatingId: null,
       apiTypeMap: { '0': 'SIP', '1': '网关', '2': 'API' },
       queryParams: {
         pageNum: 1,
         pageSize: 10,
-        companyId: null,
+        tenantId: null,
         apiId: null,
         status: null
       },
-      // 租户搜索
-      companyOptions: [],
-      companySearchLoading: false,
-      // 接口列表
+      tenantOptions: [],
+      tenantSearchLoading: false,
       apiOptions: [],
-      // 新增绑定
       addDialogVisible: false,
       addSubmitting: false,
-      addForm: { apiId: undefined, companyId: undefined, price: 0, priority: 1, isPrimary: 0, allowManual: 0 },
+      addForm: { apiId: undefined, tenantId: undefined, salePrice: 0, priority: 1, isPrimary: 0, selectable: '0' },
       addRules: {
         apiId: [{ required: true, message: '请选择接口', trigger: 'change' }],
-        companyId: [{ required: true, message: '请选择租户', trigger: 'change' }],
-        price: [{ required: true, message: '请填写售价', trigger: 'blur' }]
+        tenantId: [{ required: true, message: '请选择租户', trigger: 'change' }],
+        salePrice: [{ required: true, message: '请填写售价', trigger: 'blur' }]
       },
-      // 租户列表(新增绑定用)
-      companyList: [],
-      // 定价编辑
+      tenantList: [],
       pricingOpen: false,
       pricingSubmitting: false,
       pricingForm: {
         id: null,
-        companyName: '',
+        tenantName: '',
         apiName: '',
         costPrice: null,
-        price: null,
+        salePrice: null,
         priority: 1,
         isPrimary: 0,
-        allowManual: 0,
+        selectable: '0',
         status: 1
+      },
+      batchPricingOpen: false,
+      batchPricingSubmitting: false,
+      batchPricingForm: {
+        salePrice: undefined,
+        priority: undefined,
+        applyIsPrimary: false,
+        isPrimary: 0,
+        applySelectable: false,
+        selectable: '0'
       }
     }
   },
@@ -221,12 +300,19 @@ export default {
   created() {
     this.getList()
     this.loadApiOptions()
-    this.loadCompanyList()
+    this.loadTenantList()
   },
   methods: {
+    isSelectable(row) {
+      return row.selectable === '1' || row.selectable === 1 || row.selectable === true
+    },
+    normalizeSelectable(value) {
+      if (value === 1 || value === true || value === '1') return '1'
+      return '0'
+    },
     getList() {
       this.loading = true
-      listVoiceApiTenant(this.queryParams).then(response => {
+      listVoiceApiTenantV2(this.queryParams).then(response => {
         this.dataList = response.rows || []
         this.total = response.total || 0
         this.loading = false
@@ -240,24 +326,31 @@ export default {
       this.resetForm('queryForm')
       this.handleQuery()
     },
+    handleSelectionChange(rows) {
+      this.selectedRows = rows || []
+    },
     loadApiOptions() {
-      getVoiceApiList().then(response => {
+      getVoiceApiListV2().then(response => {
         this.apiOptions = response.rows || response.data || []
       }).catch(() => {})
     },
-    loadCompanyList() {
+    loadTenantList() {
       listAllCompanies({ pageNum: 1, pageSize: 1000 }).then(res => {
-        this.companyList = res.rows || res.data || []
+        const rows = res.rows || res.data || []
+        this.tenantList = rows.map(c => ({
+          tenantId: c.id || c.companyId,
+          tenantName: c.tenantName || c.companyName
+        }))
       })
     },
     onApiChange(apiId) {
       const api = this.apiOptions.find(a => a.apiId === apiId)
       if (api && api.costPrice) {
-        this.addForm.price = api.costPrice
+        this.addForm.salePrice = api.costPrice
       }
     },
     handleAdd() {
-      this.addForm = { apiId: undefined, companyId: undefined, price: 0, priority: 1, isPrimary: 0, allowManual: 0 }
+      this.addForm = { apiId: undefined, tenantId: undefined, salePrice: 0, priority: 1, isPrimary: 0, selectable: '0' }
       this.addDialogVisible = true
       this.$nextTick(() => {
         if (this.$refs.addForm) this.$refs.addForm.clearValidate()
@@ -267,16 +360,15 @@ export default {
       this.$refs.addForm.validate(valid => {
         if (!valid) return
         this.addSubmitting = true
-        assignTenants(this.addForm.apiId, [this.addForm.companyId]).then(() => {
-          // 分配后设置定价
-          if (this.addForm.price || this.addForm.priority > 1 || this.addForm.isPrimary || this.addForm.allowManual) {
-            updateTenantPricing({
+        assignTenantsV2(this.addForm.apiId, [this.addForm.tenantId]).then(() => {
+          if (this.addForm.salePrice || this.addForm.priority > 1 || this.addForm.isPrimary || this.addForm.selectable === '1') {
+            updateTenantPricingV2({
               apiId: this.addForm.apiId,
-              companyId: this.addForm.companyId,
-              price: this.addForm.price,
+              tenantId: this.addForm.tenantId,
+              salePrice: this.addForm.salePrice,
               priority: this.addForm.priority,
               isPrimary: this.addForm.isPrimary,
-              allowManual: this.addForm.allowManual,
+              selectable: this.addForm.selectable,
               status: 1
             }).catch(() => {})
           }
@@ -287,35 +379,35 @@ export default {
       })
     },
     handleUnbind(row) {
-      this.$confirm('确认解除租户"' + row.companyName + '"与接口"' + row.apiName + '"的绑定?', '提示', {
+      this.$confirm('确认解除租户"' + row.tenantName + '"与接口"' + row.apiName + '"的绑定?', '提示', {
         confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
       }).then(() => {
-        unassignTenant(row.apiId, row.companyId).then(() => {
+        unassignTenantV2(row.apiId, row.tenantId).then(() => {
           this.$message.success('解除绑定成功')
           this.getList()
         })
       }).catch(() => {})
     },
-    searchCompanies(query) {
-      if (query.length < 1) { this.companyOptions = []; return }
-      this.companySearchLoading = true
+    searchTenants(query) {
+      if (query.length < 1) { this.tenantOptions = []; return }
+      this.tenantSearchLoading = 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
+        this.tenantOptions = (response.rows || []).map(c => ({
+          tenantId: c.id || c.companyId,
+          tenantName: c.tenantName || c.companyName
         }))
-      }).finally(() => { this.companySearchLoading = false })
+      }).finally(() => { this.tenantSearchLoading = false })
     },
     handleEditPricing(row) {
       this.pricingForm = {
         id: row.id,
-        companyName: row.companyName,
+        tenantName: row.tenantName,
         apiName: row.apiName,
         costPrice: row.costPrice,
-        price: row.price,
+        salePrice: row.salePrice,
         priority: row.priority || 1,
         isPrimary: row.isPrimary || 0,
-        allowManual: row.allowManual || 0,
+        selectable: this.normalizeSelectable(row.selectable),
         status: row.status
       }
       this.pricingOpen = true
@@ -325,24 +417,116 @@ export default {
     },
     submitPricing() {
       this.pricingSubmitting = true
-      updateTenantPricing({
+      updateTenantPricingV2({
         id: this.pricingForm.id,
-        price: this.pricingForm.price,
+        salePrice: this.pricingForm.salePrice,
         priority: this.pricingForm.priority,
         isPrimary: this.pricingForm.isPrimary,
-        allowManual: this.pricingForm.allowManual,
+        selectable: this.pricingForm.selectable,
         status: this.pricingForm.status
       }).then(() => {
         this.$message.success('定价保存成功')
         this.pricingOpen = false
         this.getList()
       }).finally(() => { this.pricingSubmitting = false })
+    },
+    handleBatchPricing() {
+      if (!this.selectedRows.length) {
+        this.$message.warning('请先选择要批量定价的记录')
+        return
+      }
+      this.batchPricingForm = {
+        salePrice: undefined,
+        priority: undefined,
+        applyIsPrimary: false,
+        isPrimary: 0,
+        applySelectable: false,
+        selectable: '0'
+      }
+      this.batchPricingOpen = true
+    },
+    submitBatchPricing() {
+      const data = { ids: this.selectedRows.map(row => row.id) }
+      if (this.batchPricingForm.salePrice != null && this.batchPricingForm.salePrice !== '') {
+        data.salePrice = this.batchPricingForm.salePrice
+      }
+      if (this.batchPricingForm.priority != null && this.batchPricingForm.priority !== '') {
+        data.priority = this.batchPricingForm.priority
+      }
+      if (this.batchPricingForm.applyIsPrimary) {
+        data.isPrimary = this.batchPricingForm.isPrimary
+      }
+      if (this.batchPricingForm.applySelectable) {
+        data.selectable = this.batchPricingForm.selectable
+      }
+      if (data.salePrice == null && data.priority == null && data.isPrimary == null && data.selectable == null) {
+        this.$message.warning('请至少填写或勾选一项要批量更新的配置')
+        return
+      }
+      this.batchPricingSubmitting = true
+      batchUpdateTenantPricingV2(data).then(() => {
+        this.$message.success('批量定价成功')
+        this.batchPricingOpen = false
+        this.getList()
+      }).finally(() => { this.batchPricingSubmitting = false })
+    },
+    handleBatchStatus(status) {
+      if (!this.selectedRows.length) {
+        this.$message.warning('请先选择要操作的记录')
+        return
+      }
+      const actionText = status === 1 ? '启用' : '禁用'
+      this.$confirm('确认批量' + actionText + '已选 ' + this.selectedRows.length + ' 条记录?', '提示', {
+        confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
+      }).then(() => {
+        return batchUpdateTenantStatusV2({
+          ids: this.selectedRows.map(row => row.id),
+          status
+        })
+      }).then(() => {
+        this.$message.success('批量' + actionText + '成功')
+        this.getList()
+      }).catch(() => {})
+    },
+    handleStatusChange(row, status) {
+      const previousStatus = status === 1 ? 0 : 1
+      this.statusUpdatingId = row.id
+      updateTenantPricingV2({ id: row.id, status }).then(() => {
+        this.$message.success(status === 1 ? '已启用' : '已禁用')
+      }).catch(() => {
+        row.status = previousStatus
+      }).finally(() => {
+        this.statusUpdatingId = null
+      })
     }
   }
 }
 </script>
 
 <style scoped>
+.mb8 { margin-bottom: 8px; }
 .mb16 { margin-bottom: 16px; }
 .filter-card { padding-bottom: 0; }
+.selected-tip { color: #909399; font-size: 13px; line-height: 28px; }
+.batch-tip {
+  margin-bottom: 16px;
+  padding: 10px 12px;
+  background: #f4f4f5;
+  border-radius: 4px;
+  color: #606266;
+  font-size: 13px;
+  line-height: 1.5;
+}
+.voice-api-tenant-table {
+  width: 100%;
+}
+.voice-api-tenant-table >>> .status-column .cell {
+  overflow: visible;
+  white-space: nowrap;
+  padding-left: 8px;
+  padding-right: 8px;
+}
+.voice-api-tenant-table >>> .status-column .el-switch {
+  vertical-align: middle;
+}
 </style>