boss 4 дней назад
Родитель
Сommit
83bc304589
43 измененных файлов с 3091 добавлено и 323 удалено
  1. 9 0
      src/api/admin/sysCompany.js
  2. 10 0
      src/api/admin/voiceNumber.js
  3. 20 0
      src/api/admin/voiceOrder.js
  4. 18 0
      src/api/admin/voicePackage.js
  5. 19 3
      src/permission.js
  6. 21 14
      src/views/admin/ad/index.vue
  7. 7 3
      src/views/admin/aiChatQuality/index.vue
  8. 3 1
      src/views/admin/aiProvider/index.vue
  9. 7 3
      src/views/admin/article/index.vue
  10. 11 3
      src/views/admin/callRecord/index.vue
  11. 201 3
      src/views/admin/cidConfig/index.vue
  12. 10 2
      src/views/admin/commissionRecord/index.vue
  13. 7 3
      src/views/admin/course/index.vue
  14. 21 14
      src/views/admin/crm/index.vue
  15. 202 3
      src/views/admin/frontConfig/index.vue
  16. 137 6
      src/views/admin/ipadServer/index.vue
  17. 155 6
      src/views/admin/keywordManage/index.vue
  18. 7 3
      src/views/admin/live/index.vue
  19. 21 14
      src/views/admin/liveVideo/index.vue
  20. 207 4
      src/views/admin/ossConfig/index.vue
  21. 40 36
      src/views/admin/product/index.vue
  22. 17 17
      src/views/admin/proxy/index.vue
  23. 53 46
      src/views/admin/qwExternalContact/index.vue
  24. 10 2
      src/views/admin/rechargeRecord/index.vue
  25. 240 6
      src/views/admin/sms/index.vue
  26. 146 6
      src/views/admin/smsOrder/index.vue
  27. 97 6
      src/views/admin/smsPackage/index.vue
  28. 21 14
      src/views/admin/sop/index.vue
  29. 21 14
      src/views/admin/storeOrder/index.vue
  30. 23 19
      src/views/admin/sysCompany/index.vue
  31. 12 4
      src/views/admin/sysUser/index.vue
  32. 40 4
      src/views/admin/textModel/index.vue
  33. 21 14
      src/views/admin/videoResource/index.vue
  34. 180 6
      src/views/admin/voice/index.vue
  35. 149 6
      src/views/admin/voiceApi/index.vue
  36. 263 6
      src/views/admin/voiceBlacklist/index.vue
  37. 111 3
      src/views/admin/voiceFrequency/index.vue
  38. 59 6
      src/views/admin/voiceNumber/index.vue
  39. 89 6
      src/views/admin/voiceOrder/index.vue
  40. 63 6
      src/views/admin/voicePackage/index.vue
  41. 157 6
      src/views/admin/voiceSeat/index.vue
  42. 10 2
      src/views/admin/withdrawalManage/index.vue
  43. 176 3
      src/views/admin/wxConfig/index.vue

+ 9 - 0
src/api/admin/sysCompany.js

@@ -41,4 +41,13 @@ export function getStatistics() {
     url: '/admin/company/statistics',
     method: 'get'
   })
+}
+
+// 租户充值/扣款
+export function rechargeCompany(companyId, data) {
+  return request({
+    url: '/admin/company/' + companyId + '/recharge',
+    method: 'post',
+    data: data
+  })
 }

+ 10 - 0
src/api/admin/voiceNumber.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 查询CID主叫号码列表
+export function listCidNumber(query) {
+  return request({
+    url: '/company/companyVoiceRobotic/cidList',
+    method: 'get',
+    params: query
+  })
+}

+ 20 - 0
src/api/admin/voiceOrder.js

@@ -0,0 +1,20 @@
+import request from '@/utils/request'
+
+// 查询通话套餐订单列表
+export function listVoiceOrder(query) {
+  return request({
+    url: '/company/companyVoicePackageOrder/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出通话套餐订单
+export function exportVoiceOrder(query) {
+  return request({
+    url: '/company/companyVoicePackageOrder/export',
+    method: 'get',
+    params: query,
+    responseType: 'blob'
+  })
+}

+ 18 - 0
src/api/admin/voicePackage.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 查询通话套餐列表
+export function listVoicePackage(query) {
+  return request({
+    url: '/company/companyVoicePackageOrder/getVoicePackagelist',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询通话套餐详情
+export function getVoicePackage(id) {
+  return request({
+    url: '/company/companyVoicePackageOrder/' + id,
+    method: 'get'
+  })
+}

+ 19 - 3
src/permission.js

@@ -16,7 +16,9 @@ router.beforeEach((to, from, next) => {
   if (getToken()) {
     to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
     if (to.path === '/login') {
-      next({ path: '/' })
+      const roles = store.getters.roles || []
+      const isAdmin = roles.includes('admin') || roles.includes('superadmin')
+      next({ path: isAdmin ? '/admin/dashboard' : '/' })
       NProgress.done()
     } else {
       if (store.getters.roles.length === 0) {
@@ -25,7 +27,14 @@ router.beforeEach((to, from, next) => {
           store.dispatch('GenerateRoutes').then(accessRoutes => {
             // 根据roles权限生成可访问的路由表
             router.addRoutes(accessRoutes) // 动态添加可访问路由表
-            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
+            // admin用户首次进入时重定向到总后台
+            const roles = store.getters.roles || []
+            const isAdmin = roles.includes('admin') || roles.includes('superadmin')
+            if (isAdmin && (to.path === '/' || to.path === '/index')) {
+              next({ path: '/admin/dashboard', replace: true })
+            } else {
+              next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
+            }
           })
         }).catch(err => {
             store.dispatch('LogOut').then(() => {
@@ -34,7 +43,14 @@ router.beforeEach((to, from, next) => {
             })
           })
       } else {
-        next()
+        // admin用户访问/index时重定向到总后台
+        const roles = store.getters.roles || []
+        const isAdmin = roles.includes('admin') || roles.includes('superadmin')
+        if (isAdmin && (to.path === '/' || to.path === '/index')) {
+          next({ path: '/admin/dashboard', replace: true })
+        } else {
+          next()
+        }
       }
     }
   } else {

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

@@ -1,18 +1,20 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
-      <el-form-item label="租户名称" prop="companyName">
-        <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item label="账户名称" prop="accountName">
-        <el-input v-model="queryParams.accountName" placeholder="请输入账户名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item>
-        <inline-tenant-selector @change="onTenantChange" />
-        <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 shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px" size="small">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="账户名称" prop="accountName">
+          <el-input v-model="queryParams.accountName" placeholder="请输入账户名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <inline-tenant-selector @change="onTenantChange" />
+          <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>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -21,7 +23,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="list">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="list">
       <el-table-column label="租户名称" align="center" prop="companyName" min-width="150" />
       <el-table-column label="账户名称" align="center" prop="accountName" />
       <el-table-column label="平台" align="center" prop="platform" width="100" />
@@ -68,3 +70,8 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 7 - 3
src/views/admin/aiChatQuality/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px" size="small">
       <el-form-item label="租户名称" prop="companyName">
         <el-input
           v-model="queryParams.companyName"
@@ -34,7 +35,8 @@
       <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>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -51,7 +53,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="sessionList">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="sessionList">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="租户名称" align="center" prop="companyName" />
       <el-table-column label="用户名称" align="center" prop="userName" />
@@ -275,4 +277,6 @@ export default {
   font-size: 12px;
   color: #999;
 }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

+ 3 - 1
src/views/admin/aiProvider/index.vue

@@ -17,7 +17,7 @@
         style="margin-bottom: 20px"
       />
 
-      <el-table :data="tableData" v-loading="loading" border>
+      <el-table :data="tableData" v-loading="loading" border size="small" style="width:100%">
         <el-table-column type="index" label="序号" width="60" align="center" />
         <el-table-column prop="providerName" label="供应商名称" min-width="120" />
         <el-table-column prop="providerCode" label="供应商编码" width="100" />
@@ -199,4 +199,6 @@ export default {
 
 <style scoped>
 .card-header { display: flex; justify-content: space-between; align-items: center; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

+ 7 - 3
src/views/admin/article/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px" size="small">
       <el-form-item label="租户名称" prop="companyName">
         <el-input
           v-model="queryParams.companyName"
@@ -34,7 +35,8 @@
       <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>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -69,7 +71,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="articleList">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="articleList">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="租户名称" align="center" prop="companyName" />
       <el-table-column label="文章标题" align="center" prop="articleTitle" />
@@ -371,4 +373,6 @@ export default {
   font-size: 14px;
   color: #666;
 }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

+ 11 - 3
src/views/admin/callRecord/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <el-card shadow="never" class="mb16 filter-card">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
       <el-form-item label="租户名称" prop="companyName">
         <el-input
           v-model="queryParams.companyName"
@@ -34,6 +35,7 @@
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -68,7 +70,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="callRecordList" @selection-change="handleSelectionChange">
+    <el-table v-loading="loading" :data="callRecordList" 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="companyName" />
       <el-table-column label="员工名称" align="center" prop="userName" />
@@ -187,7 +189,7 @@
 
     <!-- 质检记录弹窗 -->
     <el-dialog title="质检记录" :visible.sync="showQualityTab" width="800px" append-to-body>
-      <el-table border :data="qualityList">
+      <el-table :data="qualityList" border>
         <el-table-column label="租户名称" align="center" prop="companyName" />
         <el-table-column label="员工名称" align="center" prop="userName" />
         <el-table-column label="客户号码" align="center" prop="customerPhone" />
@@ -485,4 +487,10 @@ export default {
   font-size: 14px;
   color: #666;
 }
+</style>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

+ 201 - 3
src/views/admin/cidConfig/index.vue

@@ -1,19 +1,217 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
+    <el-card v-loading="loading" shadow="never">
       <div slot="header">
         <span>CID配置</span>
       </div>
-      <el-empty description="功能开发中,敬请期待" />
+      <el-form ref="form" :model="form" label-width="220px">
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>手机号配置</span></div>
+          <el-form-item label="是否开启手机号配置" prop="enablePhoneConfig">
+            <el-switch v-model="form.enablePhoneConfig"></el-switch>
+          </el-form-item>
+          <template v-if="form.enablePhoneConfig">
+            <el-form-item label="生成条数" prop="generateCount">
+              <el-input-number v-model="form.generateCount" :min="1" :step="1" :precision="0" placeholder="请输入生成条数"></el-input-number>
+            </el-form-item>
+            <el-form-item label="开始位置" prop="startIndex">
+              <el-input-number v-model="form.startIndex" :min="1" :step="1" :precision="0" placeholder="例如: 1"></el-input-number>
+              <span class="tip-text">(从第几位开始生成)</span>
+            </el-form-item>
+            <el-form-item label="结束位置" prop="endIndex">
+              <el-input-number v-model="form.endIndex" :min="1" :step="1" :precision="0" placeholder="例如: 11"></el-input-number>
+              <span class="tip-text">(到第几位结束)</span>
+            </el-form-item>
+          </template>
+          <el-form-item label="是否开手机号拨打次数限制" prop="enablePhoneLimitConfig">
+            <el-switch v-model="form.enablePhoneLimitConfig"></el-switch>
+          </el-form-item>
+          <template v-if="form.enablePhoneLimitConfig">
+            <el-form-item label="拨打次数限制" prop="numberCalls">
+              <el-input-number v-model="form.numberCalls" :min="1" :step="1" :precision="0" placeholder="例如: 1"></el-input-number>
+            </el-form-item>
+          </template>
+        </el-card>
+
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>DeepSeekChat配置</span></div>
+          <el-form-item label="DeepSeekChat模型并发数" prop="concurrency">
+            <el-input v-model="form.concurrency" placeholder="请输入DeepSeekChat模型并发数"></el-input>
+          </el-form-item>
+          <el-form-item label="DeepSeekChat服务地址" prop="serverAddress">
+            <el-input v-model="form.serverAddress" placeholder="请输入DeepSeekChat服务地址"></el-input>
+          </el-form-item>
+          <el-form-item label="DeepSeekChat_apiKey" prop="apiKey">
+            <el-input v-model="form.apiKey" placeholder="请输入DeepSeekChat_apiKey"></el-input>
+          </el-form-item>
+          <el-form-item label="DeepSeekChat模型名称" prop="modelName">
+            <el-input v-model="form.modelName" placeholder="请输入DeepSeekChat模型名称"></el-input>
+          </el-form-item>
+        </el-card>
+
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>外呼网关配置</span></div>
+          <el-form-item label="是否限制外呼网关" prop="enableGateWayLimit">
+            <el-switch v-model="form.enableGateWayLimit"></el-switch>
+          </el-form-item>
+          <el-form-item label="系统可见外呼网关" prop="showGatewayIds" v-if="!!form.enableGateWayLimit">
+            <el-select v-model="form.showGatewayIds" multiple filterable placeholder="请选择系统可见外呼网关">
+              <el-option
+                v-for="item in gatewayList"
+                :key="item.id"
+                :label="item.gwDesc"
+                :value="item.id">
+              </el-option>
+            </el-select>
+          </el-form-item>
+        </el-card>
+
+        <div class="footer">
+          <el-button type="primary" :loading="submitLoading" @click="submitForm">提 交</el-button>
+        </div>
+      </el-form>
     </el-card>
   </div>
 </template>
 
 <script>
+import { getConfigByKey, updateConfigByKey, getGatewayList } from '@/api/system/config'
+
 export default {
   name: 'AdminCidConfig',
   data() {
-    return {}
+    return {
+      loading: false,
+      submitLoading: false,
+      configId: null,
+      configKey: 'cId.config',
+      gatewayList: [],
+      form: {
+        enablePhoneConfig: false,
+        enablePhoneLimitConfig: false,
+        generateCount: 1,
+        startIndex: 1,
+        endIndex: 11,
+        numberCalls: 1,
+        concurrency: '',
+        serverAddress: '',
+        apiKey: '',
+        modelName: '',
+        enableGateWayLimit: false,
+        showGatewayIds: []
+      }
+    }
+  },
+  created() {
+    this.loadConfig()
+  },
+  methods: {
+    loadConfig() {
+      this.loading = true
+      getConfigByKey(this.configKey).then(response => {
+        if (response.data) {
+          this.configId = response.data.configId
+          try {
+            const parsed = JSON.parse(response.data.configValue)
+            this.form = { ...this.form, ...parsed }
+          } catch (e) {
+            // 使用默认值
+          }
+        } else {
+          this.configId = null
+        }
+        // 加载网关列表
+        if (this.form.enableGateWayLimit) {
+          this.loadGatewayList()
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    loadGatewayList() {
+      getGatewayList().then(res => {
+        this.gatewayList = res.data || []
+      })
+    },
+    submitForm() {
+      // 验证手机号配置
+      if (this.form.enablePhoneConfig) {
+        if (!this.form.generateCount || this.form.generateCount < 1) {
+          this.msgError('生成条数不能为空且不能小于1')
+          return false
+        }
+        if (!this.form.startIndex || this.form.startIndex < 1 || this.form.startIndex > 11) {
+          this.msgError('开始位置不能为空,且必须在1到11之间')
+          return false
+        }
+        if (!this.form.endIndex || this.form.endIndex < 1 || this.form.endIndex > 11) {
+          this.msgError('结束位置不能为空,且必须在1到11之间')
+          return false
+        }
+        if (this.form.startIndex > this.form.endIndex) {
+          this.msgError('开始位置不能大于结束位置')
+          return false
+        }
+        if (this.form.startIndex === this.form.endIndex) {
+          this.msgError('开始位置不能等于结束位置')
+          return false
+        }
+        let num = this.form.endIndex - this.form.startIndex
+        if (num < 4) {
+          this.msgError('开始和结束位置不能小于4位')
+          return false
+        }
+      }
+      // 验证拨打次数限制
+      if (this.form.enablePhoneLimitConfig) {
+        if (this.form.numberCalls == null || this.form.numberCalls === 0) {
+          this.msgError('限制次数不能为空或者大于0!')
+          return false
+        }
+        if (this.form.numberCalls > 100000) {
+          this.msgError('限制次数不能超过10万次!')
+          return false
+        }
+      }
+      this.submitLoading = true
+      const param = {
+        configId: this.configId,
+        configKey: this.configKey,
+        configValue: JSON.stringify(this.form)
+      }
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('修改成功')
+        }
+      }).finally(() => {
+        this.submitLoading = false
+      })
+    }
+  },
+  watch: {
+    'form.enableGateWayLimit'(val) {
+      if (val && this.gatewayList.length === 0) {
+        this.loadGatewayList()
+      }
+    }
   }
 }
 </script>
+
+<style scoped>
+.section-card {
+  margin-bottom: 20px;
+}
+.tip-text {
+  color: #909399;
+  font-size: 12px;
+  margin-left: 8px;
+}
+.footer {
+  width: 100%;
+  display: flex;
+  margin-top: 30px;
+  align-items: flex-end;
+  justify-content: flex-end;
+}
+</style>

+ 10 - 2
src/views/admin/commissionRecord/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <el-card shadow="never" class="mb16 filter-card">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
       <el-form-item label="代理名称" prop="proxyName">
         <el-input v-model="queryParams.proxyName" placeholder="请输入代理名称" clearable size="small" @keyup.enter.native="handleQuery" />
       </el-form-item>
@@ -24,6 +25,7 @@
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -32,7 +34,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
     </el-row>
 
-    <el-table border v-loading="loading" :data="recordList">
+    <el-table border v-loading="loading" :data="recordList" size="small" style="width:100%">
       <el-table-column type="index" label="序号" width="55" align="center" />
       <el-table-column label="代理ID" align="center" prop="proxyId" width="70" />
       <el-table-column label="代理名称" align="center" prop="proxyName" width="120" />
@@ -161,3 +163,9 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 7 - 3
src/views/admin/course/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px" size="small">
       <el-form-item label="租户名称" prop="companyName">
         <el-input
           v-model="queryParams.companyName"
@@ -34,7 +35,8 @@
       <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>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -60,7 +62,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="courseList">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="courseList">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="租户名称" align="center" prop="companyName" />
       <el-table-column label="课程名称" align="center" prop="courseName" />
@@ -285,4 +287,6 @@ export default {
   font-size: 14px;
   color: #666;
 }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

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

@@ -1,18 +1,20 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
-      <el-form-item label="租户名称" prop="companyName">
-        <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item label="客户名称" prop="customerName">
-        <el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item>
-        <inline-tenant-selector @change="onTenantChange" />
-        <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 shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px" size="small">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="客户名称" prop="customerName">
+          <el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <inline-tenant-selector @change="onTenantChange" />
+          <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>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -21,7 +23,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="list">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="list">
       <el-table-column label="租户名称" align="center" prop="companyName" min-width="150" />
       <el-table-column label="客户名称" align="center" prop="customerName" />
       <el-table-column label="手机号" align="center" prop="customerPhone" width="130" />
@@ -68,3 +70,8 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 202 - 3
src/views/admin/frontConfig/index.vue

@@ -1,19 +1,218 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
+    <el-card v-loading="loading" shadow="never">
       <div slot="header">
         <span>前端配置</span>
       </div>
-      <el-empty description="功能开发中,敬请期待" />
+      <el-form ref="form" :model="form" label-width="200px">
+        <!-- 存储桶配置 -->
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>存储桶配置</span></div>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="OBS Access Key ID" prop="obsAccessKeyId">
+                <el-input v-model="form.obsAccessKeyId" type="password" show-password placeholder="请输入Access Key ID" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="OBS Secret Access Key" prop="obsSecretAccessKey">
+                <el-input v-model="form.obsSecretAccessKey" type="password" show-password placeholder="请输入Secret Access Key" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="OBS服务器地址" prop="obsServer">
+                <el-input v-model="form.obsServer" placeholder="请输入OBS服务器地址">
+                  <template slot="prepend">https://</template>
+                </el-input>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="OBS存储桶" prop="obsBucket">
+                <el-input v-model="form.obsBucket" placeholder="请输入OBS存储桶名称" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="COS存储桶" prop="cosBucket">
+                <el-input v-model="form.cosBucket" placeholder="请输入COS存储桶名称" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="COS存储区域" prop="cosRegion">
+                <el-input v-model="form.cosRegion" placeholder="请输入COS存储区域" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 视频线路配置 -->
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>视频线路配置</span></div>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="线路一地址" prop="videoLinePrimary">
+                <el-input v-model="form.videoLinePrimary" placeholder="请输入线路一地址">
+                  <template slot="prepend">https://</template>
+                </el-input>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="线路二地址" prop="videoLineSecondary">
+                <el-input v-model="form.videoLineSecondary" placeholder="请输入线路二地址">
+                  <template slot="prepend">https://</template>
+                </el-input>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="直播解码路径" prop="livePath">
+                <el-input v-model="form.livePath" placeholder="请输入直播解码路径">
+                  <template slot="prepend">/</template>
+                </el-input>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 火山云配置 -->
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>火山云配置</span></div>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="火山云视频地址" prop="volcanoVideoDomain">
+                <el-input v-model="form.volcanoVideoDomain" placeholder="请输入火山云视频地址">
+                  <template slot="prepend">https://</template>
+                </el-input>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="点播空间名称" prop="volcanoVodSpace">
+                <el-input v-model="form.volcanoVodSpace" placeholder="请输入点播空间名称" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 直播配置 -->
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>直播配置</span></div>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="WebSocket地址" prop="liveWebSocketUrl">
+                <el-input v-model="form.liveWebSocketUrl" placeholder="请输入WebSocket地址">
+                  <template slot="prepend">wss://</template>
+                </el-input>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 业务配置 -->
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>业务配置</span></div>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="默认课程类型" prop="courseDefaultType">
+                <el-radio-group v-model="form.courseDefaultType">
+                  <el-radio label="1">会员</el-radio>
+                  <el-radio label="2">企微</el-radio>
+                </el-radio-group>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <div class="footer">
+          <el-button type="primary" :loading="submitLoading" @click="submitForm">提 交</el-button>
+        </div>
+      </el-form>
     </el-card>
   </div>
 </template>
 
 <script>
+import { getConfigByKey, updateConfigByKey } from '@/api/system/config'
+
 export default {
   name: 'AdminFrontConfig',
   data() {
-    return {}
+    return {
+      loading: false,
+      submitLoading: false,
+      configId: null,
+      configKey: 'his.adminUi.config',
+      form: {
+        obsAccessKeyId: '',
+        obsSecretAccessKey: '',
+        obsServer: '',
+        obsBucket: '',
+        cosBucket: '',
+        cosRegion: '',
+        videoLinePrimary: '',
+        videoLineSecondary: '',
+        livePath: '',
+        volcanoVideoDomain: '',
+        volcanoVodSpace: '',
+        liveWebSocketUrl: '',
+        courseDefaultType: '1'
+      }
+    }
+  },
+  created() {
+    this.loadConfig()
+  },
+  methods: {
+    loadConfig() {
+      this.loading = true
+      getConfigByKey(this.configKey).then(response => {
+        if (response.data) {
+          this.configId = response.data.configId
+          try {
+            const parsed = JSON.parse(response.data.configValue)
+            this.form = { ...this.form, ...parsed }
+          } catch (e) {
+            // 使用默认值
+          }
+        } else {
+          this.configId = null
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    submitForm() {
+      this.submitLoading = true
+      const param = {
+        configId: this.configId,
+        configName: '前端配置',
+        configKey: this.configKey,
+        configValue: JSON.stringify(this.form)
+      }
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('修改成功')
+        }
+      }).finally(() => {
+        this.submitLoading = false
+      })
+    }
   }
 }
 </script>
+
+<style scoped>
+.section-card {
+  margin-bottom: 20px;
+}
+.footer {
+  width: 100%;
+  display: flex;
+  margin-top: 30px;
+  align-items: flex-end;
+  justify-content: flex-end;
+}
+</style>

+ 137 - 6
src/views/admin/ipadServer/index.vue

@@ -1,19 +1,150 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>Ipad服务器</span>
+    <!-- ===== 操作栏 ===== -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <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-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="loadList" />
+    </el-row>
+
+    <!-- ===== 列表 ===== -->
+    <el-table border v-loading="loading" :data="dataList" size="small" style="width:100%">
+      <el-table-column label="服务器ID" prop="id" width="80" align="center" />
+      <el-table-column label="服务器地址/IP" prop="serverIp" min-width="180" />
+      <el-table-column label="端口" prop="port" width="100" align="center" />
+      <el-table-column label="状态" prop="status" width="100" align="center">
+        <template slot-scope="s">
+          <el-tag v-if="s.row.status === 1" type="success" size="mini">在线</el-tag>
+          <el-tag v-else-if="s.row.status === 0" type="danger" size="mini">离线</el-tag>
+          <el-tag v-else size="mini">{{ s.row.status || '-' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="在线设备数" prop="onlineDeviceCount" width="100" align="center" />
+      <el-table-column label="创建时间" prop="createTime" width="160" align="center" />
+      <el-table-column label="操作" align="center" width="160" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="s">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openDialog(s.row)">编辑</el-button>
+          <el-button size="mini" type="text" style="color:#f5222d" icon="el-icon-delete" @click="handleDelete(s.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="loadList" />
+
+    <!-- ===== 编辑弹窗 ===== -->
+    <el-dialog :title="formTitle" :visible.sync="dialogVisible" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="服务器地址" prop="serverIp">
+          <el-input v-model="form.serverIp" placeholder="请输入服务器地址/IP" />
+        </el-form-item>
+        <el-form-item label="端口" prop="port">
+          <el-input-number v-model="form.port" :min="1" :max="65535" :precision="0" style="width:100%" placeholder="请输入端口号" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
       </div>
-      <el-empty description="功能开发中,敬请期待" />
-    </el-card>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import request from '@/utils/request'
+
 export default {
   name: 'AdminIpadServer',
   data() {
-    return {}
+    return {
+      showSearch: true,
+      loading: false,
+      dataList: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10 },
+      dialogVisible: false,
+      formTitle: '',
+      submitting: false,
+      form: { id: null, serverIp: '', port: 8080, remark: '' },
+      rules: {
+        serverIp: [{ required: true, message: '请输入服务器地址', trigger: 'blur' }],
+        port: [{ required: true, message: '请输入端口', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.loadList()
+  },
+  methods: {
+    loadList() {
+      this.loading = true
+      request({ url: '/qw/qwIpadServer/list', method: 'get', params: this.queryParams }).then(r => {
+        this.dataList = r.rows
+        this.total = r.total
+        this.loading = false
+      })
+    },
+    openDialog(row) {
+      this.reset()
+      if (row) {
+        this.formTitle = '编辑服务器'
+        request({ url: `/qw/qwIpadServer/${row.id}`, method: 'get' }).then(r => {
+          this.form = { ...this.form, ...r.data }
+          this.dialogVisible = true
+        })
+      } else {
+        this.formTitle = '新增服务器'
+        this.dialogVisible = true
+      }
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        const req = this.form.id
+          ? request({ url: '/qw/qwIpadServer', method: 'put', data: this.form })
+          : request({ url: '/qw/qwIpadServer', method: 'post', data: this.form })
+        req.then(() => {
+          this.$message.success(this.form.id ? '修改成功' : '新增成功')
+          this.dialogVisible = false
+          this.loadList()
+        }).finally(() => { this.submitting = false })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确认删除服务器 "${row.serverIp}"?`, '提示', { type: 'warning' }).then(() => {
+        request({ url: `/qw/qwIpadServer/${row.id}`, method: 'delete' }).then(() => {
+          this.$message.success('删除成功')
+          this.loadList()
+        })
+      })
+    },
+    handleExport() {
+      request({ url: '/qw/qwIpadServer/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 = 'iPad服务器数据.xlsx'
+        link.click()
+        URL.revokeObjectURL(link.href)
+      })
+    },
+    reset() {
+      this.form = { id: null, serverIp: '', port: 8080, remark: '' }
+      if (this.$refs['form']) this.$refs['form'].resetFields()
+    }
   }
 }
 </script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 155 - 6
src/views/admin/keywordManage/index.vue

@@ -1,19 +1,168 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>关键词管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <!-- ===== 搜索栏 ===== -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
+        <el-form-item label="关键词" prop="keywordName">
+          <el-input v-model="queryParams.keywordName" placeholder="请输入关键词" clearable @keyup.enter.native="loadList" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="loadList">查询</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
     </el-card>
+
+    <!-- ===== 操作栏 ===== -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <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-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="loadList" />
+    </el-row>
+
+    <!-- ===== 列表 ===== -->
+    <el-table border v-loading="loading" :data="dataList" size="small" style="width:100%">
+      <el-table-column label="关键词ID" prop="keywordId" width="80" align="center" />
+      <el-table-column label="关键词内容" prop="keywordContent" min-width="200" />
+      <el-table-column label="类型" prop="keywordType" width="120" 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>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" prop="createTime" width="160" align="center" />
+      <el-table-column label="操作" align="center" width="160" fixed="right" class-name="small-padding fixed-width">
+        <template slot-scope="s">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openDialog(s.row)">编辑</el-button>
+          <el-button size="mini" type="text" style="color:#f5222d" icon="el-icon-delete" @click="handleDelete(s.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="loadList" />
+
+    <!-- ===== 编辑弹窗 ===== -->
+    <el-dialog :title="formTitle" :visible.sync="dialogVisible" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="关键词内容" prop="keywordContent">
+          <el-input v-model="form.keywordContent" placeholder="请输入关键词内容" />
+        </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-select>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import request from '@/utils/request'
+
 export default {
   name: 'AdminKeywordManage',
   data() {
-    return {}
+    return {
+      showSearch: true,
+      loading: false,
+      dataList: [],
+      total: 0,
+      queryParams: { pageNum: 1, pageSize: 10, keywordName: null },
+      dialogVisible: false,
+      formTitle: '',
+      submitting: false,
+      form: { keywordId: null, keywordContent: '', keywordType: 1, remark: '' },
+      rules: {
+        keywordContent: [{ required: true, message: '请输入关键词内容', trigger: 'blur' }],
+        keywordType: [{ required: true, message: '请选择类型', trigger: 'change' }]
+      }
+    }
+  },
+  created() {
+    this.loadList()
+  },
+  methods: {
+    loadList() {
+      this.loading = true
+      request({ url: '/system/keyword/list', method: 'get', params: this.queryParams }).then(r => {
+        this.dataList = r.rows
+        this.total = r.total
+        this.loading = false
+      })
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.loadList()
+    },
+    openDialog(row) {
+      this.reset()
+      if (row) {
+        this.formTitle = '编辑关键词'
+        request({ url: `/system/keyword/${row.keywordId}`, method: 'get' }).then(r => {
+          this.form = { ...this.form, ...r.data }
+          this.dialogVisible = true
+        })
+      } else {
+        this.formTitle = '新增关键词'
+        this.dialogVisible = true
+      }
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        const req = this.form.keywordId
+          ? request({ url: '/system/keyword', method: 'put', data: this.form })
+          : request({ url: '/system/keyword', method: 'post', data: this.form })
+        req.then(() => {
+          this.$message.success(this.form.keywordId ? '修改成功' : '新增成功')
+          this.dialogVisible = false
+          this.loadList()
+        }).finally(() => { this.submitting = false })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`确认删除关键词 "${row.keywordContent}"?`, '提示', { type: 'warning' }).then(() => {
+        request({ url: `/system/keyword/${row.keywordId}`, method: 'delete' }).then(() => {
+          this.$message.success('删除成功')
+          this.loadList()
+        })
+      })
+    },
+    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)
+      })
+    },
+    reset() {
+      this.form = { keywordId: null, keywordContent: '', keywordType: 1, remark: '' }
+      if (this.$refs['form']) this.$refs['form'].resetFields()
+    }
   }
 }
 </script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 7 - 3
src/views/admin/live/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px" size="small">
       <el-form-item label="租户名称" prop="companyName">
         <el-input
           v-model="queryParams.companyName"
@@ -34,7 +35,8 @@
       <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>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -60,7 +62,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="liveList">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="liveList">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="租户名称" align="center" prop="companyName" />
       <el-table-column label="直播标题" align="center" prop="liveTitle" />
@@ -289,4 +291,6 @@ export default {
   font-size: 14px;
   color: #666;
 }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

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

@@ -1,20 +1,22 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
-      <el-form-item label="租户名称" prop="companyName">
-        <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item label="视频标题" prop="videoTitle">
-        <el-input v-model="queryParams.videoTitle" placeholder="请输入视频标题" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item>
-        <inline-tenant-selector @change="onTenantChange" />
-        <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 shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" size="small" label-width="100px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="视频标题" prop="videoTitle">
+          <el-input v-model="queryParams.videoTitle" placeholder="请输入视频标题" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <inline-tenant-selector @change="onTenantChange" />
+          <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>
 
-    <el-table v-loading="loading" :data="list">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="list">
       <el-table-column label="租户名称" prop="companyName" width="160" />
       <el-table-column label="视频标题" prop="title" :show-overflow-tooltip="true" />
       <el-table-column label="直播ID" prop="liveId" width="120" />
@@ -84,3 +86,8 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 207 - 4
src/views/admin/ossConfig/index.vue

@@ -1,19 +1,222 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
+    <el-card v-loading="loading" shadow="never">
       <div slot="header">
-        <span>OSS配置</span>
+        <span>OSS云存储配置</span>
       </div>
-      <el-empty description="功能开发中,敬请期待" />
+      <el-form ref="form" :model="form" label-width="200px">
+        <el-form-item label="类型" prop="type">
+          <el-radio-group v-model="form.type">
+            <el-radio :label="1">七牛云</el-radio>
+            <el-radio :label="2">阿里云</el-radio>
+            <el-radio :label="3">腾讯云</el-radio>
+            <el-radio :label="4">华为云</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <!-- 七牛云 -->
+        <template v-if="form.type === 1">
+          <el-card shadow="never" class="section-card">
+            <div slot="header"><span>七牛云配置</span></div>
+            <el-form-item label="七牛绑定的域名" prop="qiniuDomain">
+              <el-input v-model="form.qiniuDomain" placeholder="请输入七牛绑定的域名"></el-input>
+            </el-form-item>
+            <el-form-item label="七牛路径前缀" prop="qiniuPrefix">
+              <el-input v-model="form.qiniuPrefix" placeholder="请输入七牛路径前缀"></el-input>
+            </el-form-item>
+            <el-form-item label="七牛ACCESS_KEY" prop="qiniuAccessKey">
+              <el-input v-model="form.qiniuAccessKey" placeholder="请输入七牛ACCESS_KEY"></el-input>
+            </el-form-item>
+            <el-form-item label="七牛SecretKey" prop="qiniuSecretKey">
+              <el-input v-model="form.qiniuSecretKey" placeholder="请输入七牛SecretKey"></el-input>
+            </el-form-item>
+            <el-form-item label="七牛空间名" prop="qiniuBucketName">
+              <el-input v-model="form.qiniuBucketName" placeholder="请输入七牛空间名"></el-input>
+            </el-form-item>
+          </el-card>
+        </template>
+
+        <!-- 阿里云 -->
+        <template v-if="form.type === 2">
+          <el-card shadow="never" class="section-card">
+            <div slot="header"><span>阿里云配置</span></div>
+            <el-form-item label="阿里云绑定的域名" prop="aliyunDomain">
+              <el-input v-model="form.aliyunDomain" placeholder="请输入阿里云绑定的域名"></el-input>
+            </el-form-item>
+            <el-form-item label="阿里云路径前缀" prop="aliyunPrefix">
+              <el-input v-model="form.aliyunPrefix" placeholder="请输入阿里云路径前缀"></el-input>
+            </el-form-item>
+            <el-form-item label="阿里云EndPoint" prop="aliyunEndPoint">
+              <el-input v-model="form.aliyunEndPoint" placeholder="请输入阿里云EndPoint"></el-input>
+            </el-form-item>
+            <el-form-item label="阿里云AccessKeyId" prop="aliyunAccessKeyId">
+              <el-input v-model="form.aliyunAccessKeyId" placeholder="请输入阿里云AccessKeyId"></el-input>
+            </el-form-item>
+            <el-form-item label="阿里云AccessKeySecret" prop="aliyunAccessKeySecret">
+              <el-input v-model="form.aliyunAccessKeySecret" placeholder="请输入阿里云AccessKeySecret"></el-input>
+            </el-form-item>
+            <el-form-item label="阿里云BucketName" prop="aliyunBucketName">
+              <el-input v-model="form.aliyunBucketName" placeholder="请输入阿里云BucketName"></el-input>
+            </el-form-item>
+          </el-card>
+        </template>
+
+        <!-- 腾讯云 -->
+        <template v-if="form.type === 3">
+          <el-card shadow="never" class="section-card">
+            <div slot="header"><span>腾讯云配置</span></div>
+            <el-form-item label="腾讯云绑定的域名" prop="qcloudDomain">
+              <el-input v-model="form.qcloudDomain" placeholder="请输入腾讯云绑定的域名"></el-input>
+            </el-form-item>
+            <el-form-item label="腾讯云前缀" prop="qcloudPrefix">
+              <el-input v-model="form.qcloudPrefix" placeholder="请输入腾讯云前缀"></el-input>
+            </el-form-item>
+            <el-form-item label="腾讯云SecretId" prop="qcloudSecretId">
+              <el-input v-model="form.qcloudSecretId" placeholder="请输入腾讯云SecretId"></el-input>
+            </el-form-item>
+            <el-form-item label="腾讯云SecretKey" prop="qcloudSecretKey">
+              <el-input v-model="form.qcloudSecretKey" placeholder="请输入腾讯云SecretKey"></el-input>
+            </el-form-item>
+            <el-form-item label="腾讯云BucketName" prop="qcloudBucketName">
+              <el-input v-model="form.qcloudBucketName" placeholder="请输入腾讯云BucketName"></el-input>
+            </el-form-item>
+            <el-form-item label="所属地区" prop="qcloudRegion">
+              <el-input v-model="form.qcloudRegion" placeholder="请输入所属地区"></el-input>
+            </el-form-item>
+          </el-card>
+        </template>
+
+        <!-- 华为云 -->
+        <template v-if="form.type === 4">
+          <el-card shadow="never" class="section-card">
+            <div slot="header"><span>华为云配置</span></div>
+            <el-form-item label="华为云绑定的域名" prop="huaweiDomain">
+              <el-input v-model="form.huaweiDomain" placeholder="请输入华为云绑定的域名"></el-input>
+            </el-form-item>
+            <el-form-item label="华为云Endpoint" prop="huaweiEndpoint">
+              <el-input v-model="form.huaweiEndpoint" placeholder="请输入华为云Endpoint"></el-input>
+            </el-form-item>
+            <el-form-item label="华为云AK" prop="huaweiAK">
+              <el-input v-model="form.huaweiAK" placeholder="请输入华为云AK"></el-input>
+            </el-form-item>
+            <el-form-item label="华为云SK" prop="huaweiSK">
+              <el-input v-model="form.huaweiSK" placeholder="请输入华为云SK"></el-input>
+            </el-form-item>
+            <el-form-item label="华为云BucketName" prop="huaweiBucketName">
+              <el-input v-model="form.huaweiBucketName" placeholder="请输入华为云BucketName"></el-input>
+            </el-form-item>
+          </el-card>
+        </template>
+
+        <div class="footer">
+          <el-button type="primary" :loading="submitLoading" @click="submitForm">提 交</el-button>
+        </div>
+      </el-form>
     </el-card>
   </div>
 </template>
 
 <script>
+import { getConfigByKey, updateConfigByKey } from '@/api/system/config'
+
 export default {
   name: 'AdminOssConfig',
   data() {
-    return {}
+    return {
+      loading: false,
+      submitLoading: false,
+      configId: null,
+      configKey: 'sys.oss.cloudStorage',
+      form: {
+        type: 1,
+        // 七牛云
+        qiniuDomain: '',
+        qiniuPrefix: '',
+        qiniuAccessKey: '',
+        qiniuSecretKey: '',
+        qiniuBucketName: '',
+        // 阿里云
+        aliyunDomain: '',
+        aliyunPrefix: '',
+        aliyunEndPoint: '',
+        aliyunAccessKeyId: '',
+        aliyunAccessKeySecret: '',
+        aliyunBucketName: '',
+        // 腾讯云
+        qcloudDomain: '',
+        qcloudPrefix: '',
+        qcloudSecretId: '',
+        qcloudSecretKey: '',
+        qcloudBucketName: '',
+        qcloudRegion: '',
+        // 华为云
+        huaweiDomain: '',
+        huaweiEndpoint: '',
+        huaweiAK: '',
+        huaweiSK: '',
+        huaweiBucketName: ''
+      }
+    }
+  },
+  created() {
+    this.loadConfig()
+  },
+  methods: {
+    loadConfig() {
+      this.loading = true
+      getConfigByKey(this.configKey).then(response => {
+        const configValue = response && response.data ? response.data.configValue : null
+        if (response.data) {
+          this.configId = response.data.configId
+        } else {
+          this.configId = null
+        }
+        // 安全解析
+        if (configValue !== null && configValue !== undefined && configValue !== '' && configValue !== 'null') {
+          try {
+            const parsed = JSON.parse(configValue)
+            if (parsed !== null && parsed !== undefined) {
+              this.form = { ...this.form, ...parsed }
+            }
+          } catch (e) {
+            // 使用默认值
+          }
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (valid) {
+          this.submitLoading = true
+          const param = {
+            configId: this.configId,
+            configValue: JSON.stringify(this.form)
+          }
+          updateConfigByKey(param).then(response => {
+            if (response.code === 200) {
+              this.msgSuccess('修改成功')
+            }
+          }).finally(() => {
+            this.submitLoading = false
+          })
+        }
+      })
+    }
   }
 }
 </script>
+
+<style scoped>
+.section-card {
+  margin-bottom: 20px;
+}
+.footer {
+  width: 100%;
+  display: flex;
+  margin-top: 30px;
+  align-items: flex-end;
+  justify-content: flex-end;
+}
+</style>

+ 40 - 36
src/views/admin/product/index.vue

@@ -1,40 +1,42 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
-      <el-form-item label="租户名称" prop="companyName">
-        <el-input
-          v-model="queryParams.companyName"
-          placeholder="请输入租户名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="商品名称" prop="productName">
-        <el-input
-          v-model="queryParams.productName"
-          placeholder="请输入商品名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="商品状态" prop="status">
-        <el-select v-model="queryParams.status" placeholder="请选择商品状态" clearable size="small">
-          <el-option
-            v-for="item in statusOptions"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value">
-          </el-option>
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <inline-tenant-selector @change="onTenantChange" />
-      <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 shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" size="small" label-width="100px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input
+            v-model="queryParams.companyName"
+            placeholder="请输入租户名称"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="商品名称" prop="productName">
+          <el-input
+            v-model="queryParams.productName"
+            placeholder="请输入商品名称"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="商品状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="请选择商品状态" clearable size="small">
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <inline-tenant-selector @change="onTenantChange" />
+        <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>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -69,7 +71,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="productList">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="productList">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="租户名称" align="center" prop="companyName" />
       <el-table-column label="商品名称" align="center" prop="productName" />
@@ -366,4 +368,6 @@ export default {
   font-size: 14px;
   color: #666;
 }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

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

@@ -2,21 +2,21 @@
   <div class="app-container">
     <!-- ===== 搜索栏 ===== -->
     <el-card shadow="never" class="mb16 filter-card">
-      <el-form :model="proxyQuery" ref="proxyQueryForm" :inline="true" size="small">
-        <el-form-item label="关键词" prop="proxyName">
-          <el-input v-model="proxyQuery.proxyName" placeholder="用户名/姓名/编码" clearable @keyup.enter.native="loadProxyList" />
-        </el-form-item>
-        <el-form-item label="状态" prop="status">
-          <el-select v-model="proxyQuery.status" placeholder="全部" clearable style="width:120px">
-            <el-option label="启用" :value="1" />
-            <el-option label="禁用" :value="0" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="el-icon-search" @click="loadProxyList">查询</el-button>
-          <el-button icon="el-icon-refresh" @click="resetProxyQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
+    <el-form :model="proxyQuery" ref="proxyQueryForm" :inline="true" size="small">
+      <el-form-item label="关键词" prop="proxyName">
+        <el-input v-model="proxyQuery.proxyName" placeholder="用户名/姓名/编码" clearable @keyup.enter.native="loadProxyList" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="proxyQuery.status" placeholder="全部" clearable style="width:120px">
+          <el-option label="启用" :value="1" />
+          <el-option label="禁用" :value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="loadProxyList">查询</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetProxyQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
     </el-card>
 
     <!-- ===== 操作栏 ===== -->
@@ -233,7 +233,7 @@ export default {
 </script>
 
 <style scoped>
-.filter-card { padding-bottom: 0; }
-.mb16 { margin-bottom: 16px; }
 .mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

+ 53 - 46
src/views/admin/qwExternalContact/index.vue

@@ -1,49 +1,51 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
-      <el-form-item label="租户名称" prop="companyName">
-        <el-input
-          v-model="queryParams.companyName"
-          placeholder="请输入租户名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="客户名称" prop="externalContactName">
-        <el-input
-          v-model="queryParams.externalContactName"
-          placeholder="请输入客户名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="客户手机号" prop="phone">
-        <el-input
-          v-model="queryParams.phone"
-          placeholder="请输入客户手机号"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="客户状态" prop="status">
-        <el-select v-model="queryParams.status" placeholder="请选择客户状态" clearable size="small">
-          <el-option
-            v-for="item in statusOptions"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value">
-          </el-option>
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <inline-tenant-selector @change="handleQuery" />
-      <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 shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" size="small" label-width="100px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input
+            v-model="queryParams.companyName"
+            placeholder="请输入租户名称"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="客户名称" prop="externalContactName">
+          <el-input
+            v-model="queryParams.externalContactName"
+            placeholder="请输入客户名称"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="客户手机号" prop="phone">
+          <el-input
+            v-model="queryParams.phone"
+            placeholder="请输入客户手机号"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="客户状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="请选择客户状态" clearable size="small">
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <inline-tenant-selector @change="handleQuery" />
+        <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>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -60,7 +62,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="externalContactList">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="externalContactList">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="租户名称" align="center" prop="companyName" />
       <el-table-column label="客户名称" align="center" prop="externalContactName" />
@@ -198,4 +200,9 @@ export default {
     }
   }
 }
-</script>
+</script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 10 - 2
src/views/admin/rechargeRecord/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <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 size="small" @keyup.enter.native="handleQuery" />
       </el-form-item>
@@ -24,6 +25,7 @@
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -32,7 +34,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
     </el-row>
 
-    <el-table border v-loading="loading" :data="recordList">
+    <el-table v-loading="loading" :data="recordList" border size="small" style="width:100%">
       <el-table-column type="index" label="序号" width="55" align="center" />
       <el-table-column label="租户ID" align="center" prop="tenantId" width="80" />
       <el-table-column label="租户名称" align="center" prop="tenantName" min-width="140" />
@@ -158,3 +160,9 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 240 - 6
src/views/admin/sms/index.vue

@@ -1,19 +1,253 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>短信管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <!-- 统计卡片 -->
+    <el-row :gutter="16" class="mb16">
+      <el-col :span="6">
+        <el-card shadow="never" class="stat-card">
+          <div class="stat-label">租户总数</div>
+          <div class="stat-value">{{ smsCount.totalCount || 0 }}</div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="never" class="stat-card">
+          <div class="stat-label">短信剩余总量</div>
+          <div class="stat-value" style="color:#1890ff">{{ smsCount.totalRemain || 0 }}</div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="never" class="stat-card">
+          <div class="stat-label">正常租户数</div>
+          <div class="stat-value" style="color:#52c41a">{{ smsCount.activeCount || 0 }}</div>
+        </el-card>
+      </el-col>
+      <el-col :span="6">
+        <el-card shadow="never" class="stat-card">
+          <div class="stat-label">禁用租户数</div>
+          <div class="stat-value" style="color:#ff4d4f">{{ smsCount.disabledCount || 0 }}</div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 搜索栏 -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable @keyup.enter.native="handleQuery" />
+        </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>
+
+    <!-- 操作栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" :loading="exportLoading" @click="handleExport">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- 表格 -->
+    <el-table v-loading="loading" :data="smsList" border size="small" style="width:100%">
+      <el-table-column label="ID" align="center" prop="smsId" width="80" />
+      <el-table-column label="所属租户" align="center" prop="companyName" min-width="140" />
+      <el-table-column label="短信剩余条数" align="center" prop="smsRemain" width="130">
+        <template slot-scope="scope">
+          <span style="color:#1890ff;font-weight:bold">{{ scope.row.smsRemain || 0 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === '0' || scope.row.status === 0" type="success" size="mini">正常</el-tag>
+          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+      <el-table-column label="操作" align="center" width="100" fixed="right">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <!-- 编辑弹窗 -->
+    <el-dialog title="编辑短信配置" :visible.sync="editVisible" width="500px" append-to-body>
+      <el-form ref="editForm" :model="editForm" :rules="editRules" label-width="110px">
+        <el-form-item label="所属租户">
+          <span>{{ editForm.companyName }}</span>
+        </el-form-item>
+        <el-form-item label="短信剩余条数">
+          <span style="color:#1890ff;font-weight:bold">{{ editForm.smsRemain || 0 }}</span>
+        </el-form-item>
+        <el-form-item label="调整条数" prop="adjustCount">
+          <el-input-number v-model="editForm.adjustCount" :min="1" :precision="0" style="width:200px" />
+          <el-radio-group v-model="editForm.adjustType" style="margin-left:10px">
+            <el-radio label="add">增加</el-radio>
+            <el-radio label="reduce">减少</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="editForm.status" placeholder="请选择状态" style="width:200px">
+            <el-option label="正常" value="0" />
+            <el-option label="禁用" value="1" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="editForm.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="editVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="submitEdit">确 定</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import request from '@/utils/request'
+
 export default {
   name: 'AdminSms',
   data() {
-    return {}
+    return {
+      loading: false,
+      exportLoading: false,
+      submitLoading: false,
+      showSearch: true,
+      total: 0,
+      smsList: [],
+      smsCount: {},
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyName: null
+      },
+      editVisible: false,
+      editForm: {
+        smsId: null,
+        companyName: '',
+        smsRemain: 0,
+        adjustCount: 1,
+        adjustType: 'add',
+        status: '0',
+        remark: ''
+      },
+      editRules: {
+        adjustCount: [{ required: true, message: '请输入调整条数', trigger: 'blur' }],
+        status: [{ required: true, message: '请选择状态', trigger: 'change' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+    this.getCount()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      request({
+        url: '/company/companySms/list',
+        method: 'get',
+        params: this.queryParams
+      }).then(res => {
+        this.smsList = res.rows || []
+        this.total = res.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    getCount() {
+      request({
+        url: '/company/companySms/getCompanySmsCount',
+        method: 'get'
+      }).then(res => {
+        this.smsCount = res.data || {}
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleEdit(row) {
+      request({
+        url: '/company/companySms/' + row.smsId,
+        method: 'get'
+      }).then(res => {
+        const data = res.data || {}
+        this.editForm = {
+          smsId: data.smsId,
+          companyName: data.companyName,
+          smsRemain: data.smsRemain,
+          adjustCount: 1,
+          adjustType: 'add',
+          status: String(data.status || '0'),
+          remark: ''
+        }
+        this.editVisible = true
+      })
+    },
+    submitEdit() {
+      this.$refs['editForm'].validate(valid => {
+        if (!valid) return
+        this.submitLoading = true
+        const data = {
+          smsId: this.editForm.smsId,
+          status: this.editForm.status,
+          remark: this.editForm.remark
+        }
+        // 计算调整后的短信条数
+        if (this.editForm.adjustType === 'add') {
+          data.smsRemain = (this.editForm.smsRemain || 0) + (this.editForm.adjustCount || 0)
+        } else {
+          data.smsRemain = Math.max(0, (this.editForm.smsRemain || 0) - (this.editForm.adjustCount || 0))
+        }
+        request({
+          url: '/company/companySms',
+          method: 'put',
+          data: data
+        }).then(() => {
+          this.$message.success('修改成功')
+          this.editVisible = false
+          this.getList()
+          this.getCount()
+        }).finally(() => { this.submitLoading = false })
+      })
+    },
+    handleExport() {
+      this.exportLoading = true
+      request({
+        url: '/company/companySms/export',
+        method: 'get',
+        params: this.queryParams,
+        responseType: 'blob'
+      }).then(res => {
+        const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+        const link = document.createElement('a')
+        link.href = URL.createObjectURL(blob)
+        link.download = '短信管理.xlsx'
+        link.click()
+        URL.revokeObjectURL(link.href)
+      }).catch(() => {
+        this.$message.error('导出失败')
+      }).finally(() => { this.exportLoading = false })
+    }
   }
 }
 </script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+.stat-card { text-align: center; }
+.stat-label { font-size: 13px; color: #909399; margin-bottom: 8px; }
+.stat-value { font-size: 24px; font-weight: bold; color: #303133; }
+</style>

+ 146 - 6
src/views/admin/smsOrder/index.vue

@@ -1,19 +1,159 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>短信订单</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <!-- 搜索栏 -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable @keyup.enter.native="handleQuery" />
+        </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>
+
+    <!-- 操作栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" :loading="exportLoading" @click="handleExport">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- 表格 -->
+    <el-table v-loading="loading" :data="orderList" border size="small" style="width:100%">
+      <el-table-column label="订单ID" align="center" prop="orderId" width="80" />
+      <el-table-column label="所属租户" align="center" prop="companyName" min-width="140" />
+      <el-table-column label="套餐名称" align="center" prop="packageName" min-width="140" />
+      <el-table-column label="购买条数" align="center" prop="smsCount" width="110">
+        <template slot-scope="scope">
+          <span style="font-weight:bold">{{ scope.row.smsCount || 0 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="金额" align="center" prop="amount" width="120">
+        <template slot-scope="scope">
+          <span style="color:#fa8c16;font-weight:bold">¥{{ scope.row.amount ? Number(scope.row.amount).toFixed(2) : '0.00' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="购买时间" align="center" prop="createTime" width="160" />
+      <el-table-column label="状态" align="center" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === '0' || scope.row.status === 0" type="success" size="mini">成功</el-tag>
+          <el-tag v-else-if="scope.row.status === '1' || scope.row.status === 1" type="warning" size="mini">待支付</el-tag>
+          <el-tag v-else-if="scope.row.status === '2' || scope.row.status === 2" type="danger" size="mini">已取消</el-tag>
+          <el-tag v-else type="info" size="mini">{{ scope.row.status }}</el-tag>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+    <!-- 详情弹窗 -->
+    <el-dialog title="订单详情" :visible.sync="detailVisible" width="550px" append-to-body>
+      <el-descriptions :column="2" border size="small">
+        <el-descriptions-item label="订单ID">{{ detail.orderId }}</el-descriptions-item>
+        <el-descriptions-item label="所属租户">{{ detail.companyName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="套餐名称">{{ detail.packageName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="购买条数">
+          <span style="font-weight:bold">{{ detail.smsCount || 0 }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="金额">
+          <span style="color:#fa8c16;font-weight:bold">¥{{ detail.amount ? Number(detail.amount).toFixed(2) : '0.00' }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag v-if="detail.status === '0' || detail.status === 0" type="success" size="mini">成功</el-tag>
+          <el-tag v-else-if="detail.status === '1' || detail.status === 1" type="warning" size="mini">待支付</el-tag>
+          <el-tag v-else-if="detail.status === '2' || detail.status === 2" type="danger" size="mini">已取消</el-tag>
+          <el-tag v-else type="info" size="mini">{{ detail.status }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="购买时间">{{ detail.createTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="支付方式">{{ detail.payMethod || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="交易流水号" :span="2">{{ detail.txnNo || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import request from '@/utils/request'
+
 export default {
   name: 'AdminSmsOrder',
   data() {
-    return {}
+    return {
+      loading: false,
+      exportLoading: false,
+      showSearch: true,
+      total: 0,
+      orderList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyName: null
+      },
+      detailVisible: false,
+      detail: {}
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      request({
+        url: '/company/companySmsOrder/list',
+        method: 'get',
+        params: this.queryParams
+      }).then(res => {
+        this.orderList = res.rows || []
+        this.total = res.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleDetail(row) {
+      request({
+        url: '/company/companySmsOrder/' + row.orderId,
+        method: 'get'
+      }).then(res => {
+        this.detail = res.data || {}
+        this.detailVisible = true
+      })
+    },
+    handleExport() {
+      this.exportLoading = true
+      request({
+        url: '/company/companySmsOrder/export',
+        method: 'get',
+        params: this.queryParams,
+        responseType: 'blob'
+      }).then(res => {
+        const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+        const link = document.createElement('a')
+        link.href = URL.createObjectURL(blob)
+        link.download = '短信订单.xlsx'
+        link.click()
+        URL.revokeObjectURL(link.href)
+      }).catch(() => {
+        this.$message.error('导出失败')
+      }).finally(() => { this.exportLoading = false })
+    }
   }
 }
 </script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 97 - 6
src/views/admin/smsPackage/index.vue

@@ -1,19 +1,110 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>短信套餐</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <!-- 搜索栏 -->
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
+        <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>
+
+    <!-- 操作栏 -->
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- 表格 -->
+    <el-table v-loading="loading" :data="packageList" border size="small" style="width:100%">
+      <el-table-column label="套餐ID" align="center" prop="packageId" width="80" />
+      <el-table-column label="套餐名称" align="center" prop="packageName" min-width="160" />
+      <el-table-column label="短信条数" align="center" prop="smsCount" width="120">
+        <template slot-scope="scope">
+          <span style="font-weight:bold">{{ scope.row.smsCount || 0 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="价格" align="center" prop="price" width="120">
+        <template slot-scope="scope">
+          <span style="color:#fa8c16;font-weight:bold">¥{{ scope.row.price ? Number(scope.row.price).toFixed(2) : '0.00' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="单价" align="center" width="120">
+        <template slot-scope="scope">
+          <span v-if="scope.row.smsCount > 0">¥{{ (scope.row.price / scope.row.smsCount).toFixed(4) }}/条</span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === '0' || scope.row.status === 0" type="success" size="mini">启用</el-tag>
+          <el-tag v-else type="info" size="mini">停用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
   </div>
 </template>
 
 <script>
+import request from '@/utils/request'
+
 export default {
   name: 'AdminSmsPackage',
   data() {
-    return {}
+    return {
+      loading: false,
+      showSearch: true,
+      total: 0,
+      packageList: [],
+      statusOptions: [
+        { value: '0', label: '启用' },
+        { value: '1', label: '停用' }
+      ],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        status: null
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      request({
+        url: '/company/companySmsPackage/list',
+        method: 'get',
+        params: this.queryParams
+      }).then(res => {
+        this.packageList = res.rows || []
+        this.total = res.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    }
   }
 }
 </script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

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

@@ -5,19 +5,21 @@
       <el-tab-pane label="SOP模板" name="temp" />
     </el-tabs>
 
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
-      <el-form-item label="租户名称" prop="companyName">
-        <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item :label="activeTab==='sop'?'SOP名称':'模板名称'" prop="sopName">
-        <el-input v-model="queryParams.sopName" placeholder="请输入名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item>
-        <inline-tenant-selector @change="onTenantChange" />
-        <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 shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" size="small" label-width="100px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="activeTab==='sop'?'SOP名称':'模板名称'" prop="sopName">
+          <el-input v-model="queryParams.sopName" placeholder="请输入名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <inline-tenant-selector @change="onTenantChange" />
+          <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>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -26,7 +28,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="list">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="list">
       <el-table-column label="租户名称" align="center" prop="companyName" min-width="150" />
       <el-table-column label="名称" align="center" prop="name" />
       <el-table-column label="状态" align="center" prop="status" width="100" />
@@ -73,3 +75,8 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

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

@@ -1,18 +1,20 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
-      <el-form-item label="租户名称" prop="companyName">
-        <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item label="订单编号" prop="orderNo">
-        <el-input v-model="queryParams.orderNo" placeholder="请输入订单编号" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item>
-        <inline-tenant-selector @change="onTenantChange" />
-        <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 shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" size="small" label-width="100px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="订单编号" prop="orderNo">
+          <el-input v-model="queryParams.orderNo" placeholder="请输入订单编号" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <inline-tenant-selector @change="onTenantChange" />
+          <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>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -21,7 +23,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="list">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="list">
       <el-table-column label="租户名称" align="center" prop="companyName" min-width="150" />
       <el-table-column label="订单编号" align="center" prop="orderNo" width="180" />
       <el-table-column label="订单金额" align="center" prop="orderAmount" width="120" />
@@ -68,3 +70,8 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 23 - 19
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="companyName">
-          <el-input v-model="queryParams.companyName" 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" @click="handleQuery">查询</el-button>
-          <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
+    <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
+      <el-form-item label="租户名称" prop="companyName">
+        <el-input v-model="queryParams.companyName" 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>
 
     <!-- ===== 操作栏 ===== -->
@@ -143,7 +143,7 @@
 </template>
 
 <script>
-import { listAllCompanies, getCompanyInfo, disableCompany, enableCompany } from '@/api/admin/sysCompany'
+import { listAllCompanies, getCompanyInfo, disableCompany, enableCompany, rechargeCompany } from '@/api/admin/sysCompany'
 
 export default {
   name: 'SysCompanyAdmin',
@@ -224,8 +224,12 @@ export default {
     submitRecharge() {
       this.$refs['rechargeForm'].validate(valid => {
         if (!valid) return
-        this.$message.info('充值/扣款功能开发中,待后端API对接')
-        this.rechargeOpen = false
+        const { companyId, operateType, amount, remark } = this.rechargeForm
+        rechargeCompany(companyId, { operateType, amount, remark }).then(() => {
+          this.$message.success(operateType === 'recharge' ? '充值成功' : '扣款成功')
+          this.rechargeOpen = false
+          this.getList()
+        })
       })
     },
     /** 禁用租户 */
@@ -260,7 +264,7 @@ export default {
 </script>
 
 <style scoped>
-.filter-card { padding-bottom: 0; }
-.mb16 { margin-bottom: 16px; }
 .mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
 </style>

+ 12 - 4
src/views/admin/sysUser/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <el-card shadow="never" class="mb16 filter-card">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
       <el-form-item label="租户名称" prop="companyName">
         <el-input
           v-model="queryParams.companyName"
@@ -43,6 +44,7 @@
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -68,7 +70,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="userList">
+    <el-table v-loading="loading" :data="userList" border size="small" style="width:100%">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="租户名称" align="center" prop="companyName" />
       <el-table-column label="员工账号" align="center" prop="loginName" />
@@ -151,7 +153,7 @@
 
     <!-- 变化记录弹窗 -->
     <el-dialog title="员工账户变化记录" :visible.sync="showChangeLogs" width="800px" append-to-body>
-      <el-table border :data="changeLogList">
+      <el-table :data="changeLogList" border>
         <el-table-column label="租户名称" align="center" prop="companyName" />
         <el-table-column label="员工账号" align="center" prop="loginName" />
         <el-table-column label="员工名称" align="center" prop="userName" />
@@ -282,4 +284,10 @@ export default {
     }
   }
 }
-</script>
+</script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 40 - 4
src/views/admin/textModel/index.vue

@@ -2,7 +2,7 @@
   <div class="app-container">
     <el-card shadow="never">
       <div slot="header"><span>文本模型配置</span></div>
-      <el-table :data="models" border style="width: 100%">
+      <el-table :data="models" border size="small" style="width: 100%">
         <el-table-column label="模型名称" width="120">
           <template slot-scope="scope">
             <el-tag>{{ scope.row.name }}</el-tag>
@@ -25,17 +25,20 @@
         </el-table-column>
       </el-table>
       <div style="margin-top: 20px; text-align: right;">
-        <el-button type="primary" @click="handleSave">保存配置</el-button>
+        <el-button type="primary" :loading="saving" @click="handleSave">保存配置</el-button>
       </div>
     </el-card>
   </div>
 </template>
 
 <script>
+import request from '@/utils/request'
+
 export default {
   name: 'AdminTextModel',
   data() {
     return {
+      saving: false,
       models: [
         { name: '豆包', apiKey: '', apiUrl: '', modelId: '', placeholder: '如 doubao-pro-32k' },
         { name: 'DeepSeek', apiKey: '', apiUrl: '', modelId: '', placeholder: '如 deepseek-chat' },
@@ -43,10 +46,43 @@ export default {
       ]
     }
   },
+  created() {
+    this.loadConfig()
+  },
   methods: {
+    loadConfig() {
+      request({ url: '/system/config/getConfigByKey/text.model.config', method: 'get' }).then(r => {
+        if (r.msg && r.msg !== 'text.model.config') {
+          // r.msg is the configValue string
+          try {
+            const saved = JSON.parse(r.msg)
+            if (Array.isArray(saved) && saved.length > 0) {
+              // 合并:保留默认placeholder等,覆盖已保存值
+              this.models = this.models.map((m, i) => {
+                if (saved[i]) {
+                  return { ...m, ...saved[i] }
+                }
+                return m
+              })
+            }
+          } catch (e) {
+            console.warn('解析文本模型配置失败:', e)
+          }
+        }
+      }).catch(() => {})
+    },
     handleSave() {
-      this.$message.success('配置保存成功')
-      console.log('保存模型配置:', JSON.stringify(this.models))
+      this.saving = true
+      const data = {
+        configKey: 'text.model.config',
+        configName: '文本模型配置',
+        configValue: JSON.stringify(this.models)
+      }
+      request({ url: '/system/config/updateConfigByKey', method: 'post', data }).then(() => {
+        this.$message.success('配置保存成功')
+      }).finally(() => {
+        this.saving = false
+      })
     }
   }
 }

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

@@ -1,20 +1,22 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
-      <el-form-item label="租户名称" prop="companyName">
-        <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item label="视频名称" prop="videoName">
-        <el-input v-model="queryParams.videoName" placeholder="请输入视频名称" clearable size="small" @keyup.enter.native="handleQuery" />
-      </el-form-item>
-      <el-form-item>
-        <inline-tenant-selector @change="onTenantChange" />
-        <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 shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" size="small" label-width="100px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="视频名称" prop="videoName">
+          <el-input v-model="queryParams.videoName" placeholder="请输入视频名称" clearable size="small" @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <inline-tenant-selector @change="onTenantChange" />
+          <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>
 
-    <el-table v-loading="loading" :data="list">
+    <el-table border size="small" style="width:100%" v-loading="loading" :data="list">
       <el-table-column label="租户名称" prop="companyName" width="160" />
       <el-table-column label="视频名称" prop="videoName" :show-overflow-tooltip="true" />
       <el-table-column label="视频类型" prop="videoType" width="100" />
@@ -84,3 +86,8 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 180 - 6
src/views/admin/voice/index.vue

@@ -1,19 +1,193 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>外呼管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="100px">
+        <el-form-item label="任务名称" prop="taskName">
+          <el-input
+            v-model="queryParams.taskName"
+            placeholder="请输入任务名称"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input
+            v-model="queryParams.companyName"
+            placeholder="请输入租户名称"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
+            <el-option label="待启动" :value="0" />
+            <el-option label="运行中" :value="1" />
+            <el-option label="已停止" :value="2" />
+            <el-option label="已完成" :value="3" />
+          </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>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="dataList" border size="small" style="width:100%">
+      <el-table-column label="任务名称" align="center" prop="taskName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="所属租户" align="center" prop="companyName" min-width="120" show-overflow-tooltip />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 0" type="info">待启动</el-tag>
+          <el-tag v-else-if="scope.row.status === 1" type="success">运行中</el-tag>
+          <el-tag v-else-if="scope.row.status === 2" type="danger">已停止</el-tag>
+          <el-tag v-else-if="scope.row.status === 3" type="warning">已完成</el-tag>
+          <el-tag v-else type="info">未知</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="外呼总数" align="center" prop="callTotal" width="100" />
+      <el-table-column label="成功数" align="center" prop="callSuccess" width="100" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+      <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-view"
+            @click="handleDetail(scope.row)"
+          >详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 详情弹窗 -->
+    <el-dialog title="任务详情" :visible.sync="detailOpen" width="650px" append-to-body>
+      <el-form :model="detailForm" label-width="100px" size="small">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="任务名称">
+              <span>{{ detailForm.taskName }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属租户">
+              <span>{{ detailForm.companyName }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态">
+              <el-tag v-if="detailForm.status === 0" type="info">待启动</el-tag>
+              <el-tag v-else-if="detailForm.status === 1" type="success">运行中</el-tag>
+              <el-tag v-else-if="detailForm.status === 2" type="danger">已停止</el-tag>
+              <el-tag v-else-if="detailForm.status === 3" type="warning">已完成</el-tag>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="外呼总数">
+              <span>{{ detailForm.callTotal }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="成功数">
+              <span>{{ detailForm.callSuccess }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="创建时间">
+              <span>{{ detailForm.createTime }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="任务描述">
+              <span>{{ detailForm.remark || '-' }}</span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import { listRobotic, getRobotic, exportRobotic } from '@/api/company/companyVoiceRobotic'
+
 export default {
   name: 'AdminVoice',
   data() {
-    return {}
+    return {
+      loading: true,
+      exportLoading: false,
+      showSearch: true,
+      total: 0,
+      dataList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        taskName: null,
+        companyName: null,
+        status: null
+      },
+      detailOpen: false,
+      detailForm: {}
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listRobotic(this.queryParams).then(response => {
+        this.dataList = response.rows
+        this.total = response.total
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleDetail(row) {
+      getRobotic(row.id).then(response => {
+        this.detailForm = response.data
+        this.detailOpen = true
+      })
+    },
+    handleExport() {
+      this.exportLoading = true
+      exportRobotic(this.queryParams).then(response => {
+        this.download(response.msg)
+        this.exportLoading = false
+      }).catch(() => {
+        this.exportLoading = false
+      })
+    }
   }
 }
 </script>

+ 149 - 6
src/views/admin/voiceApi/index.vue

@@ -1,19 +1,162 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>通话接口管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="100px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input
+            v-model="queryParams.companyName"
+            placeholder="请输入租户名称"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </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>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="dataList" border size="small" style="width:100%">
+      <el-table-column label="接口ID" align="center" prop="apiId" width="80" />
+      <el-table-column label="所属租户" align="center" prop="companyName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="接口类型" align="center" prop="apiType" width="120">
+        <template slot-scope="scope">
+          <span>{{ getApiTypeLabel(scope.row.apiType) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="接口名称" align="center" prop="apiName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 0 || scope.row.status === '0'" type="danger">禁用</el-tag>
+          <el-tag v-else type="success">启用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+      <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-view"
+            @click="handleDetail(scope.row)"
+          >详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 详情弹窗 -->
+    <el-dialog title="接口详情" :visible.sync="detailOpen" width="600px" append-to-body>
+      <el-form :model="detailForm" label-width="100px" size="small">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="接口ID">
+              <span>{{ detailForm.apiId }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属租户">
+              <span>{{ detailForm.companyName }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="接口类型">
+              <span>{{ getApiTypeLabel(detailForm.apiType) }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="接口名称">
+              <span>{{ detailForm.apiName }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态">
+              <el-tag v-if="detailForm.status === 0 || detailForm.status === '0'" type="danger">禁用</el-tag>
+              <el-tag v-else type="success">启用</el-tag>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="创建时间">
+              <span>{{ detailForm.createTime }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <span>{{ detailForm.remark || '-' }}</span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import { listCompanyVoiceApi, getCompanyVoiceApi } from '@/api/company/companyVoiceApi'
+
 export default {
   name: 'AdminVoiceApi',
   data() {
-    return {}
+    return {
+      loading: true,
+      showSearch: true,
+      total: 0,
+      dataList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyName: null
+      },
+      detailOpen: false,
+      detailForm: {},
+      apiTypeMap: {
+        0: 'SIP',
+        1: '网关',
+        2: 'API'
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listCompanyVoiceApi(this.queryParams).then(response => {
+        this.dataList = response.rows
+        this.total = response.total
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleDetail(row) {
+      getCompanyVoiceApi(row.apiId).then(response => {
+        this.detailForm = response.data
+        this.detailOpen = true
+      })
+    },
+    getApiTypeLabel(type) {
+      return this.apiTypeMap[type] || '未知'
+    }
   }
 }
 </script>

+ 263 - 6
src/views/admin/voiceBlacklist/index.vue

@@ -1,19 +1,276 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>黑名单管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="100px">
+        <el-form-item label="手机号" prop="phone">
+          <el-input
+            v-model="queryParams.phone"
+            placeholder="请输入手机号"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input
+            v-model="queryParams.companyName"
+            placeholder="请输入租户名称"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </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>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <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="companyName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="1"
+            :inactive-value="0"
+            @change="handleStatusChange(scope.row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >编辑</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="500px" append-to-body>
+      <el-form ref="editForm" :model="editForm" :rules="editRules" label-width="100px" size="small">
+        <el-form-item label="手机号" prop="phone">
+          <el-input v-model="editForm.phone" placeholder="请输入手机号" maxlength="11" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="editForm.status">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="editForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="dialogOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import {
+  listVoiceRoboticCallBlacklist,
+  getVoiceRoboticCallBlacklist,
+  addVoiceRoboticCallBlacklist,
+  updateVoiceRoboticCallBlacklist,
+  delVoiceRoboticCallBlacklist,
+  changeVoiceRoboticCallBlacklistStatus
+} from '@/api/company/companyVoiceRoboticCallBlacklist'
+
 export default {
   name: 'AdminVoiceBlacklist',
   data() {
-    return {}
+    return {
+      loading: true,
+      exportLoading: false,
+      showSearch: true,
+      total: 0,
+      dataList: [],
+      // 选中数组
+      ids: [],
+      // 非多个禁用
+      multiple: true,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        phone: null,
+        companyName: null
+      },
+      // 弹窗
+      dialogOpen: false,
+      dialogTitle: '',
+      editForm: {
+        blacklistId: null,
+        phone: null,
+        status: 1,
+        remark: null
+      },
+      editRules: {
+        phone: [
+          { required: true, message: '请输入手机号', trigger: 'blur' },
+          { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listVoiceRoboticCallBlacklist(this.queryParams).then(response => {
+        this.dataList = response.rows
+        this.total = response.total
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.blacklistId)
+      this.multiple = !selection.length
+    },
+    handleAdd() {
+      this.resetEditForm()
+      this.dialogTitle = '新增黑名单'
+      this.dialogOpen = true
+    },
+    handleUpdate(row) {
+      this.resetEditForm()
+      const id = row.blacklistId
+      getVoiceRoboticCallBlacklist(id).then(response => {
+        this.editForm = response.data
+        this.dialogTitle = '编辑黑名单'
+        this.dialogOpen = true
+      })
+    },
+    submitForm() {
+      this.$refs['editForm'].validate(valid => {
+        if (valid) {
+          if (this.editForm.blacklistId) {
+            updateVoiceRoboticCallBlacklist(this.editForm).then(response => {
+              this.$message.success('修改成功')
+              this.dialogOpen = false
+              this.getList()
+            })
+          } else {
+            addVoiceRoboticCallBlacklist(this.editForm).then(response => {
+              this.$message.success('新增成功')
+              this.dialogOpen = false
+              this.getList()
+            })
+          }
+        }
+      })
+    },
+    handleDelete(row) {
+      const blacklistIds = row.blacklistId ? [row.blacklistId] : this.ids
+      this.$confirm('是否确认删除选中的黑名单记录?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return delVoiceRoboticCallBlacklist(blacklistIds)
+      }).then(() => {
+        this.getList()
+        this.$message.success('删除成功')
+      })
+    },
+    handleStatusChange(row) {
+      changeVoiceRoboticCallBlacklistStatus({
+        blacklistId: row.blacklistId,
+        status: row.status
+      }).then(() => {
+        this.$message.success('状态修改成功')
+      }).catch(() => {
+        row.status = row.status === 1 ? 0 : 1
+      })
+    },
+    handleExport() {
+      this.exportLoading = true
+      this.download('/company/companyVoiceRoboticCallBlacklist/export', { ...this.queryParams }).then(() => {
+        this.exportLoading = false
+      }).catch(() => {
+        this.exportLoading = false
+      })
+    },
+    resetEditForm() {
+      this.editForm = {
+        blacklistId: null,
+        phone: null,
+        status: 1,
+        remark: null
+      }
+      this.$nextTick(() => {
+        if (this.$refs['editForm']) {
+          this.$refs['editForm'].clearValidate()
+        }
+      })
+    }
   }
 }
 </script>

+ 111 - 3
src/views/admin/voiceFrequency/index.vue

@@ -1,19 +1,127 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
+    <el-card v-loading="loading" shadow="never">
       <div slot="header">
         <span>呼叫频率配置</span>
       </div>
-      <el-empty description="功能开发中,敬请期待" />
+      <el-form ref="form" :model="form" label-width="200px">
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>频率限制配置</span></div>
+          <el-form-item label="是否启用频率限制" prop="enabled">
+            <el-switch v-model="form.enabled"></el-switch>
+          </el-form-item>
+          <template v-if="form.enabled">
+            <el-form-item label="每分钟最大呼叫次数" prop="maxCallsPerMinute">
+              <el-input-number v-model="form.maxCallsPerMinute" :min="1" :max="1000" :step="1" :precision="0" placeholder="请输入每分钟最大呼叫次数"></el-input-number>
+              <span class="tip-text">(限制每分钟内允许的最大呼叫次数)</span>
+            </el-form-item>
+            <el-form-item label="每天最大呼叫次数" prop="maxCallsPerDay">
+              <el-input-number v-model="form.maxCallsPerDay" :min="1" :max="100000" :step="1" :precision="0" placeholder="请输入每天最大呼叫次数"></el-input-number>
+              <span class="tip-text">(限制每天允许的最大呼叫次数)</span>
+            </el-form-item>
+            <el-form-item label="两次呼叫间隔(秒)" prop="callIntervalSeconds">
+              <el-input-number v-model="form.callIntervalSeconds" :min="0" :max="3600" :step="1" :precision="0" placeholder="请输入两次呼叫间隔秒数"></el-input-number>
+              <span class="tip-text">(同一号码两次呼叫之间的最小间隔时间)</span>
+            </el-form-item>
+          </template>
+        </el-card>
+
+        <div class="footer">
+          <el-button type="primary" :loading="submitLoading" @click="submitForm">保 存</el-button>
+        </div>
+      </el-form>
     </el-card>
   </div>
 </template>
 
 <script>
+import { getConfigByKey, updateConfigByKey } from '@/api/system/config'
+
 export default {
   name: 'AdminVoiceFrequency',
   data() {
-    return {}
+    return {
+      loading: false,
+      submitLoading: false,
+      configId: null,
+      configKey: 'voice.frequency.config',
+      form: {
+        enabled: false,
+        maxCallsPerMinute: 60,
+        maxCallsPerDay: 1000,
+        callIntervalSeconds: 10
+      }
+    }
+  },
+  created() {
+    this.loadConfig()
+  },
+  methods: {
+    loadConfig() {
+      this.loading = true
+      getConfigByKey(this.configKey).then(response => {
+        if (response.data) {
+          this.configId = response.data.configId
+          try {
+            const parsed = JSON.parse(response.data.configValue)
+            this.form = { ...this.form, ...parsed }
+          } catch (e) {
+            // 使用默认值
+          }
+        } else {
+          this.configId = null
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    submitForm() {
+      if (this.form.enabled) {
+        if (!this.form.maxCallsPerMinute || this.form.maxCallsPerMinute < 1) {
+          this.msgError('每分钟最大呼叫次数不能为空且不能小于1')
+          return false
+        }
+        if (!this.form.maxCallsPerDay || this.form.maxCallsPerDay < 1) {
+          this.msgError('每天最大呼叫次数不能为空且不能小于1')
+          return false
+        }
+        if (this.form.callIntervalSeconds == null || this.form.callIntervalSeconds < 0) {
+          this.msgError('两次呼叫间隔不能为空且不能小于0')
+          return false
+        }
+      }
+      this.submitLoading = true
+      const param = {
+        configId: this.configId,
+        configKey: this.configKey,
+        configValue: JSON.stringify(this.form)
+      }
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('保存成功')
+        }
+      }).finally(() => {
+        this.submitLoading = false
+      })
+    }
   }
 }
 </script>
+
+<style scoped>
+.section-card {
+  margin-bottom: 20px;
+}
+.tip-text {
+  color: #909399;
+  font-size: 12px;
+  margin-left: 8px;
+}
+.footer {
+  width: 100%;
+  display: flex;
+  margin-top: 30px;
+  align-items: flex-end;
+  justify-content: flex-end;
+}
+</style>

+ 59 - 6
src/views/admin/voiceNumber/index.vue

@@ -1,19 +1,72 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>号码管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="80px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable @keyup.enter.native="handleQuery" />
+        </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>
+
+    <el-table v-loading="loading" :data="numberList" border size="small" style="width:100%">
+      <el-table-column label="号码ID" align="center" prop="id" width="80" />
+      <el-table-column label="所属租户" align="center" prop="companyName" min-width="140" />
+      <el-table-column label="号码/CID" align="center" prop="cid" min-width="160" />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === '0' || scope.row.status === 0" type="success" size="small">正常</el-tag>
+          <el-tag v-else-if="scope.row.status === '1' || scope.row.status === 1" type="danger" size="small">停用</el-tag>
+          <el-tag v-else size="small">{{ scope.row.status }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
   </div>
 </template>
 
 <script>
+import { listCidNumber } from '@/api/admin/voiceNumber'
+
 export default {
   name: 'AdminVoiceNumber',
   data() {
-    return {}
+    return {
+      loading: false,
+      total: 0,
+      numberList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyName: null
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listCidNumber(this.queryParams).then(res => {
+        this.numberList = res.rows || []
+        this.total = res.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    }
   }
 }
 </script>

+ 89 - 6
src/views/admin/voiceOrder/index.vue

@@ -1,19 +1,102 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>通话套餐订单</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="80px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input v-model="queryParams.companyName" placeholder="请输入租户名称" clearable @keyup.enter.native="handleQuery" />
+        </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>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" :loading="exportLoading" @click="handleExport">导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <el-table v-loading="loading" :data="orderList" border size="small" style="width:100%">
+      <el-table-column label="订单号" align="center" prop="orderNo" min-width="180" />
+      <el-table-column label="所属租户" align="center" prop="companyName" min-width="140" />
+      <el-table-column label="套餐名称" align="center" prop="packageName" min-width="140" />
+      <el-table-column label="金额" align="center" prop="amount" width="120">
+        <template slot-scope="scope">
+          <span style="color:#E6A23C;font-weight:bold">¥{{ scope.row.amount || '0.00' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="购买时间" align="center" prop="createTime" width="160" />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === '0' || scope.row.status === 0" type="success" size="small">已支付</el-tag>
+          <el-tag v-else-if="scope.row.status === '1' || scope.row.status === 1" type="warning" size="small">待支付</el-tag>
+          <el-tag v-else-if="scope.row.status === '2' || scope.row.status === 2" type="info" size="small">已取消</el-tag>
+          <el-tag v-else size="small">{{ scope.row.status }}</el-tag>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
   </div>
 </template>
 
 <script>
+import { listVoiceOrder, exportVoiceOrder } from '@/api/admin/voiceOrder'
+
 export default {
   name: 'AdminVoiceOrder',
   data() {
-    return {}
+    return {
+      loading: false,
+      exportLoading: false,
+      showSearch: true,
+      total: 0,
+      orderList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyName: null
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listVoiceOrder(this.queryParams).then(res => {
+        this.orderList = res.rows || []
+        this.total = res.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleExport() {
+      this.exportLoading = true
+      exportVoiceOrder(this.queryParams).then(res => {
+        const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+        const link = document.createElement('a')
+        link.href = URL.createObjectURL(blob)
+        link.download = '通话套餐订单.xlsx'
+        link.click()
+        URL.revokeObjectURL(link.href)
+      }).catch(() => {
+        this.$message.error('导出失败')
+      }).finally(() => {
+        this.exportLoading = false
+      })
+    }
   }
 }
 </script>

+ 63 - 6
src/views/admin/voicePackage/index.vue

@@ -1,19 +1,76 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>通话套餐管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="80px">
+        <el-form-item label="套餐名称" prop="packageName">
+          <el-input v-model="queryParams.packageName" placeholder="请输入套餐名称" clearable @keyup.enter.native="handleQuery" />
+        </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>
+
+    <el-table v-loading="loading" :data="packageList" border size="small" style="width:100%">
+      <el-table-column label="套餐ID" align="center" prop="id" width="80" />
+      <el-table-column label="套餐名称" align="center" prop="packageName" min-width="140" />
+      <el-table-column label="套餐分钟数" align="center" prop="packageMinutes" width="120" />
+      <el-table-column label="套餐价格" align="center" prop="packagePrice" width="120">
+        <template slot-scope="scope">
+          <span style="color:#E6A23C;font-weight:bold">¥{{ scope.row.packagePrice || '0.00' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === '0' || scope.row.status === 0" type="success" size="small">启用</el-tag>
+          <el-tag v-else type="info" size="small">停用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
   </div>
 </template>
 
 <script>
+import { listVoicePackage } from '@/api/admin/voicePackage'
+
 export default {
   name: 'AdminVoicePackage',
   data() {
-    return {}
+    return {
+      loading: false,
+      total: 0,
+      packageList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        packageName: null
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listVoicePackage(this.queryParams).then(res => {
+        this.packageList = res.rows || []
+        this.total = res.total || 0
+        this.loading = false
+      }).catch(() => { this.loading = false })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    }
   }
 }
 </script>

+ 157 - 6
src/views/admin/voiceSeat/index.vue

@@ -1,19 +1,170 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
-      <div slot="header">
-        <span>坐席管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="100px">
+        <el-form-item label="租户名称" prop="companyName">
+          <el-input
+            v-model="queryParams.companyName"
+            placeholder="请输入租户名称"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </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>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="dataList" border size="small" style="width:100%">
+      <el-table-column label="坐席ID" align="center" prop="callerId" width="80" />
+      <el-table-column label="所属租户" align="center" prop="companyName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="坐席号码" align="center" prop="callerNumber" min-width="140" />
+      <el-table-column label="坐席名称" align="center" prop="callerName" min-width="120" show-overflow-tooltip />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 0 || scope.row.status === '0'" type="danger">禁用</el-tag>
+          <el-tag v-else type="success">启用</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="绑定时间" align="center" prop="bindTime" width="160" />
+      <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-view"
+            @click="handleDetail(scope.row)"
+          >详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 详情弹窗 -->
+    <el-dialog title="坐席详情" :visible.sync="detailOpen" width="600px" append-to-body>
+      <el-form :model="detailForm" label-width="100px" size="small">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="坐席ID">
+              <span>{{ detailForm.callerId }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属租户">
+              <span>{{ detailForm.companyName }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="坐席号码">
+              <span>{{ detailForm.callerNumber }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="坐席名称">
+              <span>{{ detailForm.callerName }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态">
+              <el-tag v-if="detailForm.status === 0 || detailForm.status === '0'" type="danger">禁用</el-tag>
+              <el-tag v-else type="success">启用</el-tag>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="绑定时间">
+              <span>{{ detailForm.bindTime }}</span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <span>{{ detailForm.remark || '-' }}</span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import { listCompanyVoiceCaller, getCompanyVoiceCaller, exportCompanyVoiceCaller } from '@/api/company/companyVoiceCaller'
+
 export default {
   name: 'AdminVoiceSeat',
   data() {
-    return {}
+    return {
+      loading: true,
+      exportLoading: false,
+      showSearch: true,
+      total: 0,
+      dataList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyName: null
+      },
+      detailOpen: false,
+      detailForm: {}
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listCompanyVoiceCaller(this.queryParams).then(response => {
+        this.dataList = response.rows
+        this.total = response.total
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleDetail(row) {
+      getCompanyVoiceCaller(row.callerId).then(response => {
+        this.detailForm = response.data
+        this.detailOpen = true
+      })
+    },
+    handleExport() {
+      this.exportLoading = true
+      exportCompanyVoiceCaller(this.queryParams).then(response => {
+        this.download(response.msg)
+        this.exportLoading = false
+      }).catch(() => {
+        this.exportLoading = false
+      })
+    }
   }
 }
 </script>

+ 10 - 2
src/views/admin/withdrawalManage/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+    <el-card shadow="never" class="mb16 filter-card">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
       <el-form-item label="代理名称" prop="proxyName">
         <el-input v-model="queryParams.proxyName" placeholder="请输入代理名称" clearable size="small" @keyup.enter.native="handleQuery" />
       </el-form-item>
@@ -29,6 +30,7 @@
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
+    </el-card>
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
@@ -37,7 +39,7 @@
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
     </el-row>
 
-    <el-table border v-loading="loading" :data="recordList">
+    <el-table border v-loading="loading" :data="recordList" size="small" style="width:100%">
       <el-table-column type="index" label="序号" width="55" align="center" />
       <el-table-column label="代理ID" align="center" prop="proxyId" width="70" />
       <el-table-column label="代理名称" align="center" prop="proxyName" min-width="120" />
@@ -218,3 +220,9 @@ export default {
   }
 }
 </script>
+
+<style scoped>
+.mb8 { margin-bottom: 8px; }
+.mb16 { margin-bottom: 16px; }
+.filter-card { padding-bottom: 0; }
+</style>

+ 176 - 3
src/views/admin/wxConfig/index.vue

@@ -1,19 +1,192 @@
 <template>
   <div class="app-container">
-    <el-card shadow="never">
+    <el-card v-loading="loading" shadow="never">
       <div slot="header">
         <span>个微配置</span>
       </div>
-      <el-empty description="功能开发中,敬请期待" />
+      <el-form ref="form" :model="form" label-width="220px">
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>账号配置</span></div>
+          <el-form-item label="新账号判断时间" prop="newAccountTime">
+            <el-input v-model="form.newAccountTime" placeholder="请输入天" style="width: 150px">
+              <template slot="append">天</template>
+            </el-input>
+          </el-form-item>
+          <el-form-item label="新账号每天添加数量" prop="newAccountAddNum">
+            <el-input v-model="form.newAccountAddNum" placeholder="请输入个" style="width: 150px">
+              <template slot="append">个</template>
+            </el-input>
+          </el-form-item>
+          <el-form-item label="普通账号每天添加数量" prop="accountAddNum">
+            <el-input v-model="form.accountAddMin" placeholder="请输入个" style="width: 150px">
+              <template slot="append">个</template>
+            </el-input>
+            <span class="range-separator"> 到 </span>
+            <el-input v-model="form.accountAddMax" placeholder="请输入个" style="width: 150px">
+              <template slot="append">个</template>
+            </el-input>
+          </el-form-item>
+        </el-card>
+
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>消息频率配置</span></div>
+          <el-form-item label="文字消息(条/分)" prop="txtMsg">
+            <el-input v-model="form.txtMsgMinNum" placeholder="请输入条" style="width: 150px">
+              <template slot="append">条</template>
+            </el-input>
+            <span class="range-separator"> 到 </span>
+            <el-input v-model="form.txtMsgMaxNum" placeholder="请输入条" style="width: 150px">
+              <template slot="append">条</template>
+            </el-input>
+          </el-form-item>
+          <el-form-item label="图片消息(条/分)" prop="imgMsg">
+            <el-input v-model="form.imgMsgMinNum" placeholder="请输入条" style="width: 150px">
+              <template slot="append">条</template>
+            </el-input>
+            <span class="range-separator"> 到 </span>
+            <el-input v-model="form.imgMsgMaxNum" placeholder="请输入条" style="width: 150px">
+              <template slot="append">条</template>
+            </el-input>
+          </el-form-item>
+        </el-card>
+
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>新号频率配置</span></div>
+          <el-form-item label="新号添加好友频率(条/分)" prop="newAccountAddWx">
+            <el-input v-model="form.newAccountAddWxMin" placeholder="请输入分" style="width: 150px">
+              <template slot="append">分</template>
+            </el-input>
+            <span class="range-separator"> 到 </span>
+            <el-input v-model="form.newAccountAddWxMax" placeholder="请输入分" style="width: 150px">
+              <template slot="append">分</template>
+            </el-input>
+          </el-form-item>
+          <el-form-item label="新号消息发送频率(条/秒)" prop="newAccountSendMsg">
+            <el-input v-model="form.newAccountSendMsgMin" placeholder="请输入秒" style="width: 150px">
+              <template slot="append">秒</template>
+            </el-input>
+            <span class="range-separator"> 到 </span>
+            <el-input v-model="form.newAccountSendMsgMax" placeholder="请输入秒" style="width: 150px">
+              <template slot="append">秒</template>
+            </el-input>
+          </el-form-item>
+        </el-card>
+
+        <el-card shadow="never" class="section-card">
+          <div slot="header"><span>普通号频率配置</span></div>
+          <el-form-item label="添加好友频率(条/分)" prop="accountAddWx">
+            <el-input v-model="form.accountAddWxMin" placeholder="请输入分" style="width: 150px">
+              <template slot="append">分</template>
+            </el-input>
+            <span class="range-separator"> 到 </span>
+            <el-input v-model="form.accountAddWxMax" placeholder="请输入分" style="width: 150px">
+              <template slot="append">分</template>
+            </el-input>
+          </el-form-item>
+          <el-form-item label="消息发送频率(条/秒)" prop="accountSendMsg">
+            <el-input v-model="form.accountSendMsgMin" placeholder="请输入秒" style="width: 150px">
+              <template slot="append">秒</template>
+            </el-input>
+            <span class="range-separator"> 到 </span>
+            <el-input v-model="form.accountSendMsgMax" placeholder="请输入秒" style="width: 150px">
+              <template slot="append">秒</template>
+            </el-input>
+          </el-form-item>
+        </el-card>
+
+        <div class="footer">
+          <el-button type="primary" :loading="submitLoading" @click="submitForm">提 交</el-button>
+        </div>
+      </el-form>
     </el-card>
   </div>
 </template>
 
 <script>
+import { getConfigByKey, updateConfigByKey } from '@/api/system/config'
+
 export default {
   name: 'AdminWxConfig',
   data() {
-    return {}
+    return {
+      loading: false,
+      submitLoading: false,
+      configId: null,
+      configKey: 'wx.config',
+      form: {
+        newAccountTime: '',
+        newAccountAddNum: '',
+        accountAddMin: '',
+        accountAddMax: '',
+        txtMsgMinNum: '',
+        txtMsgMaxNum: '',
+        imgMsgMinNum: '',
+        imgMsgMaxNum: '',
+        newAccountAddWxMin: '',
+        newAccountAddWxMax: '',
+        newAccountSendMsgMin: '',
+        newAccountSendMsgMax: '',
+        accountAddWxMin: '',
+        accountAddWxMax: '',
+        accountSendMsgMin: '',
+        accountSendMsgMax: ''
+      }
+    }
+  },
+  created() {
+    this.loadConfig()
+  },
+  methods: {
+    loadConfig() {
+      this.loading = true
+      getConfigByKey(this.configKey).then(response => {
+        if (response.data) {
+          this.configId = response.data.configId
+          try {
+            const parsed = JSON.parse(response.data.configValue)
+            this.form = { ...this.form, ...parsed }
+          } catch (e) {
+            // 使用默认值
+          }
+        } else {
+          this.configId = null
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    submitForm() {
+      this.submitLoading = true
+      const param = {
+        configId: this.configId,
+        configKey: this.configKey,
+        configValue: JSON.stringify(this.form)
+      }
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('修改成功')
+        }
+      }).finally(() => {
+        this.submitLoading = false
+      })
+    }
   }
 }
 </script>
+
+<style scoped>
+.section-card {
+  margin-bottom: 20px;
+}
+.range-separator {
+  margin: 0 8px;
+  color: #606266;
+}
+.footer {
+  width: 100%;
+  display: flex;
+  margin-top: 30px;
+  align-items: flex-end;
+  justify-content: flex-end;
+}
+</style>