index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <template>
  2. <div class="app-container">
  3. <!-- ===== 顶部 KPI 概览卡片 ===== -->
  4. <el-row :gutter="16" class="mb16">
  5. <el-col :span="6" v-for="card in overviewCards" :key="card.label">
  6. <el-card shadow="hover" class="overview-card">
  7. <div class="card-inner">
  8. <div class="card-icon" :style="{ background: card.bg }"><i :class="card.icon"></i></div>
  9. <div class="card-info">
  10. <span class="card-value">{{ card.value }}</span>
  11. <span class="card-label">{{ card.label }}</span>
  12. </div>
  13. </div>
  14. </el-card>
  15. </el-col>
  16. </el-row>
  17. <!-- ===== 搜索与操作栏 ===== -->
  18. <el-card shadow="never" class="mb16 filter-card">
  19. <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
  20. <el-form-item label="租户名称" prop="tenantName">
  21. <el-input v-model="queryParams.tenantName" placeholder="请输入租户名称" clearable @keyup.enter.native="handleQuery" />
  22. </el-form-item>
  23. <el-form-item label="归属代理" prop="proxyName">
  24. <el-input v-model="queryParams.proxyName" placeholder="请输入代理名称" clearable @keyup.enter.native="handleQuery" />
  25. </el-form-item>
  26. <el-form-item>
  27. <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
  28. <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
  29. </el-form-item>
  30. </el-form>
  31. </el-card>
  32. <!-- ===== 操作按钮 ===== -->
  33. <el-row :gutter="10" class="mb8">
  34. <el-col :span="1.5">
  35. <el-button type="warning" plain icon="el-icon-download" size="mini" :loading="exportLoading" @click="handleExport">导出</el-button>
  36. </el-col>
  37. <el-col :span="1.5">
  38. <el-button type="primary" plain icon="el-icon-refresh" size="mini" @click="handleRefreshAll">全量统计刷新</el-button>
  39. </el-col>
  40. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  41. </el-row>
  42. <!-- ===== 租户用量汇总表 ===== -->
  43. <el-table border v-loading="loading" :data="list" size="small" style="width:100%" show-summary :summary-method="getSummary">
  44. <el-table-column label="租户名称" align="center" prop="tenantName" min-width="140" fixed="left" />
  45. <el-table-column label="归属代理" align="center" prop="proxyName" min-width="120" />
  46. <!-- AI 模型用量 -->
  47. <el-table-column label="AI Token" align="center" prop="aiTokenTotal" width="100" />
  48. <el-table-column label="AI外呼" align="center" prop="aiCallTotal" width="80" />
  49. <el-table-column label="AI对话" align="center" prop="aiChatTotal" width="80" />
  50. <!-- 通讯 -->
  51. <el-table-column label="外呼次数" align="center" prop="outboundCallCount" width="85" />
  52. <el-table-column label="短信量" align="center" prop="smsSentCount" width="75" />
  53. <!-- 个微 -->
  54. <el-table-column label="个微用户" align="center" prop="wxUserCount" width="85" />
  55. <el-table-column label="个微绑定" align="center" prop="wxAccountCount" width="85" />
  56. <el-table-column label="个微客户" align="center" prop="wxCustomerCount" width="85" />
  57. <!-- 企微 -->
  58. <el-table-column label="企微用户" align="center" prop="qwUserCount" width="85" />
  59. <el-table-column label="企微绑定" align="center" prop="qwAccountCount" width="85" />
  60. <!-- 业务模块 -->
  61. <el-table-column label="SOP" align="center" prop="sopCount" width="60" />
  62. <el-table-column label="课程" align="center" prop="courseCount" width="60" />
  63. <el-table-column label="直播" align="center" prop="liveCount" width="60" />
  64. <el-table-column label="商品" align="center" prop="productCount" width="60" />
  65. <el-table-column label="订单" align="center" prop="orderCount" width="60" />
  66. <!-- 龙虾引擎 -->
  67. <el-table-column label="工作流" align="center" prop="workflowCount" width="75" />
  68. <el-table-column label="龙虾模板" align="center" prop="lobsterTemplateCount" width="85" />
  69. <el-table-column label="龙虾任务" align="center" prop="lobsterTaskCount" width="85" />
  70. <!-- 组织 -->
  71. <el-table-column label="部门" align="center" prop="deptCount" width="60" />
  72. <el-table-column label="员工" align="center" prop="employeeCount" width="60" />
  73. <!-- 汇总 -->
  74. <el-table-column label="活跃模块" align="center" prop="activeModuleCount" width="85">
  75. <template slot-scope="scope">
  76. <el-tag type="success" size="mini">{{ scope.row.activeModuleCount || 0 }}</el-tag>
  77. </template>
  78. </el-table-column>
  79. <el-table-column label="消费总额" align="center" prop="totalConsumeAmount" width="110" fixed="right">
  80. <template slot-scope="scope">
  81. <span style="color:#1890ff;font-weight:bold">¥{{ scope.row.totalConsumeAmount || '0.00' }}</span>
  82. </template>
  83. </el-table-column>
  84. <el-table-column label="操作" align="center" width="80" fixed="right">
  85. <template slot-scope="scope">
  86. <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)">详情</el-button>
  87. </template>
  88. </el-table-column>
  89. </el-table>
  90. <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
  91. <!-- ===== 租户模型用量详情弹窗 ===== -->
  92. <el-dialog title="租户模型用量详情" :visible.sync="detailVisible" width="720px">
  93. <el-descriptions :column="3" border size="small">
  94. <el-descriptions-item label="租户名称" :span="2">{{ detail.tenantName }}</el-descriptions-item>
  95. <el-descriptions-item label="归属代理">{{ detail.proxyName || '-' }}</el-descriptions-item>
  96. <!-- AI模型 -->
  97. <el-descriptions-item label="AI Token总量">
  98. <span style="color:#1890ff;font-weight:bold">{{ detail.aiTokenTotal || 0 }}</span>
  99. </el-descriptions-item>
  100. <el-descriptions-item label="AI外呼总量">
  101. <span style="color:#722ed1;font-weight:bold">{{ detail.aiCallTotal || 0 }}</span>
  102. </el-descriptions-item>
  103. <el-descriptions-item label="AI对话总量">
  104. <span style="color:#52c41a;font-weight:bold">{{ detail.aiChatTotal || 0 }}</span>
  105. </el-descriptions-item>
  106. <!-- 通讯 -->
  107. <el-descriptions-item label="外呼拨打次数">{{ detail.outboundCallCount || 0 }}</el-descriptions-item>
  108. <el-descriptions-item label="短信发送量">{{ detail.smsSentCount || 0 }}</el-descriptions-item>
  109. <el-descriptions-item label="SOP数量">{{ detail.sopCount || 0 }}</el-descriptions-item>
  110. <!-- 个微 -->
  111. <el-descriptions-item label="个微用户数">{{ detail.wxUserCount || 0 }}</el-descriptions-item>
  112. <el-descriptions-item label="绑定个微账号">{{ detail.wxAccountCount || 0 }}</el-descriptions-item>
  113. <el-descriptions-item label="个微客户数">{{ detail.wxCustomerCount || 0 }}</el-descriptions-item>
  114. <!-- 企微 -->
  115. <el-descriptions-item label="企微用户数">{{ detail.qwUserCount || 0 }}</el-descriptions-item>
  116. <el-descriptions-item label="绑定企微号数">{{ detail.qwAccountCount || 0 }}</el-descriptions-item>
  117. <el-descriptions-item label="课程数量">{{ detail.courseCount || 0 }}</el-descriptions-item>
  118. <!-- 业务 -->
  119. <el-descriptions-item label="直播次数">{{ detail.liveCount || 0 }}</el-descriptions-item>
  120. <el-descriptions-item label="商品数量">{{ detail.productCount || 0 }}</el-descriptions-item>
  121. <el-descriptions-item label="订单数量">{{ detail.orderCount || 0 }}</el-descriptions-item>
  122. <!-- 龙虾 -->
  123. <el-descriptions-item label="工作流数量">{{ detail.workflowCount || 0 }}</el-descriptions-item>
  124. <el-descriptions-item label="龙虾引擎模板">{{ detail.lobsterTemplateCount || 0 }}</el-descriptions-item>
  125. <el-descriptions-item label="龙虾引擎任务">{{ detail.lobsterTaskCount || 0 }}</el-descriptions-item>
  126. <!-- 组织 -->
  127. <el-descriptions-item label="部门数量">{{ detail.deptCount || 0 }}</el-descriptions-item>
  128. <el-descriptions-item label="员工数量">{{ detail.employeeCount || 0 }}</el-descriptions-item>
  129. <el-descriptions-item label="活跃模块数">
  130. <el-tag type="success" size="mini">{{ detail.activeModuleCount || 0 }}</el-tag>
  131. </el-descriptions-item>
  132. <!-- 汇总 -->
  133. <el-descriptions-item label="消费总额" :span="3">
  134. <span style="color:#1890ff;font-weight:bold;font-size:16px">¥{{ detail.totalConsumeAmount || '0.00' }}</span>
  135. </el-descriptions-item>
  136. </el-descriptions>
  137. </el-dialog>
  138. </div>
  139. </template>
  140. <script>
  141. import { listModuleUsageSummary, getTenantDetail, refreshStatistics, exportModuleUsage } from '@/api/admin/moduleUsage'
  142. export default {
  143. name: 'AdminModelUsage',
  144. data() {
  145. return {
  146. loading: false,
  147. exportLoading: false,
  148. showSearch: true,
  149. list: [],
  150. total: 0,
  151. detailVisible: false,
  152. detail: {},
  153. queryParams: {
  154. pageNum: 1,
  155. pageSize: 10,
  156. tenantName: null,
  157. proxyName: null
  158. },
  159. // KPI 概览卡片
  160. overviewCards: [
  161. { label: '租户总数', value: 0, icon: 'el-icon-office-building', bg: '#e6f7ff' },
  162. { label: '活跃模块总数', value: 0, icon: 'el-icon-data-line', bg: '#f6ffed' },
  163. { label: '消费总额', value: '¥0.00', icon: 'el-icon-wallet', bg: '#fff7e6' },
  164. { label: 'AI调用总量', value: 0, icon: 'el-icon-cpu', bg: '#f9f0ff' }
  165. ]
  166. }
  167. },
  168. created() {
  169. this.getList()
  170. },
  171. methods: {
  172. getList() {
  173. this.loading = true
  174. listModuleUsageSummary(this.queryParams).then(res => {
  175. this.list = res.rows || []
  176. this.total = res.total || 0
  177. this.updateOverview(this.list)
  178. this.loading = false
  179. }).catch(() => { this.loading = false })
  180. },
  181. /** 根据列表数据更新KPI卡片 */
  182. updateOverview(list) {
  183. const totalModules = list.reduce((s, r) => s + (r.activeModuleCount || 0), 0)
  184. const totalConsume = list.reduce((s, r) => s + (parseFloat(r.totalConsumeAmount) || 0), 0)
  185. const totalAiCalls = list.reduce((s, r) => s + (r.aiCallTotal || 0) + (r.aiChatTotal || 0), 0)
  186. this.overviewCards[0].value = this.total
  187. this.overviewCards[1].value = totalModules
  188. this.overviewCards[2].value = '¥' + totalConsume.toFixed(2)
  189. this.overviewCards[3].value = totalAiCalls
  190. },
  191. /** 表格合计行 */
  192. getSummary({ columns, data }) {
  193. const sums = []
  194. columns.forEach((col, idx) => {
  195. if (idx === 0) { sums[idx] = '合计'; return }
  196. const prop = col.property
  197. if (!prop) { sums[idx] = ''; return }
  198. const numFields = ['aiTokenTotal','aiCallTotal','aiChatTotal','outboundCallCount','smsSentCount',
  199. 'wxUserCount','wxAccountCount','wxCustomerCount','qwUserCount','qwAccountCount',
  200. 'sopCount','courseCount','liveCount','productCount','orderCount',
  201. 'workflowCount','lobsterTemplateCount','lobsterTaskCount','deptCount','employeeCount','activeModuleCount']
  202. if (numFields.includes(prop)) {
  203. const val = data.reduce((s, r) => s + (r[prop] || 0), 0)
  204. sums[idx] = val
  205. } else if (prop === 'totalConsumeAmount') {
  206. const val = data.reduce((s, r) => s + (parseFloat(r[prop]) || 0), 0)
  207. sums[idx] = '¥' + val.toFixed(2)
  208. } else {
  209. sums[idx] = ''
  210. }
  211. })
  212. return sums
  213. },
  214. handleQuery() {
  215. this.queryParams.pageNum = 1
  216. this.getList()
  217. },
  218. resetQuery() {
  219. this.resetForm('queryForm')
  220. this.handleQuery()
  221. },
  222. handleDetail(row) {
  223. getTenantDetail(row.tenantId).then(res => {
  224. this.detail = res.data || {}
  225. this.detailVisible = true
  226. })
  227. },
  228. handleRefreshAll() {
  229. this.$confirm('确定触发全量统计刷新吗?', '提示', { type: 'warning' }).then(() => {
  230. refreshStatistics().then(() => {
  231. this.$message.success('全量统计任务已触发')
  232. this.getList()
  233. })
  234. })
  235. },
  236. handleExport() {
  237. this.exportLoading = true
  238. exportModuleUsage(this.queryParams).then(response => {
  239. this.download(response.msg)
  240. this.exportLoading = false
  241. }).catch(() => { this.exportLoading = false })
  242. }
  243. }
  244. }
  245. </script>
  246. <style scoped>
  247. .overview-card { cursor: default; }
  248. .card-inner { display: flex; align-items: center; }
  249. .card-icon {
  250. width: 46px; height: 46px; border-radius: 50%;
  251. display: flex; align-items: center; justify-content: center;
  252. margin-right: 14px; font-size: 22px; color: #fff;
  253. }
  254. .card-value { font-size: 22px; font-weight: bold; color: #333; display: block; }
  255. .card-label { font-size: 12px; color: #999; }
  256. .filter-card { padding-bottom: 0; }
  257. .mb16 { margin-bottom: 16px; }
  258. .mb8 { margin-bottom: 8px; }
  259. </style>