Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/saas_adminUi' into saas_adminUi

# Conflicts:
#	src/views/admin/sysCompany/index.vue
yys 3 dienas atpakaļ
vecāks
revīzija
4c05abcc3d

+ 15 - 0
jsconfig.json

@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    },
+    "target": "es5",
+    "module": "esnext",
+    "moduleResolution": "node",
+    "allowJs": true,
+    "checkJs": false
+  },
+  "include": ["src/**/*.js", "src/**/*.vue"],
+  "exclude": ["node_modules", "dist"]
+}

+ 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
+  })
+}
+
 // ========== 租户定价管理 ==========
 
 // 查询租户-接口定价列表(分页)

+ 17 - 0
src/api/company/companyVoiceRoboticCallBlacklist.js

@@ -51,3 +51,20 @@ export function changeVoiceRoboticCallBlacklistStatus(data) {
         data: data
     })
 }
+
+// 查看解密手机号
+export function queryVoiceRoboticCallBlacklistPhone(blacklistId) {
+    return request({
+        url: '/admin/voice-blacklist/queryPhone/' + blacklistId,
+        method: 'get'
+    })
+}
+
+// 导出外呼黑名单
+export function exportVoiceRoboticCallBlacklist(query) {
+    return request({
+        url: '/admin/voice-blacklist/export',
+        method: 'get',
+        params: query
+    })
+}

+ 0 - 80
src/api/monitor/job.js

@@ -1,80 +0,0 @@
-import request from '@/utils/request'
-
-// 查询定时任务调度列表
-export function listJob(query) {
-  return request({
-    url: '/monitor/job/list',
-    method: 'get',
-    params: query
-  })
-}
-
-// 查询定时任务调度详细
-export function getJob(jobId) {
-  return request({
-    url: '/monitor/job/' + jobId,
-    method: 'get'
-  })
-}
-
-// 新增定时任务调度
-export function addJob(data) {
-  return request({
-    url: '/monitor/job',
-    method: 'post',
-    data: data
-  })
-}
-
-// 修改定时任务调度
-export function updateJob(data) {
-  return request({
-    url: '/monitor/job',
-    method: 'put',
-    data: data
-  })
-}
-
-// 删除定时任务调度
-export function delJob(jobId) {
-  return request({
-    url: '/monitor/job/' + jobId,
-    method: 'delete'
-  })
-}
-
-// 导出定时任务调度
-export function exportJob(query) {
-  return request({
-    url: '/monitor/job/export',
-    method: 'get',
-    params: query
-  })
-}
-
-// 任务状态修改
-export function changeJobStatus(jobId, status) {
-  const data = {
-    jobId,
-    status
-  }
-  return request({
-    url: '/monitor/job/changeStatus',
-    method: 'put',
-    data: data
-  })
-}
-
-
-// 定时任务立即执行一次
-export function runJob(jobId, jobGroup) {
-  const data = {
-    jobId,
-    jobGroup
-  }
-  return request({
-    url: '/monitor/job/run',
-    method: 'put',
-    data: data
-  })
-}

+ 9 - 7
src/api/monitor/jobLog.js

@@ -9,19 +9,21 @@ export function listJobLog(query) {
   })
 }
 
-// 删除调度日志
-export function delJobLog(jobLogId) {
+// 删除调度日志(支持 tenantId 参数,切换对应租户库执行删除)
+export function delJobLog(jobLogIds, tenantId) {
   return request({
-    url: '/monitor/jobLog/' + jobLogId,
-    method: 'delete'
+    url: '/monitor/jobLog/' + jobLogIds,
+    method: 'delete',
+    params: tenantId ? { tenantId } : {}
   })
 }
 
-// 清空调度日志
-export function cleanJobLog() {
+// 清空调度日志(支持 tenantId,仅清空指定租户的日志)
+export function cleanJobLog(tenantId) {
   return request({
     url: '/monitor/jobLog/clean',
-    method: 'delete'
+    method: 'delete',
+    params: tenantId ? { tenantId } : {}
   })
 }
 

+ 44 - 0
src/api/monitor/jobTemplate.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// ��ѯ����ģ���б�
+export function listTemplate(query) {
+  return request({
+    url: '/monitor/tenantJob/template/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// ��ѯ����ģ����ϸ
+export function getTemplate(templateId) {
+  return request({
+    url: '/monitor/tenantJob/template/' + templateId,
+    method: 'get'
+  })
+}
+
+// ���������
+export function addTemplate(data) {
+  return request({
+    url: '/monitor/tenantJob/template',
+    method: 'post',
+    data: data
+  })
+}
+
+// �޸�����ģ��
+export function updateTemplate(data) {
+  return request({
+    url: '/monitor/tenantJob/template',
+    method: 'put',
+    data: data
+  })
+}
+
+// ɾ������ģ��
+export function delTemplate(templateIds) {
+  return request({
+    url: '/monitor/tenantJob/template/' + templateIds,
+    method: 'delete'
+  })
+}

+ 18 - 0
src/api/monitor/taskRegistry.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// Bean registry list (paginated)
+export function listTaskRegistry(query) {
+  return request({
+    url: '/monitor/taskRegistry/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// Refresh Bean registry cache
+export function refreshTaskRegistry() {
+  return request({
+    url: '/monitor/taskRegistry/refresh',
+    method: 'post'
+  })
+}

+ 53 - 0
src/api/monitor/tenantJob.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+export function listJobTemplate(query) {
+  return request({
+    url: '/monitor/tenantJob/template/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function listTenantScopeTemplates() {
+  return request({
+    url: '/monitor/tenantJob/template/tenantScope',
+    method: 'get'
+  })
+}
+
+export function getTenantJobConfig(tenantId) {
+  return request({
+    url: '/monitor/tenantJob/config/' + tenantId,
+    method: 'get'
+  })
+}
+
+export function saveTenantJobConfig(data) {
+  return request({
+    url: '/monitor/tenantJob/config',
+    method: 'put',
+    data: data
+  })
+}
+
+export function syncTenantJob(tenantId) {
+  return request({
+    url: '/monitor/tenantJob/sync/' + tenantId,
+    method: 'post'
+  })
+}
+
+export function syncAllTenantJob() {
+  return request({
+    url: '/monitor/tenantJob/syncAll',
+    method: 'post'
+  })
+}
+
+export function updateTenantJobStatus(configId, status) {
+  return request({
+    url: '/monitor/tenantJob/config/status',
+    method: 'put',
+    params: { configId, status }
+  })
+}

+ 16 - 0
src/router/index.js

@@ -170,6 +170,22 @@ export const constantRoutes = [
       }
     ]
   },
+  // SaaS 自定义:系统配置 > 定时任务(模板管理) 对应的 “调度日志” 菜单 path 为 'jobLog'(短路径,与 'job' 平级)
+  // 添加此隐藏路由确保 router.push({ path: '/jobLog' }) 或从菜单点击时能正确加载日志页,并高亮父菜单“定时任务”
+  {
+    path: '/jobLog',
+    component: Layout,
+    hidden: true,
+    redirect: '/jobLog/index',
+    children: [
+      {
+        path: 'index',
+        component: (resolve) => require(['@/views/monitor/job/log'], resolve),
+        name: 'SaaSJobLog',
+        meta: { title: '调度日志', activeMenu: '/job' }
+      }
+    ]
+  },
   {
     path: '/tool/gen-edit',
     component: Layout,

+ 21 - 14
src/views/admin/keywordManage/index.vue

@@ -19,7 +19,7 @@
         <el-button type="primary" icon="el-icon-plus" size="mini" @click="openDialog(null)">新增关键词</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
+        <el-button type="warning" icon="el-icon-download" size="mini" :loading="exportLoading" @click="handleExport">导出</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="loadList" />
     </el-row>
@@ -30,9 +30,9 @@
       <el-table-column label="关键词内容" prop="keyword" min-width="200" />
       <el-table-column label="类型" prop="keywordType" min-width="100" align="center">
         <template slot-scope="s">
-          <el-tag v-if="s.row.keywordType === 1" type="danger" size="mini">违禁词</el-tag>
-          <el-tag v-else-if="s.row.keywordType === 2" type="warning" size="mini">敏感词</el-tag>
-          <el-tag v-else size="mini">{{ s.row.keywordType || '-' }}</el-tag>
+          <el-tag v-if="s.row.keywordType == 1" type="danger" size="mini">{{ getTypeLabel(s.row.keywordType) }}</el-tag>
+          <el-tag v-else-if="s.row.keywordType == 2" type="warning" size="mini">{{ getTypeLabel(s.row.keywordType) }}</el-tag>
+          <el-tag v-else size="mini">{{ getTypeLabel(s.row.keywordType) || '-' }}</el-tag>
         </template>
       </el-table-column>
       <el-table-column label="创建时间" prop="createTime" min-width="150" align="center" />
@@ -54,8 +54,8 @@
         </el-form-item>
         <el-form-item label="类型" prop="keywordType">
           <el-select v-model="form.keywordType" placeholder="请选择类型" style="width:100%">
-            <el-option label="违禁词" :value="1" />
-            <el-option label="敏感词" :value="2" />
+            <el-option v-for="item in typeOptions" :key="item.dictValue"
+              :label="item.dictLabel" :value="item.dictValue" />
           </el-select>
         </el-form-item>
         <el-form-item label="备注" prop="remark">
@@ -72,6 +72,7 @@
 
 <script>
 import request from '@/utils/request'
+import { exportKeyword } from '@/api/system/keyword'
 
 export default {
   name: 'AdminKeywordManage',
@@ -79,6 +80,8 @@ export default {
     return {
       showSearch: true,
       loading: false,
+      exportLoading: false,
+      typeOptions: [],
       dataList: [],
       total: 0,
       queryParams: { pageNum: 1, pageSize: 10, keyword: null },
@@ -93,6 +96,9 @@ export default {
     }
   },
   created() {
+    this.getDicts('keyword_type').then(response => {
+      this.typeOptions = response.data
+    })
     this.loadList()
   },
   methods: {
@@ -144,14 +150,15 @@ export default {
       })
     },
     handleExport() {
-      request({ url: '/system/keyword/export', method: 'get', params: this.queryParams, responseType: 'blob' }).then(r => {
-        const blob = new Blob([r])
-        const link = document.createElement('a')
-        link.href = URL.createObjectURL(blob)
-        link.download = '关键词数据.xlsx'
-        link.click()
-        URL.revokeObjectURL(link.href)
-      })
+      this.exportLoading = true
+      exportKeyword(this.queryParams).then(response => {
+        this.download(response.msg)
+        this.exportLoading = false
+      }).catch(() => { this.exportLoading = false })
+    },
+    getTypeLabel(value) {
+      const item = this.typeOptions.find(o => o.dictValue == value)
+      return item ? item.dictLabel : null
     },
     reset() {
       this.form = { keywordId: null, keyword: '', keywordType: 1, remark: '' }

+ 15 - 15
src/views/admin/sysCompany/index.vue

@@ -2,20 +2,20 @@
   <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="tenantName">
-        <el-input v-model="queryParams.tenantName" placeholder="租户编码/名称" clearable @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item label="状态" prop="status">
-        <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
-          <el-option v-for="item in statusOptions" :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-form :model="queryParams" ref="queryForm" :inline="true" size="small">
+        <el-form-item label="租户名称" prop="tenantName">
+          <el-input v-model="queryParams.tenantName" placeholder="租户编码/名称" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
+            <el-option v-for="item in statusOptions" :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-card>
 
     <!-- ===== 操作栏 ===== -->
@@ -130,7 +130,7 @@
           <el-col :span="12">
             <el-form-item label="租户状态">
               <el-switch v-model="viewForm.statusBool" active-text="正常" inactive-text="禁用"
-                :active-value="1" :inactive-value="0" />
+                         :active-value="1" :inactive-value="0" />
             </el-form-item>
           </el-col>
           <el-col :span="12">

+ 12 - 1
src/views/admin/tenantMenu/index.vue

@@ -20,6 +20,16 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="显示状态" prop="visible">
+        <el-select v-model="queryParams.visible" placeholder="显示状态" clearable size="small">
+          <el-option
+            v-for="dict in visibleOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </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>
@@ -318,7 +328,8 @@ export default {
       // 查询参数
       queryParams: {
         menuName: undefined,
-        visible: undefined
+        visible: "0",
+        status: undefined
       },
       // 表单参数
       form: {},

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

+ 63 - 10
src/views/admin/voiceBlacklist/index.vue

@@ -18,6 +18,13 @@
             @keyup.enter.native="handleQuery"
           />
         </el-form-item>
+        <el-form-item label="黑名单级别" prop="businessType">
+          <el-select v-model="queryParams.businessType" placeholder="请选择黑名单级别" clearable size="small" style="width: 160px">
+            <el-option label="平台封禁" value="11" />
+            <el-option label="租户级" value="12" />
+            <el-option label="线路级" value="13" />
+          </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>
@@ -60,8 +67,20 @@
 
     <el-table v-loading="loading" :data="dataList" border size="small" style="width:100%" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="手机号" align="center" prop="phone" min-width="130" />
+      <el-table-column label="手机号" align="center" prop="phone" min-width="200">
+        <template slot-scope="scope">
+          <span>{{ scope.row.phone }}<el-button icon="el-icon-search" size="mini" @click="handlePhone(scope)" style="margin-left: 20px;" circle></el-button></span>
+        </template>
+      </el-table-column>
       <el-table-column label="所属租户" align="center" prop="companyName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="黑名单级别" align="center" prop="businessType" width="120">
+        <template slot-scope="scope">
+          <span v-if="scope.row.businessType === '11'">平台封禁</span>
+          <span v-else-if="scope.row.businessType === '12'">租户级</span>
+          <span v-else-if="scope.row.businessType === '13'">线路级</span>
+          <span v-else>{{ scope.row.businessType || '-' }}</span>
+        </template>
+      </el-table-column>
       <el-table-column label="状态" align="center" prop="status" width="100">
         <template slot-scope="scope">
           <el-switch
@@ -131,7 +150,9 @@ import {
   addVoiceRoboticCallBlacklist,
   updateVoiceRoboticCallBlacklist,
   delVoiceRoboticCallBlacklist,
-  changeVoiceRoboticCallBlacklistStatus
+  changeVoiceRoboticCallBlacklistStatus,
+  queryVoiceRoboticCallBlacklistPhone,
+  exportVoiceRoboticCallBlacklist
 } from '@/api/company/companyVoiceRoboticCallBlacklist'
 
 export default {
@@ -151,7 +172,8 @@ export default {
         pageNum: 1,
         pageSize: 10,
         phone: null,
-        companyName: null
+        companyName: null,
+        businessType: null
       },
       // 弹窗
       dialogOpen: false,
@@ -176,8 +198,15 @@ export default {
   methods: {
     getList() {
       this.loading = true
-      listVoiceRoboticCallBlacklist(this.queryParams).then(response => {
-        this.dataList = response.rows
+      listVoiceRoboticCallBlacklist({
+        ...this.queryParams,
+        targetValue: this.queryParams.phone
+      }).then(response => {
+        this.dataList = (response.rows || []).map(item => {
+          item.phone = item.targetValue
+          item.blacklistId = item.id
+          return item
+        })
         this.total = response.total
         this.loading = false
       })
@@ -204,6 +233,10 @@ export default {
       const id = row.blacklistId
       getVoiceRoboticCallBlacklist(id).then(response => {
         this.editForm = response.data
+        this.editForm.blacklistId = response.data.id
+        queryVoiceRoboticCallBlacklistPhone(id).then(res => {
+          this.editForm.phone = res.mobile
+        })
         this.dialogTitle = '编辑黑名单'
         this.dialogOpen = true
       })
@@ -211,14 +244,21 @@ export default {
     submitForm() {
       this.$refs['editForm'].validate(valid => {
         if (valid) {
+          const payload = {
+            ...this.editForm,
+            id: this.editForm.blacklistId,
+            targetValue: this.editForm.phone,
+            targetType: 1,
+            businessType: '11'
+          }
           if (this.editForm.blacklistId) {
-            updateVoiceRoboticCallBlacklist(this.editForm).then(response => {
+            updateVoiceRoboticCallBlacklist(payload).then(response => {
               this.$message.success('修改成功')
               this.dialogOpen = false
               this.getList()
             })
           } else {
-            addVoiceRoboticCallBlacklist(this.editForm).then(response => {
+            addVoiceRoboticCallBlacklist(payload).then(response => {
               this.$message.success('新增成功')
               this.dialogOpen = false
               this.getList()
@@ -242,7 +282,7 @@ export default {
     },
     handleStatusChange(row) {
       changeVoiceRoboticCallBlacklistStatus({
-        blacklistId: row.blacklistId,
+        id: row.blacklistId,
         status: row.status
       }).then(() => {
         this.$message.success('状态修改成功')
@@ -251,13 +291,26 @@ export default {
       })
     },
     handleExport() {
-      this.exportLoading = true
-      this.download('/admin/voice-blacklist/export', { ...this.queryParams }).then(() => {
+      const queryParams = { ...this.queryParams, targetValue: this.queryParams.phone }
+      this.$confirm('是否确认导出所有黑名单数据项?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.exportLoading = true
+        return exportVoiceRoboticCallBlacklist(queryParams)
+      }).then(response => {
+        this.download(response.msg)
         this.exportLoading = false
       }).catch(() => {
         this.exportLoading = false
       })
     },
+    handlePhone(scope) {
+      queryVoiceRoboticCallBlacklistPhone(scope.row.blacklistId).then(res => {
+        scope.row.phone = res.mobile
+      })
+    },
     resetEditForm() {
       this.editForm = {
         blacklistId: null,

+ 243 - 371
src/views/monitor/job/index.vue

@@ -1,33 +1,22 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="模板编码" prop="templateCode">
+        <el-input v-model="queryParams.templateCode" placeholder="请输入模板编码" clearable size="small" @keyup.enter.native="handleQuery" />
+      </el-form-item>
       <el-form-item label="任务名称" prop="jobName">
-        <el-input
-          v-model="queryParams.jobName"
-          placeholder="请输入任务名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.jobName" placeholder="请输入任务名称" clearable size="small" @keyup.enter.native="handleQuery" />
       </el-form-item>
-      <el-form-item label="任务组名" prop="jobGroup">
-        <el-select v-model="queryParams.jobGroup" placeholder="请选择任务组名" clearable size="small">
-          <el-option
-            v-for="dict in jobGroupOptions"
-            :key="dict.dictValue"
-            :label="dict.dictLabel"
-            :value="dict.dictValue"
-          />
+      <el-form-item label="作用域" prop="scope">
+        <el-select v-model="queryParams.scope" placeholder="请选择" clearable size="small">
+          <el-option label="租户级" value="TENANT" />
+          <el-option label="平台级" value="PLATFORM" />
         </el-select>
       </el-form-item>
-      <el-form-item label="任务状态" prop="status">
-        <el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable size="small">
-          <el-option
-            v-for="dict in statusOptions"
-            :key="dict.dictValue"
-            :label="dict.dictLabel"
-            :value="dict.dictValue"
-          />
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择" clearable size="small">
+          <el-option label="启用" value="0" />
+          <el-option label="停用" value="1" />
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -38,127 +27,74 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['monitor:job:add']"
-        >新增</el-button>
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['monitor:job:add']">新增模板</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['monitor:job:edit']">修改</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          type="success"
-          plain
-          icon="el-icon-edit"
-          size="mini"
-          :disabled="single"
-          @click="handleUpdate"
-          v-hasPermi="['monitor:job:edit']"
-        >修改</el-button>
+        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['monitor:job:remove']">删除</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          type="danger"
-          plain
-          icon="el-icon-delete"
-          size="mini"
-          :disabled="multiple"
-          @click="handleDelete"
-          v-hasPermi="['monitor:job:remove']"
-        >删除</el-button>
+        <el-button type="info" plain icon="el-icon-notebook-2" size="mini" @click="handleTaskRegistry" v-hasPermi="['monitor:job:add']">Bean注册表</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          type="warning"
-          plain
-          icon="el-icon-download"
-          size="mini"
-          :loading="exportLoading"
-          @click="handleExport"
-          v-hasPermi="['monitor:job:export']"
-        >导出</el-button>
+        <el-button type="primary" plain icon="el-icon-user" size="mini" @click="handleTenantJobConfig" v-hasPermi="['monitor:job:edit']">租户任务配置</el-button>
       </el-col>
       <el-col :span="1.5">
-        <el-button
-          type="info"
-          plain
-          icon="el-icon-s-operation"
-          size="mini"
-          @click="handleJobLog"
-          v-hasPermi="['monitor:job:query']"
-        >日志</el-button>
+        <el-button type="info" plain icon="el-icon-s-operation" size="mini" @click="handleJobLog" v-hasPermi="['monitor:job:query']">任务日志</el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table v-loading="loading" :data="jobList" @selection-change="handleSelectionChange">
+    <el-table v-loading="loading" :data="templateList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="任务编号" width="100" align="center" prop="jobId" />
+      <el-table-column label="模板编码" width="140" align="center" prop="templateCode" :show-overflow-tooltip="true" />
       <el-table-column label="任务名称" align="center" prop="jobName" :show-overflow-tooltip="true" />
       <el-table-column label="任务组名" align="center" prop="jobGroup">
         <template slot-scope="scope">
           <dict-tag :options="jobGroupOptions" :value="scope.row.jobGroup"/>
         </template>
       </el-table-column>
-      <el-table-column label="调用目标字符串" align="center" prop="invokeTarget" :show-overflow-tooltip="true" />
-      <el-table-column label="cron执行表达式" align="center" prop="cronExpression" :show-overflow-tooltip="true" />
-      <el-table-column label="状态" align="center">
+      <el-table-column label="调用目标" align="center" prop="invokeTarget" :show-overflow-tooltip="true" min-width="180" />
+      <el-table-column label="cron表达式" align="center" prop="cronExpression" :show-overflow-tooltip="true" width="140" />
+      <el-table-column label="作用域" align="center" width="80">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.scope === 'PLATFORM'" size="mini">平台</el-tag>
+          <el-tag v-else type="success" size="mini">租户</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="默认状态" align="center" width="80">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.defaultStatus === '0'" type="primary" size="mini">运行</el-tag>
+          <el-tag v-else type="warning" size="mini">暂停</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="启用" align="center" width="60">
         <template slot-scope="scope">
-          <el-switch
-            v-model="scope.row.status"
-            active-value="0"
-            inactive-value="1"
-            @change="handleStatusChange(scope.row)"
-          ></el-switch>
+          <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleTemplateStatusChange(scope.row)" />
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
         <template slot-scope="scope">
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-edit"
-            @click="handleUpdate(scope.row)"
-            v-hasPermi="['monitor:job:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['monitor:job:remove']"
-          >删除</el-button>
-          <el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)" v-hasPermi="['monitor:job:changeStatus', 'monitor:job:query']">
-            <span class="el-dropdown-link">
-              <i class="el-icon-d-arrow-right el-icon--right"></i>更多
-            </span>
-            <el-dropdown-menu slot="dropdown">
-              <el-dropdown-item command="handleRun" icon="el-icon-caret-right"
-                v-hasPermi="['monitor:job:changeStatus']">执行一次</el-dropdown-item>
-              <el-dropdown-item command="handleView" icon="el-icon-view"
-                v-hasPermi="['monitor:job:query']">任务详细</el-dropdown-item>
-              <el-dropdown-item command="handleJobLog" icon="el-icon-s-operation"
-                v-hasPermi="['monitor:job:query']">调度日志</el-dropdown-item>
-            </el-dropdown-menu>
-          </el-dropdown>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['monitor:job:edit']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['monitor:job:remove']">删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-s-operation" @click="handleRowJobLog(scope.row)" v-hasPermi="['monitor:job:query']">日志</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"
-    />
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
 
-    <!-- 添加或修改定时任务对话框 -->
+    <!-- 添加/修改任务模板对话框 -->
     <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="120px">
         <el-row>
+          <el-col :span="12">
+            <el-form-item label="模板编码" prop="templateCode">
+              <el-input v-model="form.templateCode" placeholder="请输入模板编码" />
+            </el-form-item>
+          </el-col>
           <el-col :span="12">
             <el-form-item label="任务名称" prop="jobName">
               <el-input v-model="form.jobName" placeholder="请输入任务名称" />
@@ -167,44 +103,41 @@
           <el-col :span="12">
             <el-form-item label="任务分组" prop="jobGroup">
               <el-select v-model="form.jobGroup" placeholder="请选择">
-                <el-option
-                  v-for="dict in jobGroupOptions"
-                  :key="dict.dictValue"
-                  :label="dict.dictLabel"
-                  :value="dict.dictValue"
-                ></el-option>
+                <el-option v-for="dict in jobGroupOptions" :key="dict.dictValue" :label="dict.dictLabel" :value="dict.dictValue" />
               </el-select>
             </el-form-item>
           </el-col>
+          <el-col :span="12">
+            <el-form-item label="模块标签" prop="moduleTag">
+              <el-input v-model="form.moduleTag" placeholder="请输入模块标签" />
+            </el-form-item>
+          </el-col>
           <el-col :span="24">
             <el-form-item prop="invokeTarget">
               <span slot="label">
                 调用方法
                 <el-tooltip placement="top">
-                  <div slot="content">
-                    Bean调用示例:Task.Params('fs')
-                    <br />Class类调用示例:com.fs.quartz.task.Task.params('fs')
-                    <br />参数说明:支持字符串,布尔类型,长整型,浮点型,整型
-                  </div>
+                  <div slot="content">Bean调用示例:liveTask.deliveryOp()<br />参数说明:支持无参方法的直接调用</div>
                   <i class="el-icon-question"></i>
                 </el-tooltip>
               </span>
-              <el-input v-model="form.invokeTarget" placeholder="请输入调用目标字符串" />
+              <el-input v-model="form.invokeTarget" placeholder="请输入调用目标字符串">
+                <template slot="append">
+                  <el-button @click="openRegistry = true; initRegistry()">从注册表选择</el-button>
+                </template>
+              </el-input>
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="cron表达式" prop="cronExpression">
               <el-input v-model="form.cronExpression" placeholder="请输入cron执行表达式">
                 <template slot="append">
-                  <el-button type="primary" @click="handleShowCron">
-                    生成表达式
-                    <i class="el-icon-time el-icon--right"></i>
-                  </el-button>
+                  <el-button type="primary" @click="handleShowCron">生成表达式<i class="el-icon-time el-icon--right"></i></el-button>
                 </template>
               </el-input>
             </el-form-item>
           </el-col>
-          <el-col :span="24">
+          <el-col :span="12">
             <el-form-item label="错误策略" prop="misfirePolicy">
               <el-radio-group v-model="form.misfirePolicy" size="small">
                 <el-radio-button label="1">立即执行</el-radio-button>
@@ -222,13 +155,26 @@
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="状态">
+            <el-form-item label="作用域">
+              <el-radio-group v-model="form.scope" size="small">
+                <el-radio-button label="TENANT">租户级</el-radio-button>
+                <el-radio-button label="PLATFORM">平台级</el-radio-button>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="默认租户状态">
+              <el-radio-group v-model="form.defaultStatus" size="small">
+                <el-radio-button label="0">运行</el-radio-button>
+                <el-radio-button label="1">暂停</el-radio-button>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="模板状态">
               <el-radio-group v-model="form.status">
-                <el-radio
-                  v-for="dict in statusOptions"
-                  :key="dict.dictValue"
-                  :label="dict.dictValue"
-                >{{dict.dictLabel}}</el-radio>
+                <el-radio-button label="0">启用</el-radio-button>
+                <el-radio-button label="1">停用</el-radio-button>
               </el-radio-group>
             </el-form-item>
           </el-col>
@@ -240,62 +186,84 @@
       </div>
     </el-dialog>
 
+    <!-- Cron表达式生成器 -->
     <el-dialog title="Cron表达式生成器" :visible.sync="openCron" append-to-body class="scrollbar">
       <crontab @hide="openCron=false" @fill="crontabFill" :expression="expression"></crontab>
     </el-dialog>
 
-    <!-- 任务日志详细 -->
-    <el-dialog title="任务详细" :visible.sync="openView" width="700px" append-to-body>
-      <el-form ref="form" :model="form" label-width="120px" size="mini">
-        <el-row>
-          <el-col :span="12">
-            <el-form-item label="任务编号:">{{ form.jobId }}</el-form-item>
-            <el-form-item label="任务名称:">{{ form.jobName }}</el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="任务分组:">{{ jobGroupFormat(form) }}</el-form-item>
-            <el-form-item label="创建时间:">{{ form.createTime }}</el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="cron表达式:">{{ form.cronExpression }}</el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="下次执行时间:">{{ parseTime(form.nextValidTime) }}</el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="调用目标方法:">{{ form.invokeTarget }}</el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="任务状态:">
-              <div v-if="form.status == 0">正常</div>
-              <div v-else-if="form.status == 1">失败</div>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="是否并发:">
-              <div v-if="form.concurrent == 0">允许</div>
-              <div v-else-if="form.concurrent == 1">禁止</div>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="执行策略:">
-              <div v-if="form.misfirePolicy == 0">默认策略</div>
-              <div v-else-if="form.misfirePolicy == 1">立即执行</div>
-              <div v-else-if="form.misfirePolicy == 2">执行一次</div>
-              <div v-else-if="form.misfirePolicy == 3">放弃执行</div>
-            </el-form-item>
-          </el-col>
-        </el-row>
+    <!-- Bean 注册表 -->
+    <el-dialog title="可调度 Bean 方法(invoke_target 参考)" :visible.sync="openRegistry" width="960px" append-to-body @open="initRegistry">
+      <el-form :model="registryQueryParams" ref="registryQueryForm" :inline="true" size="small">
+        <el-form-item label="Bean" prop="beanName">
+          <el-input v-model="registryQueryParams.beanName" placeholder="Bean 名称" clearable @keyup.enter.native="handleRegistryQuery" />
+        </el-form-item>
+        <el-form-item label="方法" prop="methodName">
+          <el-input v-model="registryQueryParams.methodName" placeholder="方法名" clearable @keyup.enter.native="handleRegistryQuery" />
+        </el-form-item>
+        <el-form-item label="调用目标" prop="invokeTarget">
+          <el-input v-model="registryQueryParams.invokeTarget" placeholder="invoke_target" clearable @keyup.enter.native="handleRegistryQuery" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleRegistryQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetRegistryQuery">重置</el-button>
+        </el-form-item>
       </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="openView = false">关 闭</el-button>
-      </div>
+      <el-table v-loading="registryLoading" :data="registryList" max-height="420">
+        <el-table-column label="Bean" prop="beanName" width="180" show-overflow-tooltip />
+        <el-table-column label="类名" prop="className" width="160" show-overflow-tooltip />
+        <el-table-column label="方法" prop="methodName" width="160" show-overflow-tooltip />
+        <el-table-column label="调用目标" prop="invokeTarget" show-overflow-tooltip />
+        <el-table-column label="操作" width="100" align="center">
+          <template slot-scope="scope">
+            <el-button type="text" size="mini" @click="applyInvokeTarget(scope.row)">选用</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination v-show="registryTotal > 0" :total="registryTotal" :page.sync="registryQueryParams.pageNum" :limit.sync="registryQueryParams.pageSize" @pagination="getRegistryList" />
+    </el-dialog>
+
+    <!-- 租户任务配置 -->
+    <el-dialog title="租户定时任务配置" :visible.sync="openTenantJob" width="960px" append-to-body @open="loadTenantJobDialog">
+      <el-form :inline="true" size="small">
+        <el-form-item label="租户">
+          <el-select v-model="tenantJobForm.tenantId" filterable placeholder="选择租户" style="width: 280px" @change="loadTenantJobSelection">
+            <el-option v-for="t in tenantOptions" :key="t.id" :label="t.tenantName + ' (' + t.tenantCode + ')'" :value="t.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="success" size="mini" :disabled="!tenantJobForm.tenantId" @click="doSyncTenantJob">同步到租户库</el-button>
+          <el-button type="warning" size="mini" @click="doSyncAllTenantJob">同步全部租户</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table v-loading="tenantJobLoading" :data="tenantJobConfigList" max-height="420" v-if="tenantJobForm.tenantId">
+        <el-table-column label="任务名称" prop="jobName" min-width="140" show-overflow-tooltip />
+        <el-table-column label="模板编码" prop="templateCode" width="140" show-overflow-tooltip />
+        <el-table-column label="调用目标" prop="invokeTarget" min-width="180" show-overflow-tooltip />
+        <el-table-column label="cron" prop="defaultCronExpression" width="140" show-overflow-tooltip />
+        <el-table-column label="状态" width="80" align="center">
+          <template slot-scope="scope">
+            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleTenantJobStatusChange(scope.row)" />
+          </template>
+        </el-table-column>
+        <el-table-column label="同步状态" width="80" align="center">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.syncStatus === '1'" type="success" size="mini">已同步</el-tag>
+            <el-tag v-else-if="scope.row.syncStatus === '2'" type="danger" size="mini">失败</el-tag>
+            <el-tag v-else type="info" size="mini">未同步</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-empty v-if="tenantJobForm.tenantId && !tenantJobLoading && !tenantJobConfigList.length" description="暂未同步,请点击「同步到租户库」按钮" />
+      <el-empty v-if="!tenantJobForm.tenantId" description="请先选择租户" />
     </el-dialog>
   </div>
 </template>
 
 <script>
-import { listJob, getJob, delJob, addJob, updateJob, exportJob, runJob, changeJobStatus } from "@/api/monitor/job";
+import { listTemplate, getTemplate, addTemplate, updateTemplate, delTemplate } from "@/api/monitor/jobTemplate";
+import { listTaskRegistry } from "@/api/monitor/taskRegistry";
+import { getTenantJobConfig, updateTenantJobStatus, syncTenantJob, syncAllTenantJob } from "@/api/monitor/tenantJob";
+import { tenantList } from "@/api/tenant/tenant";
 import Crontab from '@/components/Crontab'
 
 export default {
@@ -303,246 +271,150 @@ export default {
   name: "Job",
   data() {
     return {
-      // 遮罩层
       loading: true,
-      // 导出遮罩层
-      exportLoading: false,
-      // 选中数组
       ids: [],
-      // 非单个禁用
       single: true,
-      // 非多个禁用
       multiple: true,
-      // 显示搜索条件
       showSearch: true,
-      // 总条数
       total: 0,
-      // 定时任务表格数据
-      jobList: [],
-      // 弹出层标题
+      templateList: [],
       title: "",
-      // 是否显示弹出层
       open: false,
-      // 是否显示详细弹出层
-      openView: false,
-      // 是否显示Cron表达式弹出层
       openCron: false,
-      // 传入的表达式
       expression: "",
-      // 任务组名字典
+      // Bean 注册表
+      openRegistry: false,
+      registryLoading: false,
+      registryList: [],
+      registryTotal: 0,
+      registryQueryParams: { pageNum: 1, pageSize: 10, beanName: undefined, methodName: undefined, invokeTarget: undefined },
+      // 租户任务配置
+      openTenantJob: false,
+      tenantJobLoading: false,
+      tenantJobConfigList: [],
+      tenantOptions: [],
+      tenantJobForm: { tenantId: undefined },
+      // 字典
       jobGroupOptions: [],
-      // 状态字典
-      statusOptions: [],
       // 查询参数
-      queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        jobName: undefined,
-        jobGroup: undefined,
-        status: undefined
-      },
-      // 表单参数
+      queryParams: { pageNum: 1, pageSize: 10, templateCode: undefined, jobName: undefined, scope: undefined, status: undefined },
+      // 表单
       form: {},
-      // 表单校验
       rules: {
-        jobName: [
-          { required: true, message: "任务名称不能为空", trigger: "blur" }
-        ],
-        invokeTarget: [
-          { required: true, message: "调用目标字符串不能为空", trigger: "blur" }
-        ],
-        cronExpression: [
-          { required: true, message: "cron执行表达式不能为空", trigger: "blur" }
-        ]
+        templateCode: [{ required: true, message: "模板编码不能为空", trigger: "blur" }],
+        jobName: [{ required: true, message: "任务名称不能为空", trigger: "blur" }],
+        invokeTarget: [{ required: true, message: "调用目标不能为空", trigger: "blur" }],
+        cronExpression: [{ required: true, message: "cron表达式不能为空", trigger: "blur" }]
       }
     };
   },
   created() {
     this.getList();
-    this.getDicts("sys_job_group").then(response => {
-      this.jobGroupOptions = response.data;
-    });
-    this.getDicts("sys_job_status").then(response => {
-      this.statusOptions = response.data;
-    });
+    this.getDicts("sys_job_group").then(response => { this.jobGroupOptions = response.data; });
   },
   methods: {
-    /** 查询定时任务列表 */
     getList() {
       this.loading = true;
-      listJob(this.queryParams).then(response => {
-        this.jobList = response.rows;
+      listTemplate(this.queryParams).then(response => {
+        this.templateList = response.rows;
         this.total = response.total;
         this.loading = false;
       });
     },
-    // 任务组名字典翻译
-    jobGroupFormat(row, column) {
-      return this.selectDictLabel(this.jobGroupOptions, row.jobGroup);
-    },
-    // 取消按钮
     cancel() {
       this.open = false;
       this.reset();
     },
-    // 表单重置
     reset() {
-      this.form = {
-        jobId: undefined,
-        jobName: undefined,
-        jobGroup: undefined,
-        invokeTarget: undefined,
-        cronExpression: undefined,
-        misfirePolicy: 1,
-        concurrent: 1,
-        status: "0"
-      };
+      this.form = { templateCode: undefined, jobName: undefined, jobGroup: undefined, invokeTarget: undefined, cronExpression: undefined, misfirePolicy: '3', concurrent: '1', scope: 'TENANT', moduleTag: undefined, defaultStatus: '1', status: '0' };
       this.resetForm("form");
     },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.queryParams.pageNum = 1;
-      this.getList();
+    handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
+    resetQuery() { this.resetForm("queryForm"); this.handleQuery(); },
+    handleSelectionChange(selection) { this.ids = selection.map(item => item.templateId); this.single = selection.length != 1; this.multiple = !selection.length; },
+    // 模板状态开关
+    handleTemplateStatusChange(row) {
+      const text = row.status === "0" ? "启用" : "停用";
+      this.$confirm('确认"' + text + '"模板"' + row.jobName + '"?', "警告", { type: "warning" }).then(() => {
+        return updateTemplate({ templateId: row.templateId, status: row.status });
+      }).then(() => { this.msgSuccess(text + "成功"); }).catch(() => { row.status = row.status === "0" ? "1" : "0"; });
     },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.resetForm("queryForm");
-      this.handleQuery();
+    handleShowCron() { this.expression = this.form.cronExpression; this.openCron = true; },
+    crontabFill(value) { this.form.cronExpression = value; },
+    handleJobLog() {
+      const logPath = this.$route.path.startsWith('/admin/') ? '/admin/shezhi/jobLog' : '/monitor/jobLog';
+      this.$router.push({ path: logPath });
     },
-    // 多选框选中数据
-    handleSelectionChange(selection) {
-      this.ids = selection.map(item => item.jobId);
-      this.single = selection.length != 1;
-      this.multiple = !selection.length;
-    },
-    // 更多操作触发
-    handleCommand(command, row) {
-      switch (command) {
-        case "handleRun":
-          this.handleRun(row);
-          break;
-        case "handleView":
-          this.handleView(row);
-          break;
-        case "handleJobLog":
-          this.handleJobLog(row);
-          break;
-        default:
-          break;
-      }
-    },
-    // 任务状态修改
-    handleStatusChange(row) {
-      let text = row.status === "0" ? "启用" : "停用";
-      this.$confirm('确认要"' + text + '""' + row.jobName + '"任务吗?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return changeJobStatus(row.jobId, row.status);
-        }).then(() => {
-          this.msgSuccess(text + "成功");
-        }).catch(function() {
-          row.status = row.status === "0" ? "1" : "0";
-        });
-    },
-    /* 立即执行一次 */
-    handleRun(row) {
-      this.$confirm('确认要立即执行一次"' + row.jobName + '"任务吗?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return runJob(row.jobId, row.jobGroup);
-        }).then(() => {
-          this.msgSuccess("执行成功");
-        }).catch(() => {});
-    },
-    /** 任务详细信息 */
-    handleView(row) {
-      getJob(row.jobId).then(response => {
-        this.form = response.data;
-        this.openView = true;
-      });
+    // 表格行内"日志"按钮:跳转日志页并预填任务名称,便于快速查看该模板在各租户的执行记录(再在日志页选择租户)
+    handleRowJobLog(row) {
+      const logPath = this.$route.path.startsWith('/admin/') ? '/admin/shezhi/jobLog' : '/monitor/jobLog';
+      this.$router.push({ path: logPath, query: { jobName: row.jobName } });
     },
-    /** cron表达式按钮操作 */
-    handleShowCron() {
-      this.expression = this.form.cronExpression;
-      this.openCron = true;
+    // Bean 注册表
+    handleTaskRegistry() { this.openRegistry = true; },
+    initRegistry() { this.registryQueryParams.pageNum = 1; this.getRegistryList(); },
+    getRegistryList() {
+      this.registryLoading = true;
+      listTaskRegistry(this.registryQueryParams).then(res => { this.registryList = res.rows || []; this.registryTotal = res.total || 0; this.registryLoading = false; }).catch(() => { this.registryLoading = false; });
     },
-    /** 确定后回传值 */
-    crontabFill(value) {
-      this.form.cronExpression = value;
+    handleRegistryQuery() { this.registryQueryParams.pageNum = 1; this.getRegistryList(); },
+    resetRegistryQuery() { this.registryQueryParams = { pageNum: 1, pageSize: 10, beanName: undefined, methodName: undefined, invokeTarget: undefined }; this.getRegistryList(); },
+    applyInvokeTarget(row) {
+      if (!this.open) { this.handleAdd(); }
+      this.form.invokeTarget = row.invokeTarget;
+      this.openRegistry = false;
     },
-    /** 任务日志列表查询 */
-    handleJobLog(row) {
-      const jobId = row.jobId || 0;
-      this.$router.push({ path: '/monitor/job-log/index', query: { jobId: jobId } })
-    },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      this.open = true;
-      this.title = "添加任务";
-    },
-    /** 修改按钮操作 */
+    // 模板 CRUD
+    handleAdd() { this.reset(); this.open = true; this.title = "添加任务模板"; },
     handleUpdate(row) {
       this.reset();
-      const jobId = row.jobId || this.ids;
-      getJob(jobId).then(response => {
-        this.form = response.data;
-        this.open = true;
-        this.title = "修改任务";
-      });
+      const templateId = row.templateId || this.ids[0];
+      getTemplate(templateId).then(response => { this.form = response.data; this.open = true; this.title = "修改任务模板"; });
     },
-    /** 提交按钮 */
-    submitForm: function() {
+    submitForm() {
       this.$refs["form"].validate(valid => {
         if (valid) {
-          if (this.form.jobId != undefined) {
-            updateJob(this.form).then(response => {
-              this.msgSuccess("修改成功");
-              this.open = false;
-              this.getList();
-            });
+          if (this.form.templateId != undefined) {
+            updateTemplate(this.form).then(() => { this.msgSuccess("修改成功"); this.open = false; this.getList(); });
           } else {
-            addJob(this.form).then(response => {
-              this.msgSuccess("新增成功");
-              this.open = false;
-              this.getList();
-            });
+            addTemplate(this.form).then(() => { this.msgSuccess("新增成功"); this.open = false; this.getList(); });
           }
         }
       });
     },
-    /** 删除按钮操作 */
     handleDelete(row) {
-      const jobIds = row.jobId || this.ids;
-      this.$confirm('是否确认删除定时任务编号为"' + jobIds + '"的数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return delJob(jobIds);
-        }).then(() => {
-          this.getList();
-          this.msgSuccess("删除成功");
-        }).catch(() => {});
+      const templateIds = row.templateId || this.ids.join(',');
+      this.$confirm('确认删除模板编号为"' + templateIds + '"的数据?', "警告", { type: "warning" }).then(() => {
+        return delTemplate(templateIds);
+      }).then(() => { this.getList(); this.msgSuccess("删除成功"); }).catch(() => {});
+    },
+    // 租户任务配置
+    handleTenantJobConfig() { this.openTenantJob = true; },
+    loadTenantJobDialog() {
+      this.tenantJobLoading = true;
+      tenantList({}).then(tenantsRes => { this.tenantOptions = tenantsRes.data || tenantsRes.rows || []; this.tenantJobLoading = false; if (this.tenantJobForm.tenantId) { this.loadTenantJobSelection(); } }).catch(() => { this.tenantJobLoading = false; });
+    },
+    loadTenantJobSelection() {
+      if (!this.tenantJobForm.tenantId) return;
+      this.tenantJobLoading = true;
+      getTenantJobConfig(this.tenantJobForm.tenantId).then(res => { this.tenantJobConfigList = res.data || []; this.tenantJobLoading = false; }).catch(() => { this.tenantJobLoading = false; });
+    },
+    handleTenantJobStatusChange(row) {
+      const text = row.status === "0" ? "启用" : "停用";
+      this.$confirm('确认"' + text + '"该任务?同步后生效', "提示", { type: "warning" }).then(() => {
+        return updateTenantJobStatus(row.id, row.status);
+      }).then(() => { this.msgSuccess(text + "成功(同步后生效)"); }).catch(() => { row.status = row.status === "0" ? "1" : "0"; });
+    },
+    doSyncTenantJob() {
+      if (!this.tenantJobForm.tenantId) return;
+      this.$confirm("确认将模板同步到该租户库?", "提示", { type: "warning" }).then(() => {
+        return syncTenantJob(this.tenantJobForm.tenantId);
+      }).then(res => { this.msgSuccess("同步完成,写入 " + (res.data.synced || 0) + " 条任务"); this.loadTenantJobSelection(); }).catch(() => {});
     },
-    /** 导出按钮操作 */
-    handleExport() {
-      const queryParams = this.queryParams;
-      this.$confirm("是否确认导出所有定时任务数据项?", "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.exportLoading = true;
-          return exportJob(queryParams);
-        }).then(response => {
-          this.download(response.msg);
-          this.exportLoading = false;
-        }).catch(() => {});
+    doSyncAllTenantJob() {
+      this.$confirm("确认将全部租户的任务配置同步到各租户库?", "提示", { type: "warning" }).then(() => {
+        return syncAllTenantJob();
+      }).then(res => { const d = res.data || {}; this.msgSuccess("同步完成:成功 " + (d.successCount || 0) + ",失败 " + (d.failCount || 0)); }).catch(() => {});
     }
   }
 };

+ 51 - 15
src/views/monitor/job/log.vue

@@ -1,6 +1,24 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <!-- SaaS 平台专用:选择租户后,后端会临时切换数据源到该租户库查询其 sys_job_log(租户任务的执行日志实际落在各自租户库) -->
+      <el-form-item label="租户" prop="tenantId">
+        <el-select
+          v-model="queryParams.tenantId"
+          placeholder="全部(主库/平台日志)"
+          clearable
+          size="small"
+          style="width: 240px"
+          @change="handleQuery"
+        >
+          <el-option
+            v-for="t in tenantOptions"
+            :key="t.id"
+            :label="t.tenantCode + ' (ID:' + t.id + ')'"
+            :value="t.id"
+          />
+        </el-select>
+      </el-form-item>
       <el-form-item label="任务名称" prop="jobName">
         <el-input
           v-model="queryParams.jobName"
@@ -185,8 +203,8 @@
 </template>
 
 <script>
-import { getJob} from "@/api/monitor/job";
 import { listJobLog, delJobLog, exportJobLog, cleanJobLog } from "@/api/monitor/jobLog";
+import { tenantList } from "@/api/tenant/tenant";
 
 export default {
   name: "JobLog",
@@ -216,27 +234,40 @@ export default {
       statusOptions: [],
       // 任务组名字典
       jobGroupOptions: [],
+      // 租户选项(用于平台后台跨租户查看各租户的调度执行日志)
+      tenantOptions: [],
       // 查询参数
       queryParams: {
         pageNum: 1,
         pageSize: 10,
         jobName: undefined,
         jobGroup: undefined,
-        status: undefined
+        status: undefined,
+        tenantId: undefined   // SaaS 平台专用:选择后后端会切换到该租户数据源查询 sys_job_log
       }
     };
   },
   created() {
-    const jobId = this.$route.query.jobId;
-    if (jobId !== undefined && jobId != 0) {
-      getJob(jobId).then(response => {
-        this.queryParams.jobName = response.data.jobName;
-        this.queryParams.jobGroup = response.data.jobGroup;
-        this.getList();
-      });
-    } else {
-      this.getList();
+    // 支持从“定时任务”页面跳转时携带 ?jobName=xxx (或 jobGroup) 自动筛选对应任务的日志
+    // 即使当前“任务日志”按钮未传参,未来若加 per-row “日志”按钮可直接 push({path:'/jobLog', query:{jobName: row.jobName}})
+    const q = this.$route.query || {};
+    if (q.jobName) {
+      this.queryParams.jobName = q.jobName;
+    }
+    if (q.jobGroup) {
+      this.queryParams.jobGroup = q.jobGroup;
     }
+    if (q.tenantId) {
+      this.queryParams.tenantId = Number(q.tenantId);
+    }
+
+    // 加载活跃租户列表(用于平台管理员筛选查看不同租户的定时任务执行日志)
+    // 注意:只有平台总后台(fs-admin)有权限看到全部租户;租户自身后台通常只看到本租户日志(走自身 ds)
+    tenantList({ status: 1 }).then(response => {
+      this.tenantOptions = response.rows || response.data || [];
+    }).catch(() => {});
+
+    this.getList();
     this.getDicts("sys_common_status").then(response => {
       this.statusOptions = response.data;
     });
@@ -258,7 +289,8 @@ export default {
     // 返回按钮
     handleClose() {
       this.$store.dispatch("tagsView/delView", this.$route);
-      this.$router.push({ path: "/monitor/job" });
+      const backPath = this.$route.path.startsWith('/admin/') ? '/admin/shezhi/job' : '/monitor/job';
+      this.$router.push({ path: backPath });
     },
     /** 搜索按钮操作 */
     handleQuery() {
@@ -268,6 +300,7 @@ export default {
     /** 重置按钮操作 */
     resetQuery() {
       this.dateRange = [];
+      this.queryParams.tenantId = undefined;
       this.resetForm("queryForm");
       this.handleQuery();
     },
@@ -284,12 +317,13 @@ export default {
     /** 删除按钮操作 */
     handleDelete(row) {
       const jobLogIds = this.ids;
+      const tenantId = this.queryParams.tenantId;
       this.$confirm('是否确认删除调度日志编号为"' + jobLogIds + '"的数据项?', "警告", {
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(function() {
-          return delJobLog(jobLogIds);
+          return delJobLog(jobLogIds, tenantId);
         }).then(() => {
           this.getList();
           this.msgSuccess("删除成功");
@@ -297,12 +331,14 @@ export default {
     },
     /** 清空按钮操作 */
     handleClean() {
-      this.$confirm("是否确认清空所有调度日志数据项?", "警告", {
+      const tenantId = this.queryParams.tenantId;
+      const tip = tenantId ? ("是否确认清空租户ID=" + tenantId + " 的所有调度日志数据项?") : "是否确认清空当前数据源的所有调度日志数据项?";
+      this.$confirm(tip, "警告", {
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(function() {
-          return cleanJobLog();
+          return cleanJobLog(tenantId);
         }).then(() => {
           this.getList();
           this.msgSuccess("清空成功");

+ 12 - 1
src/views/saas/tenantMenu/index.vue

@@ -20,6 +20,16 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="显示状态" prop="visible">
+        <el-select v-model="queryParams.visible" placeholder="显示状态" clearable size="small">
+          <el-option
+            v-for="dict in visibleOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </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>
@@ -314,7 +324,8 @@ export default {
       // 查询参数
       queryParams: {
         menuName: undefined,
-        visible: undefined
+        visible: "0",
+        status: undefined
       },
       // 表单参数
       form: {},