Kaynağa Gözat

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

yuhongqi 18 saat önce
ebeveyn
işleme
5e8e0b66ff

+ 7 - 3
src/api/company/companyVoiceApi.js

@@ -172,12 +172,16 @@ export function getVoiceApiListV2() {
   })
 }
 
-/** 分配接口给租户(新版) */
-export function assignTenantsV2(apiId, tenantIds) {
+/** 分配接口给租户(新版,可选 tenants 携带定价字段) */
+export function assignTenantsV2(apiId, tenantIds, extra = {}) {
   return request({
     url: '/admin/companyVoiceApiTenant/assign',
     method: 'post',
-    data: { apiId, tenantIds }
+    data: {
+      apiId,
+      tenantIds,
+      tenants: extra.tenants
+    }
   })
 }
 

+ 14 - 24
src/api/his/redPacketConfig.js

@@ -3,26 +3,16 @@ import request from '@/utils/request'
 // 查询多商户配置列表
 export function listMore(query) {
   return request({
-    url: '/store/redPacket/more/list',
+    url: '/redPacket/more/list',
     method: 'get',
     params: query
   })
 }
 
-
-export function getRedPacketMchId(query) {
-  return request({
-    url: '/store/redPacket/more/getRedPacketConfig',
-    method: 'get',
-    params: query
-  })
-}
-
-
 // 查询多商户配置详细
 export function getMore(id) {
   return request({
-    url: '/store/redPacket/more/' + id,
+    url: '/redPacket/more/' + id,
     method: 'get'
   })
 }
@@ -30,7 +20,7 @@ export function getMore(id) {
 // 新增多商户配置
 export function addMore(data) {
   return request({
-    url: '/store/redPacket/more',
+    url: '/redPacket/more',
     method: 'post',
     data: data
   })
@@ -39,24 +29,16 @@ export function addMore(data) {
 // 修改多商户配置
 export function updateMore(data) {
   return request({
-    url: '/store/redPacket/more',
+    url: '/redPacket/more',
     method: 'put',
     data: data
   })
 }
 
-export function updateChangeMchId(data) {
-  return request({
-    url: '/store/redPacket/more/updateChangeMchId',
-    method: 'post',
-    data: data
-  })
-}
-
 // 删除多商户配置
 export function delMore(id) {
   return request({
-    url: '/store/redPacket/more/' + id,
+    url: '/redPacket/more/' + id,
     method: 'delete'
   })
 }
@@ -64,8 +46,16 @@ export function delMore(id) {
 // 导出多商户配置
 export function exportMore(query) {
   return request({
-    url: '/store/redPacket/more/export',
+    url: '/redPacket/more/export',
     method: 'get',
     params: query
   })
 }
+
+// 获取租户列表
+export function getTenantList() {
+  return request({
+    url: '/redPacket/more/tenantList',
+    method: 'get'
+  })
+}

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

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

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

@@ -44,6 +44,14 @@ export function syncAllTenantJob() {
   })
 }
 
+export function listTenantJobConfig(query) {
+  return request({
+    url: '/monitor/tenantJob/config/list',
+    method: 'get',
+    params: query
+  })
+}
+
 export function updateTenantJobStatus(configId, status) {
   return request({
     url: '/monitor/tenantJob/config/status',

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

@@ -77,6 +77,15 @@ export function addSmsApiTenant(data) {
   })
 }
 
+// 批量新增租户绑定
+export function batchAddSmsApiTenant(data) {
+  return request({
+    url: '/admin/smsApiTenant/batch',
+    method: 'post',
+    data: data
+  })
+}
+
 // 修改租户绑定
 export function updateSmsApiTenant(data) {
   return request({
@@ -93,3 +102,21 @@ export function delSmsApiTenant(id) {
     method: 'delete'
   })
 }
+
+// 批量更新租户定价
+export function batchUpdateSmsApiTenantPricing(data) {
+  return request({
+    url: '/admin/smsApiTenant/batchPricing',
+    method: 'put',
+    data: data
+  })
+}
+
+// 批量更新租户状态
+export function batchUpdateSmsApiTenantStatus(data) {
+  return request({
+    url: '/admin/smsApiTenant/batchStatus',
+    method: 'put',
+    data: data
+  })
+}

+ 13 - 8
src/views/admin/smsApi/index.vue

@@ -321,11 +321,7 @@ export default {
       portDialogTitle: '',
       currentApiId: null,
       currentApiProvider: '',
-      smsTypeOptions: [
-        { value: 1, label: '行业验证码通知短信' },
-        { value: 2, label: '营销短信' },
-        { value: 3, label: '5G消息' }
-      ],
+      smsTypeOptions: [],
       queryParams: { apiName: undefined, smsType: undefined, provider: undefined },
       form: {},
       rules: {
@@ -349,12 +345,21 @@ export default {
     }
   },
   created() {
+    this.loadSmsTypeOptions()
     this.getList()
   },
   methods: {
+    loadSmsTypeOptions() {
+      this.getDicts('sys_company_sms_temp_type').then(response => {
+        this.smsTypeOptions = (response.data || []).map(item => ({
+          value: Number(item.dictValue),
+          label: item.dictLabel
+        }))
+      })
+    },
     smsTypeFormat(type) {
-      const map = { 1: '行业验证码通知', 2: '营销短信', 3: '5G消息' }
-      return map[type] || '未知'
+      const item = this.smsTypeOptions.find(o => o.value === Number(type))
+      return item ? item.label : '未知'
     },
     providerFormat(p) {
       const map = { my: '迈远', card: '手机卡' }
@@ -373,7 +378,7 @@ export default {
     },
     resetForm() {
       this.form = {
-        apiId: undefined, apiName: '', smsType: 1, provider: 'my',
+        apiId: undefined, apiName: '', smsType: undefined, provider: 'my',
         account: '', password: '', url: '', code: '', sign: '',
         costPrice: 0, isDefault: 0, status: 1, remark: ''
       }

+ 262 - 39
src/views/admin/smsApiTenant/index.vue

@@ -3,7 +3,7 @@
     <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
       <el-form-item label="租户" prop="tenantId">
         <el-select v-model="queryParams.tenantId" placeholder="选择租户" clearable filterable>
-          <el-option v-for="c in companyList" :key="c.companyId" :label="c.companyName" :value="c.companyId" />
+          <el-option v-for="c in companyList" :key="c.tenantId" :label="c.tenantName" :value="c.tenantId" />
         </el-select>
       </el-form-item>
       <el-form-item label="短信类型" prop="smsType">
@@ -21,11 +21,24 @@
       <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="bindList" border size="small">
+    <el-table v-loading="loading" :data="bindList" border size="small" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="45" align="center" />
       <el-table-column label="ID" align="center" prop="id" width="60" />
-      <el-table-column label="租户名称" align="center" prop="companyName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="租户名称" align="center" prop="tenantName" min-width="140" show-overflow-tooltip />
       <el-table-column label="接口名称" align="center" prop="apiName" min-width="140" show-overflow-tooltip />
       <el-table-column label="短信类型" align="center" prop="smsType" width="140">
         <template slot-scope="scope">
@@ -75,19 +88,44 @@
       </el-table-column>
     </el-table>
 
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
     <!-- 新增绑定弹窗 -->
-    <el-dialog title="新增短信接口-租户绑定" :visible.sync="addDialogVisible" width="500px" append-to-body>
+    <el-dialog title="新增短信接口-租户绑定" :visible.sync="addDialogVisible" width="560px" append-to-body>
       <el-form ref="addForm" :model="addForm" :rules="addRules" label-width="100px" size="small">
-        <el-form-item label="选择接口" prop="apiId">
-          <el-select v-model="addForm.apiId" placeholder="请选择短信接口" style="width:100%" @change="onApiChange">
+        <el-form-item label="选择接口" prop="apiIds">
+          <el-select
+            v-model="addForm.apiIds"
+            multiple
+            collapse-tags
+            placeholder="请选择短信接口(可多选)"
+            style="width:100%"
+            @change="onApiChange"
+          >
             <el-option v-for="api in apiOptions" :key="api.apiId" :label="api.apiName + ' (' + smsTypeFormat(api.smsType) + ')'" :value="api.apiId" />
           </el-select>
         </el-form-item>
-        <el-form-item label="选择租户" prop="tenantId">
-          <el-select v-model="addForm.tenantId" placeholder="请选择租户" filterable style="width:100%">
-            <el-option v-for="c in companyList" :key="c.tenantId" :label="c.companyName" :value="c.tenantId" />
+        <el-form-item label="选择租户" prop="tenantIds">
+          <el-select
+            v-model="addForm.tenantIds"
+            multiple
+            collapse-tags
+            filterable
+            placeholder="请选择租户(可多选)"
+            style="width:100%"
+          >
+            <el-option v-for="c in companyList" :key="c.tenantId" :label="c.tenantName" :value="c.tenantId" />
           </el-select>
         </el-form-item>
+        <el-form-item v-if="addBindPreviewCount > 0" label="">
+          <span class="add-bind-preview">将创建 {{ addBindPreviewCount }} 条绑定关系</span>
+        </el-form-item>
         <el-form-item label="租户售价" prop="price">
           <el-input-number v-model="addForm.price" :precision="4" :step="0.001" :min="0" style="width:100%" />
           <span style="color:#909399;font-size:12px;margin-left:8px">元/条</span>
@@ -100,13 +138,42 @@
           <el-switch v-model="addForm.allowManual" :active-value="1" :inactive-value="0" active-text="允许" inactive-text="否" />
           <span style="color:#909399;font-size:12px;margin-left:8px">是否允许销售手动选择此接口发送</span>
         </el-form-item>
-        <el-form-item v-if="selectedApiCost" label="">
-          <span style="color:#909399;font-size:12px">该接口成本价: {{ selectedApiCost }} 元/条,建议售价 >= 成本价</span>
+        <el-form-item v-if="selectedApiCostText" label="">
+          <span style="color:#909399;font-size:12px">所选接口成本价: {{ selectedApiCostText }} 元/条,建议售价 >= 成本价</span>
         </el-form-item>
       </el-form>
       <div slot="footer">
-        <el-button type="primary" @click="submitAdd">确 定</el-button>
-        <el-button @click="addDialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="addSubmitting" :disabled="addSubmitting" @click="submitAdd">确 定</el-button>
+        <el-button :disabled="addSubmitting" @click="addDialogVisible = 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.price" :precision="4" :min="0" :step="0.001" style="width:100%" />
+        </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.applyAllowManual">更新手动选择</el-checkbox>
+          <el-switch
+            v-model="batchPricingForm.allowManual"
+            :disabled="!batchPricingForm.applyAllowManual"
+            :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" :loading="batchPricingSubmitting" @click="submitBatchPricing">确 定</el-button>
+        <el-button :disabled="batchPricingSubmitting" @click="batchPricingOpen = false">取 消</el-button>
       </div>
     </el-dialog>
 
@@ -114,7 +181,7 @@
     <el-dialog title="调整租户售价" :visible.sync="editDialogVisible" width="450px" append-to-body>
       <el-form ref="editForm" :model="editForm" label-width="100px" size="small">
         <el-form-item label="租户">
-          <span>{{ editForm.companyName }}</span>
+          <span>{{ editForm.tenantName }}</span>
         </el-form-item>
         <el-form-item label="接口">
           <span>{{ editForm.apiName }}</span>
@@ -145,7 +212,10 @@
 </template>
 
 <script>
-import { listSmsApiTenant, addSmsApiTenant, updateSmsApiTenant, delSmsApiTenant, listSmsApi } from '@/api/system/smsApi'
+import {
+  listSmsApiTenant, batchAddSmsApiTenant, updateSmsApiTenant, delSmsApiTenant, listSmsApi,
+  batchUpdateSmsApiTenantPricing, batchUpdateSmsApiTenantStatus
+} from '@/api/system/smsApi'
 import { listAllCompanies } from '@/api/admin/sysCompany'
 
 export default {
@@ -153,7 +223,9 @@ export default {
   data() {
     return {
       loading: false,
+      total: 0,
       bindList: [],
+      selectedRows: [],
       companyList: [],
       apiOptions: [],
       smsTypeOptions: [
@@ -161,22 +233,43 @@ export default {
         { value: 2, label: '营销短信' },
         { value: 3, label: '5G消息' }
       ],
-      queryParams: { tenantId: undefined, smsType: undefined },
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        tenantId: undefined,
+        smsType: undefined
+      },
       addDialogVisible: false,
+      addSubmitting: false,
       editDialogVisible: false,
-      addForm: { apiId: undefined, tenantId: undefined, price: 0, priority: 1, allowManual: 0 },
-      editForm: { id: undefined, price: 0, priority: 1, allowManual: 0, status: 1, companyName: '', apiName: '', costPrice: 0 },
+      batchPricingOpen: false,
+      batchPricingSubmitting: false,
+      batchPricingForm: {
+        price: undefined,
+        priority: undefined,
+        applyAllowManual: false,
+        allowManual: 0
+      },
+      addForm: { apiIds: [], tenantIds: [], price: 0, priority: 1, allowManual: 0 },
+      editForm: { id: undefined, price: 0, priority: 1, allowManual: 0, status: 1, tenantName: '', apiName: '', costPrice: 0 },
       addRules: {
-        apiId: [{ required: true, message: '请选择接口', trigger: 'change' }],
-        tenantId: [{ required: true, message: '请选择租户', trigger: 'change' }],
+        apiIds: [{ type: 'array', required: true, min: 1, message: '请选择至少一个接口', trigger: 'change' }],
+        tenantIds: [{ type: 'array', required: true, min: 1, message: '请选择至少一个租户', trigger: 'change' }],
         price: [{ required: true, message: '请填写售价', trigger: 'blur' }]
       }
     }
   },
   computed: {
-    selectedApiCost() {
-      const api = this.apiOptions.find(a => a.apiId === this.addForm.apiId)
-      return api && api.costPrice ? api.costPrice : ''
+    addBindPreviewCount() {
+      const apiCount = (this.addForm.apiIds || []).length
+      const tenantCount = (this.addForm.tenantIds || []).length
+      return apiCount * tenantCount
+    },
+    selectedApiCostText() {
+      const apiIds = this.addForm.apiIds || []
+      if (apiIds.length !== 1) return ''
+      const api = this.apiOptions.find(a => a.apiId === apiIds[0])
+      return api && api.costPrice != null ? api.costPrice : ''
     }
   },
   created() {
@@ -202,12 +295,16 @@ export default {
     getList() {
       this.loading = true
       listSmsApiTenant(this.queryParams).then(res => {
-        this.bindList = res.data || []
+        this.bindList = res.rows || res.data || []
+        this.total = res.total || 0
       }).finally(() => { this.loading = false })
     },
     loadCompanyList() {
       listAllCompanies({ pageNum: 1, pageSize: 1000 }).then(res => {
-        this.companyList = res.rows || res.data || []
+        this.companyList = (res.rows || res.data || []).map(c => ({
+          tenantId: c.id || c.companyId || c.tenantId,
+          tenantName: c.tenantName || c.companyName
+        }))
       })
     },
     loadApiOptions() {
@@ -215,29 +312,72 @@ export default {
         this.apiOptions = res.data || []
       })
     },
-    onApiChange(apiId) {
-      // 当选择接口时,自动填充默认售价
-      const api = this.apiOptions.find(a => a.apiId === apiId)
-      if (api && api.costPrice) {
-        this.addForm.price = api.costPrice
+    onApiChange(apiIds) {
+      const ids = apiIds || this.addForm.apiIds || []
+      if (ids.length === 1) {
+        const api = this.apiOptions.find(a => a.apiId === ids[0])
+        if (api && api.costPrice != null) {
+          this.addForm.price = api.costPrice
+        }
       }
     },
-    handleQuery() { this.getList() },
+    handleSelectionChange(rows) {
+      this.selectedRows = rows || []
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
     resetQuery() {
-      this.queryParams = { tenantId: undefined, smsType: undefined }
+      this.resetForm('queryForm')
+      this.queryParams.pageNum = 1
+      this.queryParams.pageSize = 10
       this.handleQuery()
     },
     handleAdd() {
-      this.addForm = { apiId: undefined, tenantId: undefined, price: 0, priority: 1, allowManual: 0 }
+      this.addForm = { apiIds: [], tenantIds: [], price: 0, priority: 1, allowManual: 0 }
+      this.addSubmitting = false
       this.addDialogVisible = true
+      this.$nextTick(() => {
+        if (this.$refs.addForm) this.$refs.addForm.clearValidate()
+      })
+    },
+    showBatchAddResult(res) {
+      const data = res.data || {}
+      const successCount = data.successCount || 0
+      const failCount = data.failCount || 0
+      const failMessages = data.failMessages || []
+      if (failCount > 0 && failMessages.length) {
+        const detail = failMessages.map((msg, index) => `${index + 1}. ${msg}`).join('<br/>')
+        this.$alert(detail, `失败 ${failCount} 条`, {
+          dangerouslyUseHTMLString: true,
+          customClass: 'batch-add-fail-alert'
+        })
+      }
+      if (successCount > 0) {
+        this.$message.success(res.msg || `成功绑定 ${successCount} 条`)
+        this.addDialogVisible = false
+        this.getList()
+      } else if (failCount > 0) {
+        this.$message.warning(res.msg || `绑定失败 ${failCount} 条`)
+      }
     },
     submitAdd() {
+      if (this.addSubmitting) return
       this.$refs.addForm.validate(valid => {
         if (!valid) return
-        addSmsApiTenant({ ...this.addForm, status: 1 }).then(() => {
-          this.$message.success('绑定成功')
-          this.addDialogVisible = false
-          this.getList()
+        this.addSubmitting = true
+        batchAddSmsApiTenant({
+          apiIds: this.addForm.apiIds,
+          tenantIds: this.addForm.tenantIds,
+          price: this.addForm.price,
+          priority: this.addForm.priority,
+          allowManual: this.addForm.allowManual,
+          status: 1
+        }).then(res => {
+          this.showBatchAddResult(res)
+        }).finally(() => {
+          this.addSubmitting = false
         })
       })
     },
@@ -248,7 +388,7 @@ export default {
         priority: row.priority || 1,
         allowManual: row.allowManual || 0,
         status: row.status,
-        companyName: row.companyName,
+        tenantName: row.tenantName,
         apiName: row.apiName,
         costPrice: row.costPrice
       }
@@ -261,8 +401,64 @@ export default {
         this.getList()
       })
     },
+    handleBatchPricing() {
+      if (!this.selectedRows.length) {
+        this.$message.warning('请先选择要批量定价的记录')
+        return
+      }
+      this.batchPricingForm = {
+        price: undefined,
+        priority: undefined,
+        applyAllowManual: false,
+        allowManual: 0
+      }
+      this.batchPricingOpen = true
+    },
+    submitBatchPricing() {
+      const data = { ids: this.selectedRows.map(row => row.id) }
+      if (this.batchPricingForm.price != null && this.batchPricingForm.price !== '') {
+        data.price = this.batchPricingForm.price
+      }
+      if (this.batchPricingForm.priority != null && this.batchPricingForm.priority !== '') {
+        data.priority = this.batchPricingForm.priority
+      }
+      if (this.batchPricingForm.applyAllowManual) {
+        data.allowManual = this.batchPricingForm.allowManual
+      }
+      if (data.price == null && data.priority == null && data.allowManual == null) {
+        this.$message.warning('请至少填写或勾选一项要批量更新的配置')
+        return
+      }
+      if (this.batchPricingSubmitting) return
+      this.batchPricingSubmitting = true
+      batchUpdateSmsApiTenantPricing(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 batchUpdateSmsApiTenantStatus({
+          ids: this.selectedRows.map(row => row.id),
+          status
+        })
+      }).then(() => {
+        this.$message.success('批量' + actionText + '成功')
+        this.getList()
+      }).catch(() => {})
+    },
     handleDelete(row) {
-      this.$confirm('确认解除租户"' + row.companyName + '"与接口"' + row.apiName + '"的绑定?', '提示', {
+      this.$confirm('确认解除租户"' + row.tenantName + '"与接口"' + row.apiName + '"的绑定?', '提示', {
         confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
       }).then(() => {
         delSmsApiTenant(row.id).then(() => {
@@ -276,4 +472,31 @@ export default {
 </script>
 
 <style scoped>
+.mb8 { margin-bottom: 8px; }
+.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;
+}
+.add-bind-preview {
+  color: #409eff;
+  font-size: 13px;
+}
+</style>
+
+<style>
+.batch-add-fail-alert .el-message-box__message {
+  max-height: 320px;
+  overflow-y: auto;
+  word-break: break-all;
+}
 </style>

+ 160 - 12
src/views/admin/voiceApi/index.vue

@@ -184,6 +184,7 @@
         <div class="assign-tenant-add">
           <el-divider content-position="left">添加租户</el-divider>
           <el-select
+            ref="assignTenantSelect"
             v-model="selectedTenantIds"
             multiple
             filterable
@@ -191,13 +192,14 @@
             reserve-keyword
             collapse-tags
             popper-append-to-body
-            popper-class="assign-tenant-select-popper"
+            :popper-class="assignTenantPopperClass"
             placeholder="输入租户名称搜索"
             :remote-method="searchTenants"
             :loading="tenantSearchLoading"
             :disabled="!!unassigningTenantId"
             style="width:100%"
             size="small"
+            @visible-change="handleAssignTenantDropdownVisible"
           >
             <el-option
               v-for="item in tenantOptions"
@@ -205,6 +207,22 @@
               :label="formatTenantOption(item)"
               :value="item.tenantId"
             />
+            <el-option
+              v-if="tenantLoadingMore"
+              key="assign-tenant-loading"
+              disabled
+              class="assign-tenant-status-option"
+            >
+              <span class="assign-tenant-status-text"><i class="el-icon-loading" /> 加载中...</span>
+            </el-option>
+            <el-option
+              v-if="showAssignTenantNoMore"
+              key="assign-tenant-no-more"
+              disabled
+              class="assign-tenant-status-option"
+            >
+              <span class="assign-tenant-status-text">没有更多数据了</span>
+            </el-option>
           </el-select>
         </div>
       </div>
@@ -294,6 +312,16 @@ export default {
       selectedTenantIds: [],
       tenantOptions: [],
       tenantSearchLoading: false,
+      tenantLoadingMore: false,
+      hasMoreTenants: true,
+      tenantOptionsTotal: 0,
+      tenantQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        tenantName: ''
+      },
+      assignTenantScrollWrap: null,
+      assignTenantScrollHandler: null,
       // 查看租户列表
       tenantListOpen: false,
       viewTenants: [],
@@ -303,6 +331,22 @@ export default {
   created() {
     this.getList()
   },
+  computed: {
+    assignTenantPopperClass() {
+      const base = 'assign-tenant-select-popper'
+      if (!this.hasMoreTenants && this.tenantOptions.length > 0) {
+        return base + ' assign-tenant-select-ended'
+      }
+      return base
+    },
+    showAssignTenantNoMore() {
+      return !this.hasMoreTenants && this.tenantOptions.length > 0 &&
+        !this.tenantSearchLoading && !this.tenantLoadingMore
+    }
+  },
+  beforeDestroy() {
+    this.unbindAssignTenantSelectScrollGuard()
+  },
   methods: {
     getList() {
       this.loading = true
@@ -434,26 +478,116 @@ export default {
       this.assignOpen = true
       this.selectedTenantIds = []
       this.tenantOptions = []
+      this.resetTenantSearchState()
       this.unassigningTenantId = null
       this.loadAssignedTenants()
     },
+    resetTenantSearchState() {
+      this.tenantQueryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        tenantName: ''
+      }
+      this.hasMoreTenants = true
+      this.tenantOptionsTotal = 0
+      this.tenantLoadingMore = false
+    },
+    mapCompanyToTenantOption(c) {
+      return {
+        tenantId: c.id || c.companyId,
+        tenantName: c.tenantName || c.companyName,
+        tenantCode: c.tenantCode || c.companyCode || null
+      }
+    },
+    fetchTenantOptions(isLoadMore = false) {
+      if (!isLoadMore) {
+        this.tenantSearchLoading = true
+      } else {
+        this.tenantLoadingMore = true
+      }
+      return listAllCompanies(this.tenantQueryParams).then(response => {
+        const rows = (response.rows || []).map(c => this.mapCompanyToTenantOption(c))
+        if (isLoadMore) {
+          const existIds = new Set(this.tenantOptions.map(o => o.tenantId))
+          const append = rows.filter(r => !existIds.has(r.tenantId))
+          this.tenantOptions = this.tenantOptions.concat(append)
+        } else {
+          this.tenantOptions = rows
+        }
+        this.tenantOptionsTotal = response.total || 0
+        this.hasMoreTenants = this.tenantOptions.length < this.tenantOptionsTotal
+      }).finally(() => {
+        this.tenantSearchLoading = false
+        this.tenantLoadingMore = false
+        this.$nextTick(() => {
+          if (this.assignOpen) {
+            this.bindAssignTenantSelectScrollGuard()
+          }
+        })
+      })
+    },
+    searchTenants(query) {
+      if (query === undefined) return
+      this.tenantQueryParams.tenantName = (query || '').trim()
+      this.tenantQueryParams.pageNum = 1
+      this.hasMoreTenants = true
+      this.fetchTenantOptions(false)
+    },
+    loadMoreAssignTenants() {
+      if (this.tenantLoadingMore || this.tenantSearchLoading || !this.hasMoreTenants) return
+      this.tenantQueryParams.pageNum += 1
+      this.fetchTenantOptions(true)
+    },
+    getAssignTenantDropdownWrap() {
+      const root = this.$refs.assignTenantSelect && this.$refs.assignTenantSelect.$el
+      if (!root) return null
+      const dropdown = document.querySelector('.assign-tenant-select-popper .el-select-dropdown__wrap')
+      return dropdown || root.querySelector('.el-select-dropdown__wrap')
+    },
+    bindAssignTenantSelectScrollGuard() {
+      this.unbindAssignTenantSelectScrollGuard()
+      const wrap = this.getAssignTenantDropdownWrap()
+      if (!wrap) return
+      this.assignTenantScrollWrap = wrap
+      this.assignTenantScrollHandler = () => {
+        const atBottom = wrap.scrollHeight - wrap.scrollTop <= wrap.clientHeight + 2
+        if (atBottom) {
+          this.loadMoreAssignTenants()
+        }
+        if (!this.hasMoreTenants) {
+          const maxScroll = wrap.scrollHeight - wrap.clientHeight
+          if (maxScroll > 0 && wrap.scrollTop > maxScroll) {
+            wrap.scrollTop = maxScroll
+          }
+        }
+      }
+      wrap.addEventListener('scroll', this.assignTenantScrollHandler)
+    },
+    unbindAssignTenantSelectScrollGuard() {
+      if (this.assignTenantScrollWrap && this.assignTenantScrollHandler) {
+        this.assignTenantScrollWrap.removeEventListener('scroll', this.assignTenantScrollHandler)
+      }
+      this.assignTenantScrollWrap = null
+      this.assignTenantScrollHandler = null
+    },
+    handleAssignTenantDropdownVisible(visible) {
+      if (visible) {
+        if (this.tenantOptions.length === 0) {
+          this.searchTenants('')
+        }
+        this.$nextTick(() => {
+          this.bindAssignTenantSelectScrollGuard()
+        })
+      } else {
+        this.unbindAssignTenantSelectScrollGuard()
+      }
+    },
     loadAssignedTenants() {
       this.assignLoading = true
       return getAssignedTenants(this.assignApi.apiId).then(response => {
         this.assignedTenants = response.data || []
       }).finally(() => { this.assignLoading = false })
     },
-    searchTenants(query) {
-      if (query.length < 1) return
-      this.tenantSearchLoading = true
-      listAllCompanies({ companyName: query, pageNum: 1, pageSize: 20 }).then(response => {
-        this.tenantOptions = (response.rows || []).map(c => ({
-          tenantId: c.id || c.companyId,
-          tenantName: c.tenantName || c.companyName,
-          tenantCode: c.tenantCode || c.companyCode || null
-        }))
-      }).finally(() => { this.tenantSearchLoading = false })
-    },
     submitAssign() {
       if (this.selectedTenantIds.length === 0) return
       this.assignSubmitting = true
@@ -548,6 +682,20 @@ export default {
 .assign-tenant-select-popper {
   z-index: 3000 !important;
 }
+.assign-tenant-select-popper .assign-tenant-status-option {
+  cursor: default;
+}
+.assign-tenant-select-popper .assign-tenant-status-text {
+  display: block;
+  text-align: center;
+  color: #909399;
+  font-size: 12px;
+  line-height: 1.5;
+  padding: 4px 0;
+}
+.assign-tenant-select-ended .el-select-dropdown__wrap {
+  overscroll-behavior: contain;
+}
 .assign-tenant-dialog .el-dialog__body {
   padding-bottom: 10px;
 }

+ 194 - 24
src/views/admin/voiceApiTenant/index.vue

@@ -3,9 +3,43 @@
     <el-card shadow="never" class="mb16 filter-card">
       <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
         <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
+            ref="filterTenantSelect"
+            v-model="queryParams.tenantId"
+            placeholder="输入租户名称搜索"
+            clearable
+            filterable
+            remote
+            reserve-keyword
+            popper-append-to-body
+            :popper-class="filterTenantPopperClass"
+            :remote-method="searchFilterTenants"
+            :loading="tenantSearchLoading"
+            style="width:200px"
+            @visible-change="handleFilterTenantDropdownVisible"
+          >
+            <el-option
+              v-for="c in tenantOptions"
+              :key="c.tenantId"
+              :label="formatTenantOption(c)"
+              :value="c.tenantId"
+            />
+            <el-option
+              v-if="tenantLoadingMore"
+              key="filter-tenant-loading"
+              disabled
+              class="filter-tenant-status-option"
+            >
+              <span class="filter-tenant-status-text"><i class="el-icon-loading" /> 加载中...</span>
+            </el-option>
+            <el-option
+              v-if="showFilterTenantNoMore"
+              key="filter-tenant-no-more"
+              disabled
+              class="filter-tenant-status-option"
+            >
+              <span class="filter-tenant-status-text">没有更多数据了</span>
+            </el-option>
           </el-select>
         </el-form-item>
         <el-form-item label="接口" prop="apiId">
@@ -256,6 +290,16 @@ export default {
       },
       tenantOptions: [],
       tenantSearchLoading: false,
+      tenantLoadingMore: false,
+      hasMoreTenants: true,
+      tenantOptionsTotal: 0,
+      tenantQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        tenantName: ''
+      },
+      filterTenantScrollWrap: null,
+      filterTenantScrollHandler: null,
       apiOptions: [],
       addDialogVisible: false,
       addSubmitting: false,
@@ -295,8 +339,22 @@ export default {
     selectedApiCost() {
       const api = this.apiOptions.find(a => a.apiId === this.addForm.apiId)
       return api && api.costPrice ? api.costPrice : ''
+    },
+    filterTenantPopperClass() {
+      const base = 'voice-api-tenant-filter-select-popper'
+      if (!this.hasMoreTenants && this.tenantOptions.length > 0) {
+        return base + ' voice-api-tenant-filter-select-ended'
+      }
+      return base
+    },
+    showFilterTenantNoMore() {
+      return !this.hasMoreTenants && this.tenantOptions.length > 0 &&
+        !this.tenantSearchLoading && !this.tenantLoadingMore
     }
   },
+  beforeDestroy() {
+    this.unbindFilterTenantSelectScrollGuard()
+  },
   created() {
     this.getList()
     this.loadApiOptions()
@@ -360,18 +418,16 @@ export default {
       this.$refs.addForm.validate(valid => {
         if (!valid) return
         this.addSubmitting = true
-        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,
-              tenantId: this.addForm.tenantId,
-              salePrice: this.addForm.salePrice,
-              priority: this.addForm.priority,
-              isPrimary: this.addForm.isPrimary,
-              selectable: this.addForm.selectable,
-              status: 1
-            }).catch(() => {})
-          }
+        const tenantId = this.addForm.tenantId
+        assignTenantsV2(this.addForm.apiId, [tenantId], {
+          tenants: [{
+            tenantId,
+            salePrice: this.addForm.salePrice,
+            priority: this.addForm.priority,
+            isPrimary: this.addForm.isPrimary,
+            selectable: this.addForm.selectable
+          }]
+        }).then(() => {
           this.$message.success('绑定成功')
           this.addDialogVisible = false
           this.getList()
@@ -388,15 +444,109 @@ export default {
         })
       }).catch(() => {})
     },
-    searchTenants(query) {
-      if (query.length < 1) { this.tenantOptions = []; return }
-      this.tenantSearchLoading = true
-      listAllCompanies({ companyName: query, pageNum: 1, pageSize: 20 }).then(response => {
-        this.tenantOptions = (response.rows || []).map(c => ({
-          tenantId: c.id || c.companyId,
-          tenantName: c.tenantName || c.companyName
-        }))
-      }).finally(() => { this.tenantSearchLoading = false })
+    formatTenantOption(item) {
+      if (item.tenantCode) {
+        return `${item.tenantName || '-'} (${item.tenantCode})`
+      }
+      return item.tenantName || String(item.tenantId)
+    },
+    resetFilterTenantSearchState() {
+      this.tenantQueryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        tenantName: ''
+      }
+      this.hasMoreTenants = true
+      this.tenantOptionsTotal = 0
+      this.tenantLoadingMore = false
+    },
+    mapCompanyToTenantOption(c) {
+      return {
+        tenantId: c.id || c.companyId,
+        tenantName: c.tenantName || c.companyName,
+        tenantCode: c.tenantCode || c.companyCode || null
+      }
+    },
+    fetchFilterTenantOptions(isLoadMore = false) {
+      if (!isLoadMore) {
+        this.tenantSearchLoading = true
+      } else {
+        this.tenantLoadingMore = true
+      }
+      return listAllCompanies(this.tenantQueryParams).then(response => {
+        const rows = (response.rows || []).map(c => this.mapCompanyToTenantOption(c))
+        if (isLoadMore) {
+          const existIds = new Set(this.tenantOptions.map(o => o.tenantId))
+          const append = rows.filter(r => !existIds.has(r.tenantId))
+          this.tenantOptions = this.tenantOptions.concat(append)
+        } else {
+          this.tenantOptions = rows
+        }
+        this.tenantOptionsTotal = response.total || 0
+        this.hasMoreTenants = this.tenantOptions.length < this.tenantOptionsTotal
+      }).finally(() => {
+        this.tenantSearchLoading = false
+        this.tenantLoadingMore = false
+        this.$nextTick(() => {
+          this.bindFilterTenantSelectScrollGuard()
+        })
+      })
+    },
+    searchFilterTenants(query) {
+      if (query === undefined) return
+      this.tenantQueryParams.tenantName = (query || '').trim()
+      this.tenantQueryParams.pageNum = 1
+      this.hasMoreTenants = true
+      this.fetchFilterTenantOptions(false)
+    },
+    loadMoreFilterTenants() {
+      if (this.tenantLoadingMore || this.tenantSearchLoading || !this.hasMoreTenants) return
+      this.tenantQueryParams.pageNum += 1
+      this.fetchFilterTenantOptions(true)
+    },
+    getFilterTenantDropdownWrap() {
+      const dropdown = document.querySelector('.voice-api-tenant-filter-select-popper .el-select-dropdown__wrap')
+      if (dropdown) return dropdown
+      const root = this.$refs.filterTenantSelect && this.$refs.filterTenantSelect.$el
+      return root ? root.querySelector('.el-select-dropdown__wrap') : null
+    },
+    bindFilterTenantSelectScrollGuard() {
+      this.unbindFilterTenantSelectScrollGuard()
+      const wrap = this.getFilterTenantDropdownWrap()
+      if (!wrap) return
+      this.filterTenantScrollWrap = wrap
+      this.filterTenantScrollHandler = () => {
+        const atBottom = wrap.scrollHeight - wrap.scrollTop <= wrap.clientHeight + 2
+        if (atBottom) {
+          this.loadMoreFilterTenants()
+        }
+        if (!this.hasMoreTenants) {
+          const maxScroll = wrap.scrollHeight - wrap.clientHeight
+          if (maxScroll > 0 && wrap.scrollTop > maxScroll) {
+            wrap.scrollTop = maxScroll
+          }
+        }
+      }
+      wrap.addEventListener('scroll', this.filterTenantScrollHandler)
+    },
+    unbindFilterTenantSelectScrollGuard() {
+      if (this.filterTenantScrollWrap && this.filterTenantScrollHandler) {
+        this.filterTenantScrollWrap.removeEventListener('scroll', this.filterTenantScrollHandler)
+      }
+      this.filterTenantScrollWrap = null
+      this.filterTenantScrollHandler = null
+    },
+    handleFilterTenantDropdownVisible(visible) {
+      if (visible) {
+        if (this.tenantOptions.length === 0) {
+          this.searchFilterTenants('')
+        }
+        this.$nextTick(() => {
+          this.bindFilterTenantSelectScrollGuard()
+        })
+      } else {
+        this.unbindFilterTenantSelectScrollGuard()
+      }
     },
     handleEditPricing(row) {
       this.pricingForm = {
@@ -530,3 +680,23 @@ export default {
   vertical-align: middle;
 }
 </style>
+
+<style>
+.voice-api-tenant-filter-select-popper {
+  z-index: 3000 !important;
+}
+.voice-api-tenant-filter-select-popper .filter-tenant-status-option {
+  cursor: default;
+}
+.voice-api-tenant-filter-select-popper .filter-tenant-status-text {
+  display: block;
+  text-align: center;
+  color: #909399;
+  font-size: 12px;
+  line-height: 1.5;
+  padding: 4px 0;
+}
+.voice-api-tenant-filter-select-ended .el-select-dropdown__wrap {
+  overscroll-behavior: contain;
+}
+</style>

+ 46 - 91
src/views/his/redPacketConfig/index.vue

@@ -68,25 +68,6 @@
           v-hasPermi="['redPacket:more:remove']"
         >删除</el-button>
       </el-col>
-      <el-col :span="1.5">
-        <el-tag
-          type="primary"
-          size="large"
-          style="font-size: 16px;"
-        >
-          当前使用的商户号:{{this.redPacketMchId}}
-        </el-tag>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="success"
-          plain
-          icon="el-icon-edit"
-          size="small"
-          @click="handleEditRedPacket"
-          v-hasPermi="['redPacket:more:editRedPacket']"
-        >修改当前发送的红包的商户号</el-button>
-      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
@@ -110,6 +91,12 @@
       <el-table-column label="pub_key.pem证书" align="center" prop="publicKeyPath" />
       <el-table-column label="回调地址" align="center" prop="notifyUrl" />
       <el-table-column label="回调地址" align="center" prop="notifyUrlScrm" />
+      <el-table-column label="分配租户" align="center" prop="tenantName" :show-overflow-tooltip="true">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.tenantId" type="success" size="small">{{ scope.row.tenantName || scope.row.tenantId }}</el-tag>
+          <el-tag v-else type="info" size="small">未分配</el-tag>
+        </template>
+      </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
@@ -183,31 +170,24 @@
         <el-form-item label="回调地址" prop="notifyUrlScrm">
           <el-input v-model="form.notifyUrlScrm" placeholder="请输入回调地址notifyUrlScrm" />
         </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-
-    <el-dialog :title="redPacketOpen.title" :visible.sync="redPacketOpen.open" width="600px" append-to-body>
-      <el-form ref="redPacketOpen" :model="redPacketOpen" label-width="110px">
-        <el-form-item label="商户号" prop="cateId">
-          <el-select v-model="redPacketOpen.newChangeMchId" placeholder="请选择" clearable size="small">
+        <el-form-item label="分配租户" prop="tenantId">
+          <el-select v-model="form.tenantId" placeholder="请选择要分配的租户(同步到租户库)" clearable style="width: 100%;">
             <el-option
-              v-for="dict in moreList"
-              :key="dict.mchId"
-              :label="dict.mchId"
-              :value="dict.mchId"
+              v-for="item in tenantList"
+              :key="item.id"
+              :label="item.tenantName + ' (' + item.tenantCode + ')'"
+              :value="item.id"
             />
           </el-select>
+          <div class="form-tip">选择租户后,该商户配置将同步到指定租户的数据库中</div>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitFormChangeMchId">确 定</el-button>
-        <el-button @click="cancelChangeMchId">取 消</el-button>
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
       </div>
     </el-dialog>
+
   </div>
 </template>
 
@@ -218,8 +198,7 @@ import {
   delMore,
   addMore,
   updateMore,
-  getRedPacketMchId,
-  updateChangeMchId
+  getTenantList
 } from '@/api/his/redPacketConfig'
 
 export default {
@@ -240,15 +219,10 @@ export default {
       showSearch: true,
       // 总条数
       total: 0,
-      redPacketMchId: null,
-      redPacketOpen:{
-        open:false,
-        title:null,
-        oldChangeMchId:null,
-        newChangeMchId:null,
-      },
       // 多商户配置表格数据
       moreList: [],
+      // 租户列表
+      tenantList: [],
       // 弹出层标题
       title: "",
       // 是否显示弹出层
@@ -275,13 +249,11 @@ export default {
       form: {},
       // 表单校验
       rules: {},
-      redPacketOpenRule:{
-        newChangeMchId:[{ required: true, trigger: "blur", message: "商户号不能为空" }]
-      },
     };
   },
   created() {
     this.getList();
+    this.loadTenantList();
   },
   methods: {
     /** 查询多商户配置列表 */
@@ -293,21 +265,12 @@ export default {
         this.total = response.total;
         this.loading = false;
       });
-      getRedPacketMchId().then(res=>{
-        this.redPacketMchId=res.data
-      })
     },
     // 取消按钮
     cancel() {
       this.open = false;
       this.reset();
     },
-    cancelChangeMchId() {
-      this.redPacketOpen.open=false;
-      this.redPacketOpen.title=false;
-      this.redPacketOpen.oldChangeMchId=false;
-      this.redPacketOpen.newChangeMchId=false;
-    },
     // 表单重置
     reset() {
       this.form = {
@@ -324,10 +287,17 @@ export default {
         publicKeyId: null,
         publicKeyPath: null,
         notifyUrl: null,
-        notifyUrlScrm: null
+        notifyUrlScrm: null,
+        tenantId: null
       };
       this.resetForm("form");
     },
+    // 加载租户列表
+    loadTenantList() {
+      getTenantList().then(res => {
+        this.tenantList = res.data || [];
+      });
+    },
     /** 搜索按钮操作 */
     handleQuery() {
       this.queryParams.pageNum = 1;
@@ -360,11 +330,6 @@ export default {
         this.title = "修改多商户配置";
       });
     },
-    handleEditRedPacket(){
-      this.redPacketOpen.open= true;
-      this.redPacketOpen.title="修改发送红包的商户号";
-
-    },
     /** 提交按钮 */
     submitForm() {
       this.$refs["form"].validate(valid => {
@@ -385,39 +350,29 @@ export default {
         }
       });
     },
-
-    /** 提交修改发送红包的商户号- */
-    submitFormChangeMchId() {
-      this.$refs["redPacketOpen"].validate(valid => {
-        if (valid) {
-            this.redPacketOpen.oldChangeMchId=this.redPacketMchId;
-            if (this.redPacketOpen.newChangeMchId==null){
-              return this.$message.error("修改的商户号不能为空")
-            }
-
-            updateChangeMchId(this.redPacketOpen).then(response => {
-              this.msgSuccess("修改成功");
-              this.cancelChangeMchId();
-              this.getList();
-            });
-
-        }
-      });
-    },
     /** 删除按钮操作 */
     handleDelete(row) {
       const ids = row.id || this.ids;
       this.$confirm('是否确认删除多商户配置编号为"' + ids + '"的数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return delMore(ids);
-        }).then(() => {
-          this.getList();
-          this.msgSuccess("删除成功");
-        }).catch(() => {});
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return delMore(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {});
     },
   }
 };
 </script>
+
+<style scoped>
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 5px;
+  line-height: 1.4;
+}
+</style>

+ 108 - 64
src/views/monitor/job/index.vue

@@ -35,12 +35,9 @@
       <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-col>
-      <el-col :span="1.5">
-        <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="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="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-col>
@@ -79,6 +76,7 @@
         <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-button size="mini" type="text" icon="el-icon-connection" @click="handleAssignTenant(scope.row)" v-hasPermi="['monitor:job:edit']">分配</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>
@@ -121,11 +119,7 @@
                   <i class="el-icon-question"></i>
                 </el-tooltip>
               </span>
-              <el-input v-model="form.invokeTarget" placeholder="请输入调用目标字符串">
-                <template slot="append">
-                  <el-button @click="openRegistry = true; initRegistry()">从注册表选择</el-button>
-                </template>
-              </el-input>
+              <el-input v-model="form.invokeTarget" placeholder="请输入调用目标字符串" />
             </el-form-item>
           </el-col>
           <el-col :span="24">
@@ -191,37 +185,6 @@
       <crontab @hide="openCron=false" @fill="crontabFill" :expression="expression"></crontab>
     </el-dialog>
 
-    <!-- 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>
-      <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">
@@ -256,13 +219,36 @@
       <el-empty v-if="tenantJobForm.tenantId && !tenantJobLoading && !tenantJobConfigList.length" description="暂未同步,请点击「同步到租户库」按钮" />
       <el-empty v-if="!tenantJobForm.tenantId" description="请先选择租户" />
     </el-dialog>
+
+    <!-- 分配租户弹窗 -->
+    <el-dialog :title="'分配租户 — ' + assignTemplate.jobName" :visible.sync="openAssignTenant" width="680px" append-to-body @open="loadAssignTenantDialog">
+      <el-form :inline="true" size="small">
+        <el-form-item>
+          <el-input v-model="assignTenantSearch" placeholder="搜索租户" clearable size="small" style="width: 220px" @input="handleAssignTenantSearch" />
+        </el-form-item>
+        <el-form-item>
+          <el-checkbox v-model="selectAllTenants" @change="handleSelectAllTenants">全选</el-checkbox>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" size="mini" :loading="assignTenantSaving" @click="submitAssignTenant">保存分配</el-button>
+        </el-form-item>
+      </el-form>
+      <el-checkbox-group v-model="selectedTenantIds" v-loading="assignTenantLoading" style="display:flex; flex-wrap:wrap; gap:4px 0;">
+        <el-checkbox
+          v-for="t in filteredAssignTenants"
+          :key="t.id"
+          :label="t.id"
+          style="width:50%; margin-right:0;"
+        >{{ t.tenantName + ' (' + t.tenantCode + ')' }}</el-checkbox>
+      </el-checkbox-group>
+      <el-empty v-if="!assignTenantLoading && !filteredAssignTenants.length" description="无可用租户" />
+    </el-dialog>
   </div>
 </template>
 
 <script>
 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 { getTenantJobConfig, updateTenantJobStatus, syncTenantJob, syncAllTenantJob, saveTenantJobConfig, listTenantJobConfig } from "@/api/monitor/tenantJob";
 import { tenantList } from "@/api/tenant/tenant";
 import Crontab from '@/components/Crontab'
 
@@ -282,18 +268,22 @@ export default {
       open: false,
       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 },
+      // 分配租户弹窗
+      openAssignTenant: false,
+      assignTenantLoading: false,
+      assignTenantSaving: false,
+      assignTemplate: {},
+      assignTenantSearch: '',
+      selectAllTenants: false,
+      selectedTenantIds: [],
+      previousSelectedTenantIds: [],
+      allTenants: [],
       // 字典
       jobGroupOptions: [],
       // 查询参数
@@ -350,20 +340,6 @@ export default {
       const logPath = this.$route.path.startsWith('/admin/') ? '/admin/shezhi/jobLog' : '/monitor/jobLog';
       this.$router.push({ path: logPath, query: { jobName: row.jobName } });
     },
-    // 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; });
-    },
-    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;
-    },
     // 模板 CRUD
     handleAdd() { this.reset(); this.open = true; this.title = "添加任务模板"; },
     handleUpdate(row) {
@@ -415,6 +391,74 @@ export default {
       this.$confirm("确认将全部租户的任务配置同步到各租户库?", "提示", { type: "warning" }).then(() => {
         return syncAllTenantJob();
       }).then(res => { const d = res.data || {}; this.msgSuccess("同步完成:成功 " + (d.successCount || 0) + ",失败 " + (d.failCount || 0)); }).catch(() => {});
+    },
+    // 分配租户
+    handleAssignTenant(row) {
+      this.assignTemplate = row
+      this.openAssignTenant = true
+    },
+    loadAssignTenantDialog() {
+      this.assignTenantLoading = true
+      this.assignTenantSearch = ''
+      this.selectAllTenants = false
+      this.selectedTenantIds = []
+      // 加载全部租户(与 tenantJobConfig 弹窗保持一致)
+      tenantList({}).then(res => {
+        this.allTenants = res.data || res.rows || []
+        this.assignTenantLoading = false
+        // 异步查询该模板已分配了哪些租户
+        listTenantJobConfig({ templateId: this.assignTemplate.templateId, status: 0, pageSize: 999 }).then(res2 => {
+          // 兼容多种响应格式:TableDataInfo { rows } / AjaxResult { data } / 直接数组
+          let rows = res2.rows || res2.data || (Array.isArray(res2) ? res2 : [])
+          console.log('分配租户 — 已分配查询结果 rows:', rows)
+          this.selectedTenantIds = rows.filter(r => String(r.status) === '0').map(r => Number(r.tenantId))
+          this.previousSelectedTenantIds = [...this.selectedTenantIds]
+          this.selectAllTenants = this.selectedTenantIds.length === this.allTenants.length && this.allTenants.length > 0
+        }).catch(() => {})
+      }).catch(() => { this.assignTenantLoading = false })
+    },
+    handleAssignTenantSearch() {},
+    handleSelectAllTenants(val) {
+      this.selectedTenantIds = val ? this.allTenants.map(t => t.id) : []
+    },
+    submitAssignTenant() {
+      this.assignTenantSaving = true
+      // 只处理选择状态发生变化的租户,避免覆盖其他模板的分配
+      const promises = []
+      const prevIds = this.previousSelectedTenantIds || []
+      this.allTenants.forEach(t => {
+        const isSelected = this.selectedTenantIds.includes(t.id)
+        const wasSelected = prevIds.includes(t.id)
+        if (isSelected === wasSelected) return  // 无变化,跳过
+        const p = getTenantJobConfig(t.id).then(res => {
+          const currentIds = (res.data || []).map(c => c.templateId)
+          const newIds = isSelected
+            ? [...new Set([...currentIds, this.assignTemplate.templateId])]
+            : currentIds.filter(id => id !== this.assignTemplate.templateId)
+          return saveTenantJobConfig({ tenantId: t.id, templateIds: newIds, status: '0' })
+        })
+        promises.push(p)
+      })
+      if (promises.length === 0) {
+        this.assignTenantSaving = false
+        this.openAssignTenant = false
+        return
+      }
+      Promise.all(promises).then(() => {
+        this.msgSuccess('分配成功')
+        this.assignTenantSaving = false
+        this.openAssignTenant = false
+      }).catch(() => { this.assignTenantSaving = false })
+    }
+  },
+  computed: {
+    filteredAssignTenants() {
+      if (!this.assignTenantSearch) return this.allTenants
+      const kw = this.assignTenantSearch.toLowerCase()
+      return this.allTenants.filter(t =>
+        (t.tenantName || '').toLowerCase().includes(kw) ||
+        (t.tenantCode || '').toLowerCase().includes(kw)
+      )
     }
   }
 };