lk 2 дней назад
Родитель
Сommit
26202fa359
2 измененных файлов с 385 добавлено и 0 удалено
  1. 384 0
      src/views/admin/aiFrontConfig/index.vue
  2. 1 0
      src/views/admin/menu.js

+ 384 - 0
src/views/admin/aiFrontConfig/index.vue

@@ -0,0 +1,384 @@
+<template>
+  <div class="app-container">
+    <el-card v-loading="loading" shadow="never">
+      <div slot="header" class="page-header">
+        <span>AI配置</span>
+        <el-form :inline="true" size="small" class="tenant-form">
+          <el-form-item label="选择租户">
+            <el-select
+              ref="tenantSelect"
+              v-model="selectedTenantId"
+              placeholder="请输入租户名称搜索"
+              filterable
+              remote
+              clearable
+              :remote-method="handleTenantSearch"
+              :loading="tenantSelectLoading"
+              style="width: 280px"
+              @visible-change="handleTenantDropdownVisible"
+              @clear="handleTenantSelectClear"
+              @change="loadConfig"
+            >
+              <el-option
+                v-for="item in tenantList"
+                :key="item.id"
+                :label="formatTenantLabel(item)"
+                :value="item.id"
+              />
+              <el-option v-if="hasMoreTenants" key="tenant-load-more" disabled class="tenant-load-more-option">
+                <div class="load-more" @click.stop="loadMoreTenants">
+                  <span>加载更多</span>
+                  <i v-if="tenantLoadingMore" class="el-icon-loading" />
+                </div>
+              </el-option>
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <el-alert
+        v-if="!selectedTenantId"
+        title="当前为系统默认配置(主库),保存后将更新全局配置;选择租户后可单独配置该租户参数"
+        type="info"
+        :closable="false"
+        show-icon
+        class="tenant-tip"
+      />
+
+      <el-alert
+        v-if="selectedTenantId && tenantConfigEmpty"
+        title="该租户尚未保存过此配置,可直接填写后保存;如需参考系统默认配置,可点击下方按钮加载(不会自动保存)。"
+        type="warning"
+        :closable="false"
+        show-icon
+        class="tenant-tip"
+      >
+        <el-button type="text" size="small" :loading="fallbackLoading" @click="loadGlobalConfigAsFallback">
+          加载系统默认配置
+        </el-button>
+      </el-alert>
+
+      <el-form ref="form" :model="form" label-width="200px">
+        <!-- iPad服务配置 -->
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>iPad服务配置</span></div>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="URL" prop="url">
+                <el-input v-model="form.ipad.url" placeholder="请输入URL地址" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="iPad URL" prop="ipadUrl">
+                <el-input v-model="form.ipad.ipadUrl" placeholder="请输入iPad URL地址" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="AI Api" prop="aiApi">
+                <el-input v-model="form.ipad.aiApi" placeholder="请输入AI接口地址" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="Voice Api" prop="voiceApi">
+                <el-input v-model="form.ipad.voiceApi" placeholder="请输入语音接口地址" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="Common Api" prop="commonApi">
+                <el-input v-model="form.ipad.commonApi" placeholder="请输入通用接口地址" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+      </el-form>
+
+      <div style="text-align: center; margin-top: 20px; padding-bottom: 20px;">
+        <el-button type="primary" size="medium" @click="submitForm" :loading="submitLoading">保存配置</el-button>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getConfigByKey, updateConfigByKey } from '@/api/system/config'
+import { listAdminTenantList } from '@/api/admin/sysCompany'
+
+const defaultForm = () => ({
+  ipad: {
+    url: '',
+    ipadUrl: '',
+    aiApi: '',
+    voiceApi: '',
+    commonApi: ''
+  }
+})
+
+function normalizeForm(raw) {
+  if (!raw || typeof raw !== 'object') {
+    return defaultForm()
+  }
+  const form = defaultForm()
+  if (raw.ipad && typeof raw.ipad === 'object') {
+    form.ipad.url = raw.ipad.url || ''
+    form.ipad.ipadUrl = raw.ipad.ipadUrl || ''
+    form.ipad.aiApi = raw.ipad.aiApi || ''
+    form.ipad.voiceApi = raw.ipad.voiceApi || ''
+    form.ipad.commonApi = raw.ipad.commonApi || ''
+  } else {
+    // 兼容旧版平铺字段
+    form.ipad.url = raw.url || ''
+    form.ipad.ipadUrl = raw.ipadUrl || ''
+    form.ipad.aiApi = raw.aiApi || ''
+    form.ipad.voiceApi = raw.voiceApi || ''
+    form.ipad.commonApi = raw.commonApi || ''
+  }
+  return form
+}
+
+export default {
+  name: 'AdminAiFrontConfig',
+  data() {
+    return {
+      loading: false,
+      submitLoading: false,
+      configId: null,
+      configKey: 'his.adminUi.aiConfig',
+      selectedTenantId: null,
+      tenantConfigEmpty: false,
+      fallbackLoading: false,
+      tenantList: [],
+      tenantTotal: 0,
+      hasMoreTenants: false,
+      tenantSelectLoading: false,
+      tenantLoadingMore: false,
+      tenantQueryParams: {
+        pageNum: 1,
+        pageSize: 20,
+        tenantName: '',
+        status: 1
+      },
+      form: defaultForm(),
+      tenantSearchTimer: null,
+      tenantSelectInputEl: null,
+      tenantSelectInputHandler: null
+    }
+  },
+  mounted() {
+    this.loadConfig()
+  },
+  beforeDestroy() {
+    this.unbindTenantSelectInputListener()
+  },
+  methods: {
+    formatTenantLabel(item) {
+      if (item.tenantCode) {
+        return `${item.tenantName}(${item.tenantCode})`
+      }
+      return item.tenantName
+    },
+    handleTenantSearch(query) {
+      if (query === undefined) return
+      this.tenantQueryParams.tenantName = (query || '').trim()
+      this.tenantQueryParams.pageNum = 1
+      this.fetchTenantList()
+    },
+    handleTenantSelectClear() {
+      this.handleTenantSearch('')
+    },
+    getTenantSelectInputEl() {
+      const root = this.$refs.tenantSelect && this.$refs.tenantSelect.$el
+      return root ? root.querySelector('input.el-input__inner') : null
+    },
+    bindTenantSelectInputListener() {
+      this.unbindTenantSelectInputListener()
+      const input = this.getTenantSelectInputEl()
+      if (!input) return
+      this.tenantSelectInputEl = input
+      this.tenantSelectInputHandler = () => {
+        const val = (input.value || '').trim()
+        if (!val && this.tenantQueryParams.tenantName) {
+          if (this.tenantSearchTimer) clearTimeout(this.tenantSearchTimer)
+          this.tenantSearchTimer = setTimeout(() => {
+            this.handleTenantSearch('')
+          }, 150)
+        }
+      }
+      input.addEventListener('input', this.tenantSelectInputHandler)
+    },
+    unbindTenantSelectInputListener() {
+      if (this.tenantSearchTimer) {
+        clearTimeout(this.tenantSearchTimer)
+        this.tenantSearchTimer = null
+      }
+      if (this.tenantSelectInputEl && this.tenantSelectInputHandler) {
+        this.tenantSelectInputEl.removeEventListener('input', this.tenantSelectInputHandler)
+      }
+      this.tenantSelectInputEl = null
+      this.tenantSelectInputHandler = null
+    },
+    syncTenantSearchFromInput() {
+      const input = this.getTenantSelectInputEl()
+      const inputVal = input ? (input.value || '').trim() : ''
+      if (!inputVal && this.tenantQueryParams.tenantName) {
+        this.handleTenantSearch('')
+      }
+    },
+    handleTenantDropdownVisible(visible) {
+      if (visible) {
+        if (this.tenantList.length === 0) {
+          this.handleTenantSearch('')
+        }
+        this.$nextTick(() => {
+          this.syncTenantSearchFromInput()
+          this.bindTenantSelectInputListener()
+        })
+      } else {
+        this.unbindTenantSelectInputListener()
+      }
+    },
+    fetchTenantList(isLoadMore = false) {
+      if (!isLoadMore) {
+        this.tenantSelectLoading = true
+      } else {
+        this.tenantLoadingMore = true
+      }
+      listAdminTenantList(this.tenantQueryParams).then(response => {
+        const rows = response.rows || []
+        if (isLoadMore) {
+          const existIds = new Set(this.tenantList.map(t => t.id))
+          const append = rows.filter(r => !existIds.has(r.id))
+          this.tenantList = this.tenantList.concat(append)
+        } else {
+          this.tenantList = rows
+        }
+        this.tenantTotal = response.total || 0
+        this.hasMoreTenants = this.tenantList.length < this.tenantTotal
+      }).finally(() => {
+        this.tenantSelectLoading = false
+        this.tenantLoadingMore = false
+      })
+    },
+    loadMoreTenants() {
+      if (this.tenantLoadingMore || !this.hasMoreTenants) return
+      this.tenantQueryParams.pageNum += 1
+      this.fetchTenantList(true)
+    },
+    resetForm() {
+      this.form = defaultForm()
+      this.configId = null
+      this.tenantConfigEmpty = false
+    },
+    loadConfig() {
+      this.loading = true
+      this.tenantConfigEmpty = false
+      const tenantId = this.selectedTenantId || undefined
+      getConfigByKey(this.configKey, tenantId).then(response => {
+        const data = response.data
+        if (data) {
+          this.configId = data.configId != null ? data.configId : null
+          if (data.configValue) {
+            this.tenantConfigEmpty = false
+            try {
+              this.form = normalizeForm(JSON.parse(data.configValue))
+            } catch (e) {
+              this.form = defaultForm()
+              this.tenantConfigEmpty = !!this.selectedTenantId
+            }
+          } else {
+            this.form = defaultForm()
+            this.tenantConfigEmpty = !!this.selectedTenantId
+          }
+        } else {
+          this.configId = null
+          this.form = defaultForm()
+          this.tenantConfigEmpty = !!this.selectedTenantId
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    loadGlobalConfigAsFallback() {
+      this.fallbackLoading = true
+      getConfigByKey(this.configKey).then(response => {
+        if (!response.data || !response.data.configValue) {
+          this.msgWarning('系统默认配置为空')
+          return
+        }
+        try {
+          this.form = normalizeForm(JSON.parse(response.data.configValue))
+          this.msgSuccess('已加载系统默认配置,保存时将写入当前租户')
+        } catch (e) {
+          this.msgWarning('系统默认配置解析失败')
+        }
+      }).finally(() => {
+        this.fallbackLoading = false
+      })
+    },
+    submitForm() {
+      this.submitLoading = true
+      const param = {
+        configId: (this.configId != null && this.configId !== '') ? Number(this.configId) : null,
+        configName: 'AI配置',
+        configKey: this.configKey,
+        configValue: JSON.stringify(this.form),
+        tenantId: this.selectedTenantId ? String(this.selectedTenantId) : null
+      }
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('修改成功')
+          this.loadConfig()
+        }
+      }).finally(() => {
+        this.submitLoading = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.section-card {
+  margin-bottom: 20px;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+}
+
+.tenant-form {
+  margin-bottom: 0;
+}
+
+.tenant-form >>> .el-form-item {
+  margin-bottom: 0;
+}
+
+.tenant-tip {
+  margin-bottom: 16px;
+}
+
+.tenant-load-more-option {
+  text-align: center;
+  padding: 8px 0;
+}
+
+.load-more {
+  color: #409eff;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+}
+
+.load-more:hover {
+  color: #66b1ff;
+}
+</style>

+ 1 - 0
src/views/admin/menu.js

@@ -41,6 +41,7 @@ const adminRoutes = {
     { path: 'wxConfig', component: () => import('@/views/admin/wxConfig/index'), name: 'AdminWxConfig', meta: { title: '个微配置' } },
     { path: 'ossConfig', component: () => import('@/views/admin/ossConfig/index'), name: 'AdminOssConfig', meta: { title: 'OSS配置' } },
     { path: 'frontConfig', component: () => import('@/views/admin/frontConfig/index'), name: 'AdminFrontConfig', meta: { title: '点播线路配置' } },
+    { path: 'aiFrontConfig', component: () => import('@/views/admin/aiFrontConfig/index'), name: 'AdminAiFrontConfig', meta: { title: 'AI配置' } },
     { path: 'dbConfig', component: () => import('@/views/admin/dbConfig/index'), name: 'AdminDbConfig', meta: { title: 'DB配置' } },
 
     // 7. 外呼管理