周洋 пре 2 дана
родитељ
комит
f42fe7c551

+ 4 - 0
src/main.js

@@ -35,6 +35,10 @@ Vue.prototype.$runtimeConfig = {}
 
 // 全局方法挂载
 
+// 全局 $modal 插件(若依风格)
+import modal from '@/plugins/modal'
+Vue.use(modal)
+
 import { VueJsonp } from 'vue-jsonp'
 Vue.use(VueJsonp)
 

+ 6 - 11
src/router/index.js

@@ -7,6 +7,10 @@ Vue.use(Router)
 import Layout from '@/layout'
 import LiveConsole from "@/views/live/liveConsole/index.vue";
 
+// 静态 import() 确保龙虾视图被 webpack 编译产出 chunk(防止动态 require 上下文遗漏)
+import '@/views/lobster/workflow-generate/index.vue'
+import '@/views/lobster/sales-corpus/index.vue'
+
 
 /**
  * Note: 路由配置项
@@ -245,17 +249,8 @@ export const constantRoutes = [
   ]
   },
   // ======== 龙虾引擎 (Lobster Workflow Engine) ========
-  // 兜底路由:当 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: '销冠语料学习' } }
-    ]
-  },
+  // 静态 import() 确保龙虾视图被 webpack 编译产出 chunk(防止动态 require 上下文遗漏)
+  // 注意:不在此注册 /admin 路由,避免拦截动态路由的子路由导致 404
 ]
 
 const originalPush = Router.prototype.push

+ 2 - 2
src/views/admin/agentPricing/index.vue

@@ -8,7 +8,7 @@
       <el-form :inline="true" size="small">
         <el-form-item label="选择代理">
           <el-select v-model="selectedProxyId" placeholder="请选择代理" @change="onProxyChange" style="width:300px" filterable>
-            <el-option v-for="a in proxyList" :key="a.proxyId" :label="a.proxyName + ' — 折扣' + a.platformDiscount + '%'" :value="a.proxyId" />
+            <el-option v-for="a in proxyList" :key="a.proxyId" :label="a.proxyName + ' — 折扣' + (a.platformDiscount || a.profitShareRatio || 0) + '%'" :value="a.proxyId" />
           </el-select>
         </el-form-item>
         <el-form-item v-if="selectedProxyId && proxyDiscount">
@@ -73,7 +73,7 @@ export default {
     },
     onProxyChange() {
       const proxy = this.proxyList.find(a => a.proxyId === this.selectedProxyId)
-      this.proxyDiscount = proxy ? proxy.platformDiscount : null
+      this.proxyDiscount = proxy ? (proxy.platformDiscount || proxy.profitShareRatio) : null
       this.fetchPricing()
     },
     fetchPricing() {

+ 4 - 2
src/views/admin/agentReport/index.vue

@@ -30,7 +30,9 @@
         <el-table-column type="index" label="#" width="60" />
         <el-table-column prop="proxyName" label="代理名称" min-width="120" />
         <el-table-column prop="companyName" label="公司" min-width="140" />
-        <el-table-column prop="platformDiscount" label="平台折扣(%)" width="110" align="center" />
+        <el-table-column label="平台折扣(%)" width="110" align="center">
+          <template slot-scope="s">{{ s.row.platformDiscount || s.row.profitShareRatio || 0 }}%</template>
+        </el-table-column>
         <el-table-column label="本期佣金" min-width="130" sortable :sort-method="(a,b)=>a.commission-b.commission">
           <template slot-scope="s">
             <span style="color:#3b82f6;font-weight:600">¥{{ (s.row.commission || 0).toFixed(2) }}</span>
@@ -70,7 +72,7 @@ export default {
           proxyId: p.proxyId,
           proxyName: p.proxyName,
           companyName: p.companyName,
-          platformDiscount: p.platformDiscount,
+          platformDiscount: p.platformDiscount || p.profitShareRatio,
           commission: p.totalCommission || 0,
           balance: p.balance || 0,
           totalWithdrawal: p.totalWithdrawal || 0

+ 1 - 1
src/views/admin/proxy/index.vue

@@ -196,7 +196,7 @@ export default {
         inputType: 'text'
       }).then(({ value }) => {
         resetProxyPwd(row.proxyId, value).then(() => {
-          this.$modal.msgSuccess('密码重置成功,新密码为:' + value)
+          this.$message.success('密码重置成功,新密码为:' + value)
         }).catch(() => {
           this.$message.error('密码重置失败')
         })

+ 1 - 1
src/views/admin/serviceCost/index.vue

@@ -74,7 +74,7 @@ export default {
     loadList() {
       this.loading = true
       listServiceCost().then(res => {
-        this.costList = res.data || []
+        this.costList = (res.data || []).filter(c => c.serviceType !== 6)
         this.loading = false
       }).catch(() => { this.loading = false })
     },

+ 148 - 3
src/views/admin/sysCompany/index.vue

@@ -64,6 +64,7 @@
           <el-button size="mini" type="text" icon="el-icon-key" style="color:#722ed1" @click="handleResetPwd(scope.row)">重置密码</el-button>
           <el-button size="mini" type="text" style="color:#52c41a" icon="el-icon-coin" @click="handleRecharge(scope.row)">充值/扣款</el-button>
           <el-button size="mini" type="text" style="color:#722ed1" icon="el-icon-menu" @click="handleEditMenu(scope.row, 'sys')">管理端菜单</el-button>
+          <el-button size="mini" type="text" style="color:#e6a23c" icon="el-icon-price-tag" @click="handleModulePricing(scope.row)">模块定价</el-button>
           <el-button size="mini" type="text" style="color:#13c2c2" icon="el-icon-sell" @click="handleEditMenu(scope.row, 'com')">销售菜单</el-button>
           <el-button
             v-if="scope.row.status == 1"
@@ -125,6 +126,13 @@
               <span style="color:#999;font-size:12px;margin-left:8px">(通过充值/扣款调整)</span>
             </el-form-item>
           </el-col>
+          <el-col :span="12">
+            <el-form-item label="归属代理" prop="proxyId">
+              <el-select v-model="viewForm.proxyId" placeholder="请选择代理" clearable filterable style="width:100%">
+                <el-option v-for="p in proxyOptions" :key="p.proxyId" :label="p.proxyName" :value="p.proxyId" />
+              </el-select>
+            </el-form-item>
+          </el-col>
           <el-col :span="12">
             <el-form-item label="租户状态">
               <el-switch v-model="viewForm.statusBool" active-text="正常" inactive-text="禁用"
@@ -185,6 +193,13 @@
               <el-date-picker v-model="addDialog.form.expireTime" type="date" value-format="yyyy-MM-dd" placeholder="选择到期时间" style="width:100%" />
             </el-form-item>
           </el-col>
+          <el-col :span="12">
+            <el-form-item label="归属代理" prop="proxyId">
+              <el-select v-model="addDialog.form.proxyId" placeholder="请选择代理" clearable filterable style="width:100%">
+                <el-option v-for="p in proxyOptions" :key="p.proxyId" :label="p.proxyName" :value="p.proxyId" />
+              </el-select>
+            </el-form-item>
+          </el-col>
           <el-col :span="12">
             <el-form-item label="状态" prop="status">
               <el-radio-group v-model="addDialog.form.status">
@@ -255,11 +270,53 @@
         <el-button type="primary" :loading="menuDialog.submitting" @click="submitMenuEdit">确 定</el-button>
       </div>
     </el-dialog>
+
+    <!-- ===== 模块定价弹窗 ===== -->
+    <el-dialog :title="'模块定价 - ' + pricingDialog.tenantName" :visible.sync="pricingDialog.visible" width="800px" append-to-body destroy-on-close>
+      <div v-loading="pricingDialog.loading">
+        <el-table :data="pricingDialog.modules" border size="small" style="width:100%">
+          <el-table-column label="模块" align="center" prop="moduleName" min-width="120" />
+          <el-table-column label="全局售价" align="center" min-width="100">
+            <template slot-scope="scope">
+              <span style="color:#909399">{{ scope.row.globalPrice != null ? scope.row.globalPrice : '-' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="全局成本" align="center" min-width="100">
+            <template slot-scope="scope">
+              <span style="color:#909399">{{ scope.row.globalCost != null ? scope.row.globalCost : '-' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="租户售价" align="center" min-width="130">
+            <template slot-scope="scope">
+              <el-input-number v-model="scope.row.price" :precision="4" :min="0" :step="0.01" size="small" style="width:150px" placeholder="跟随全局" />
+            </template>
+          </el-table-column>
+          <el-table-column label="成本价" align="center" min-width="130">
+            <template slot-scope="scope">
+              <el-input-number v-model="scope.row.costPrice" :precision="4" :min="0" :step="0.01" size="small" style="width:150px" placeholder="跟随全局" />
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" align="center" min-width="80">
+            <template slot-scope="scope">
+              <el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁" size="small" />
+            </template>
+          </el-table-column>
+        </el-table>
+        <div style="color:#909399;font-size:12px;margin-top:8px">* 租户售价/成本价留空或填0则使用全局定价</div>
+      </div>
+      <div slot="footer">
+        <el-button @click="pricingDialog.visible = false">取 消</el-button>
+        <el-button type="primary" :loading="pricingDialog.submitting" @click="submitModulePricing">保 存</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
 import { listAllCompanies, getCompanyInfo, addCompany, updateTenant, disableCompany, enableCompany, rechargeCompany, exportCompany, getTenantMenuTree, editTenantMenu, resetTenantPwd } from '@/api/admin/sysCompany'
+import { listTrafficPricing, addTrafficPricing, updateTrafficPricing } from '@/api/admin/trafficPricing'
+import { allEnabledProxies } from '@/api/admin/proxy'
+import { listServiceCost } from '@/api/admin/serviceCost'
 import { generateRandomPwd } from '@/utils/index'
 
 export default {
@@ -331,13 +388,30 @@ export default {
         allMenuIds: [],     // 所有菜单ID
         loading: false,
         submitting: false
-      }
+      },
+      // 模块定价弹窗
+      pricingDialog: {
+        visible: false,
+        tenantId: null,
+        tenantName: '',
+        loading: false,
+        submitting: false,
+        modules: []
+      },
+      // 代理列表(下拉框用)
+      proxyOptions: []
     }
   },
   created() {
     this.getList()
+    this.loadProxyOptions()
   },
   methods: {
+    loadProxyOptions() {
+      allEnabledProxies().then(res => {
+        this.proxyOptions = res.data || []
+      }).catch(() => {})
+    },
     getList() {
       this.loading = true
       listAllCompanies(this.queryParams).then(response => {
@@ -363,6 +437,7 @@ export default {
         userName: '',
         password: generateRandomPwd(12),
         manager: '',
+        proxyId: null,
         startTime: null,
         expireTime: null,
         status: 1
@@ -388,7 +463,7 @@ export default {
         inputType: 'text'
       }).then(({ value }) => {
         resetTenantPwd(row.id, value).then(() => {
-          this.$modal.msgSuccess('密码重置成功,新密码为:' + value)
+          this.$message.success('密码重置成功,新密码为:' + value)
         }).catch(() => {
           this.$message.error('密码重置失败')
         })
@@ -420,6 +495,8 @@ export default {
         this.$nextTick(() => {
           if (this.$refs.editForm) this.$refs.editForm.clearValidate()
         })
+      }).catch(() => {
+        this.$message.error('获取租户信息失败')
       })
     },
     /** 提交编辑租户 */
@@ -433,7 +510,8 @@ export default {
           contactName: this.viewForm.contactName,
           contactPhone: this.viewForm.contactPhone,
           expireTime: this.viewForm.expireTime,
-          status: this.viewForm.statusBool
+          status: this.viewForm.statusBool,
+          proxyId: this.viewForm.proxyId
         }
         updateTenant(data).then(() => {
           this.$message.success('保存成功')
@@ -553,6 +631,73 @@ export default {
         this.menuDialog.loading = false
       })
     },
+    /** 模块定价 */
+    handleModulePricing(row) {
+      this.pricingDialog = {
+        visible: true,
+        tenantId: row.id,
+        tenantName: row.tenantName,
+        loading: true,
+        submitting: false,
+        modules: []
+      }
+      // 从收费配置API获取启用的模块列表(排除手拨外呼serviceType=6)
+      Promise.all([
+        listServiceCost(),
+        listTrafficPricing({ tenantId: row.id, pageSize: 100 })
+      ]).then(([costRes, pricingRes]) => {
+        const costList = (costRes.data || []).filter(c => c.enabled === 1 && c.serviceType !== 6)
+        const tenantPricings = pricingRes.rows || []
+        const pricingMap = {}
+        tenantPricings.forEach(p => { pricingMap[p.serviceType] = p })
+        this.pricingDialog.modules = costList.map(c => {
+          const existing = pricingMap[c.serviceType]
+          return {
+            serviceType: c.serviceType,
+            moduleName: c.configName,
+            unit: c.feeUnit,
+            globalPrice: c.feeStandard,
+            globalCost: c.platformCost,
+            price: existing ? existing.price : null,
+            costPrice: existing ? existing.costPrice : null,
+            status: existing ? existing.status : 1,
+            id: existing ? existing.id : null
+          }
+        })
+        this.pricingDialog.loading = false
+      }).catch(() => {
+        this.pricingDialog.loading = false
+        this.$message.error('加载定价数据失败')
+      })
+    },
+    /** 提交模块定价 */
+    submitModulePricing() {
+      this.pricingDialog.submitting = true
+      const tenantId = this.pricingDialog.tenantId
+      const promises = this.pricingDialog.modules.map(m => {
+        const data = {
+          tenantId: tenantId,
+          serviceType: m.serviceType,
+          price: m.price,
+          costPrice: m.costPrice,
+          status: m.status
+        }
+        if (m.id) {
+          data.id = m.id
+          return updateTrafficPricing(data)
+        } else {
+          return addTrafficPricing(data)
+        }
+      })
+      Promise.all(promises).then(() => {
+        this.$message.success('模块定价保存成功')
+        this.pricingDialog.visible = false
+      }).catch(() => {
+        this.$message.error('部分定价保存失败,请重试')
+      }).finally(() => {
+        this.pricingDialog.submitting = false
+      })
+    },
     /** 提交菜单编辑 */
     submitMenuEdit() {
       const tree = this.$refs.menuTree

+ 114 - 2
src/views/admin/voiceApiTenant/index.vue

@@ -26,6 +26,12 @@
       </el-form>
     </el-card>
 
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增绑定</el-button>
+      </el-col>
+    </el-row>
+
     <el-table v-loading="loading" :data="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 />
@@ -73,15 +79,52 @@
           <el-tag v-else type="danger" size="mini">禁用</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
         <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>
         </template>
       </el-table-column>
     </el-table>
 
     <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
 
+    <!-- 新增绑定弹窗 -->
+    <el-dialog title="新增外呼接口-租户绑定" :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" :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.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%" />
+          <span style="color:#909399;font-size:12px;margin-left:8px">1最高,同类型多接口时按优先级选择</span>
+        </el-form-item>
+        <el-form-item label="主线路">
+          <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-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" :loading="addSubmitting">确 定</el-button>
+        <el-button @click="addDialogVisible = false">取 消</el-button>
+      </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">
@@ -119,7 +162,7 @@
 </template>
 
 <script>
-import { listVoiceApiTenant, updateTenantPricing, getVoiceApiList } from '@/api/company/companyVoiceApi'
+import { listVoiceApiTenant, updateTenantPricing, getVoiceApiList, assignTenants, unassignTenant } from '@/api/company/companyVoiceApi'
 import { listAllCompanies } from '@/api/admin/sysCompany'
 
 export default {
@@ -142,6 +185,17 @@ export default {
       companySearchLoading: false,
       // 接口列表
       apiOptions: [],
+      // 新增绑定
+      addDialogVisible: false,
+      addSubmitting: false,
+      addForm: { apiId: undefined, companyId: undefined, price: 0, priority: 1, isPrimary: 0, allowManual: 0 },
+      addRules: {
+        apiId: [{ required: true, message: '请选择接口', trigger: 'change' }],
+        companyId: [{ required: true, message: '请选择租户', trigger: 'change' }],
+        price: [{ required: true, message: '请填写售价', trigger: 'blur' }]
+      },
+      // 租户列表(新增绑定用)
+      companyList: [],
       // 定价编辑
       pricingOpen: false,
       pricingSubmitting: false,
@@ -158,9 +212,16 @@ export default {
       }
     }
   },
+  computed: {
+    selectedApiCost() {
+      const api = this.apiOptions.find(a => a.apiId === this.addForm.apiId)
+      return api && api.costPrice ? api.costPrice : ''
+    }
+  },
   created() {
     this.getList()
     this.loadApiOptions()
+    this.loadCompanyList()
   },
   methods: {
     getList() {
@@ -184,6 +245,57 @@ export default {
         this.apiOptions = response.rows || response.data || []
       }).catch(() => {})
     },
+    loadCompanyList() {
+      listAllCompanies({ pageNum: 1, pageSize: 1000 }).then(res => {
+        this.companyList = res.rows || res.data || []
+      })
+    },
+    onApiChange(apiId) {
+      const api = this.apiOptions.find(a => a.apiId === apiId)
+      if (api && api.costPrice) {
+        this.addForm.price = api.costPrice
+      }
+    },
+    handleAdd() {
+      this.addForm = { apiId: undefined, companyId: undefined, price: 0, priority: 1, isPrimary: 0, allowManual: 0 }
+      this.addDialogVisible = true
+      this.$nextTick(() => {
+        if (this.$refs.addForm) this.$refs.addForm.clearValidate()
+      })
+    },
+    submitAdd() {
+      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({
+              apiId: this.addForm.apiId,
+              companyId: this.addForm.companyId,
+              price: this.addForm.price,
+              priority: this.addForm.priority,
+              isPrimary: this.addForm.isPrimary,
+              allowManual: this.addForm.allowManual,
+              status: 1
+            }).catch(() => {})
+          }
+          this.$message.success('绑定成功')
+          this.addDialogVisible = false
+          this.getList()
+        }).finally(() => { this.addSubmitting = false })
+      })
+    },
+    handleUnbind(row) {
+      this.$confirm('确认解除租户"' + row.companyName + '"与接口"' + row.apiName + '"的绑定?', '提示', {
+        confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
+      }).then(() => {
+        unassignTenant(row.apiId, row.companyId).then(() => {
+          this.$message.success('解除绑定成功')
+          this.getList()
+        })
+      }).catch(() => {})
+    },
     searchCompanies(query) {
       if (query.length < 1) { this.companyOptions = []; return }
       this.companySearchLoading = true