index.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. <template>
  2. <div class="app-container">
  3. <!-- ===== 搜索栏 ===== -->
  4. <el-card shadow="never" class="mb16 filter-card">
  5. <el-form :model="queryParams" ref="queryForm" :inline="true" size="small">
  6. <el-form-item label="租户名称" prop="tenantName">
  7. <el-input v-model="queryParams.tenantName" placeholder="租户编码/名称" clearable @keyup.enter.native="handleQuery" />
  8. </el-form-item>
  9. <el-form-item label="状态" prop="status">
  10. <el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
  11. <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
  12. </el-select>
  13. </el-form-item>
  14. <el-form-item>
  15. <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
  16. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  17. </el-form-item>
  18. </el-form>
  19. </el-card>
  20. <!-- ===== 操作栏 ===== -->
  21. <el-row :gutter="10" class="mb8">
  22. <el-col :span="1.5">
  23. <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['admin:platform:edit']">新增租户</el-button>
  24. </el-col>
  25. <el-col :span="1.5">
  26. <el-button type="warning" plain icon="el-icon-download" size="mini" :loading="exportLoading" @click="handleExport">导出</el-button>
  27. </el-col>
  28. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  29. </el-row>
  30. <!-- ===== 租户列表 ===== -->
  31. <el-table border v-loading="loading" :data="companyList" size="small" style="width:100%">
  32. <el-table-column label="租户编码" prop="tenantCode" min-width="80" align="center" />
  33. <el-table-column label="租户名称" prop="tenantName" min-width="120" />
  34. <el-table-column label="联系人" prop="contactName" min-width="80" align="center" />
  35. <el-table-column label="联系电话" prop="contactPhone" min-width="110" align="center" />
  36. <el-table-column label="余额" align="center" min-width="100">
  37. <template slot-scope="scope">
  38. <span style="color:#1890ff;font-weight:bold">{{ scope.row.balance ? '¥' + Number(scope.row.balance).toFixed(2) : '¥0.00' }}</span>
  39. </template>
  40. </el-table-column>
  41. <el-table-column label="已消费总额" align="center" min-width="100">
  42. <template slot-scope="scope">
  43. <span>{{ scope.row.totalConsumption ? '¥' + Number(scope.row.totalConsumption).toFixed(2) : '¥0.00' }}</span>
  44. </template>
  45. </el-table-column>
  46. <el-table-column label="开通账户数" align="center" prop="accountCount" min-width="90" />
  47. <el-table-column label="绑定企微账户数" align="center" prop="qwAccountCount" min-width="110" />
  48. <el-table-column label="绑定个微账户数" align="center" prop="wxAccountCount" min-width="110" />
  49. <el-table-column label="客户数" align="center" prop="customerCount" min-width="80" />
  50. <el-table-column label="企微用户数" align="center" prop="qwUserCount" min-width="90" />
  51. <el-table-column label="过期时间" align="center" prop="expireTime" min-width="100" />
  52. <el-table-column label="归属代理" align="center" prop="proxyName" min-width="90" />
  53. <el-table-column label="状态" align="center" min-width="70">
  54. <template slot-scope="scope">
  55. <el-tag v-if="scope.row.status == 1" type="success" size="mini">正常</el-tag>
  56. <el-tag v-else type="danger" size="mini">禁用</el-tag>
  57. </template>
  58. </el-table-column>
  59. <el-table-column label="操作" align="center" width="380" fixed="right" class-name="small-padding fixed-width">
  60. <template slot-scope="scope">
  61. <el-button size="mini" type="text" icon="el-icon-edit" @click="handleView(scope.row)">编辑</el-button>
  62. <el-button size="mini" type="text" icon="el-icon-key" style="color:#722ed1" @click="handleResetPwd(scope.row)">重置密码</el-button>
  63. <el-button size="mini" type="text" style="color:#52c41a" icon="el-icon-coin" @click="handleRecharge(scope.row)">充值/扣款</el-button>
  64. <el-button size="mini" type="text" style="color:#722ed1" icon="el-icon-menu" @click="handleEditMenu(scope.row, 'sys')">管理端菜单</el-button>
  65. <el-button size="mini" type="text" style="color:#e6a23c" icon="el-icon-price-tag" @click="handleModulePricing(scope.row)">模块定价</el-button>
  66. <el-button size="mini" type="text" style="color:#13c2c2" icon="el-icon-sell" @click="handleEditMenu(scope.row, 'com')">销售菜单</el-button>
  67. <el-button
  68. v-if="scope.row.status == 1"
  69. size="mini" type="text" style="color:#fa8c16"
  70. icon="el-icon-lock"
  71. @click="handleDisable(scope.row)"
  72. v-hasPermi="['admin:platform:edit']"
  73. >禁用</el-button>
  74. <el-button
  75. v-else
  76. size="mini" type="text" style="color:#52c41a"
  77. icon="el-icon-unlock"
  78. @click="handleEnable(scope.row)"
  79. v-hasPermi="['admin:platform:edit']"
  80. >启用</el-button>
  81. </template>
  82. </el-table-column>
  83. </el-table>
  84. <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
  85. <!-- ===== 编辑租户弹窗 ===== -->
  86. <el-dialog title="编辑租户" :visible.sync="viewOpen" width="680px" append-to-body destroy-on-close>
  87. <el-form ref="editForm" :model="viewForm" :rules="editRules" label-width="100px" size="small">
  88. <el-row :gutter="20">
  89. <el-col :span="12">
  90. <el-form-item label="租户ID">
  91. <span>{{ viewForm.id }}</span>
  92. </el-form-item>
  93. </el-col>
  94. <el-col :span="12">
  95. <el-form-item label="租户编码">
  96. <span>{{ viewForm.tenantCode || '-' }}</span>
  97. </el-form-item>
  98. </el-col>
  99. <el-col :span="12">
  100. <el-form-item label="租户名称" prop="tenantName">
  101. <el-input v-model="viewForm.tenantName" placeholder="请输入租户名称" />
  102. </el-form-item>
  103. </el-col>
  104. <el-col :span="12">
  105. <el-form-item label="联系人" prop="contactName">
  106. <el-input v-model="viewForm.contactName" placeholder="请输入联系人" />
  107. </el-form-item>
  108. </el-col>
  109. <el-col :span="12">
  110. <el-form-item label="联系电话" prop="contactPhone">
  111. <el-input v-model="viewForm.contactPhone" placeholder="请输入联系电话" />
  112. </el-form-item>
  113. </el-col>
  114. <el-col :span="12">
  115. <el-form-item label="过期时间" prop="expireTime">
  116. <el-date-picker v-model="viewForm.expireTime" type="date" value-format="yyyy-MM-dd" placeholder="选择过期时间" style="width:100%" />
  117. </el-form-item>
  118. </el-col>
  119. <el-col :span="12">
  120. <el-form-item label="租户余额">
  121. <span style="color:#1890ff;font-weight:bold">¥{{ viewForm.balance || '0.00' }}</span>
  122. <span style="color:#999;font-size:12px;margin-left:8px">(通过充值/扣款调整)</span>
  123. </el-form-item>
  124. </el-col>
  125. <el-col :span="12">
  126. <el-form-item label="归属代理" prop="proxyId">
  127. <el-select v-model="viewForm.proxyId" placeholder="请选择代理" clearable filterable style="width:100%">
  128. <el-option v-for="p in proxyOptions" :key="p.proxyId" :label="p.proxyName" :value="p.proxyId" />
  129. </el-select>
  130. </el-form-item>
  131. </el-col>
  132. <el-col :span="12">
  133. <el-form-item label="租户状态">
  134. <el-switch v-model="viewForm.statusBool" active-text="正常" inactive-text="禁用"
  135. :active-value="1" :inactive-value="0" />
  136. </el-form-item>
  137. </el-col>
  138. </el-row>
  139. </el-form>
  140. <div slot="footer">
  141. <el-button @click="viewOpen = false">取 消</el-button>
  142. <el-button type="primary" :loading="editSubmitting" @click="submitEdit">保 存</el-button>
  143. </div>
  144. </el-dialog>
  145. <!-- ===== 新增租户弹窗 ===== -->
  146. <el-dialog title="新增租户" :visible.sync="addDialog.visible" width="680px" append-to-body destroy-on-close>
  147. <el-form ref="addForm" :model="addDialog.form" :rules="addDialog.rules" label-width="100px" size="small">
  148. <el-row :gutter="20">
  149. <el-col :span="12">
  150. <el-form-item label="企业名称" prop="tenantName">
  151. <el-input v-model="addDialog.form.tenantName" placeholder="请输入企业名称" />
  152. </el-form-item>
  153. </el-col>
  154. <el-col :span="12">
  155. <el-form-item label="联系电话" prop="contactPhone">
  156. <el-input v-model="addDialog.form.contactPhone" placeholder="请输入联系电话" />
  157. </el-form-item>
  158. </el-col>
  159. <el-col :span="12">
  160. <el-form-item label="联系人" prop="contactName">
  161. <el-input v-model="addDialog.form.contactName" placeholder="请输入联系人" />
  162. </el-form-item>
  163. </el-col>
  164. <el-col :span="12">
  165. <el-form-item label="商务负责人" prop="manager">
  166. <el-input v-model="addDialog.form.manager" placeholder="请输入商务负责人" />
  167. </el-form-item>
  168. </el-col>
  169. <el-col :span="12">
  170. <el-form-item label="管理员账号" prop="userName">
  171. <el-input v-model="addDialog.form.userName" placeholder="请输入管理员账号" />
  172. </el-form-item>
  173. </el-col>
  174. <el-col :span="12">
  175. <el-form-item label="密码" prop="password">
  176. <el-input v-model="addDialog.form.password" type="password" show-password placeholder="请输入密码">
  177. <el-button slot="append" icon="el-icon-refresh" @click="generateAddPwd" title="随机生成密码">生成</el-button>
  178. </el-input>
  179. </el-form-item>
  180. </el-col>
  181. <el-col :span="12">
  182. <el-form-item label="开始时间" prop="startTime">
  183. <el-date-picker v-model="addDialog.form.startTime" type="date" value-format="yyyy-MM-dd" placeholder="选择开始时间" style="width:100%" />
  184. </el-form-item>
  185. </el-col>
  186. <el-col :span="12">
  187. <el-form-item label="到期时间" prop="expireTime">
  188. <el-date-picker v-model="addDialog.form.expireTime" type="date" value-format="yyyy-MM-dd" placeholder="选择到期时间" style="width:100%" />
  189. </el-form-item>
  190. </el-col>
  191. <el-col :span="12">
  192. <el-form-item label="归属代理" prop="proxyId">
  193. <el-select v-model="addDialog.form.proxyId" placeholder="请选择代理" clearable filterable style="width:100%">
  194. <el-option v-for="p in proxyOptions" :key="p.proxyId" :label="p.proxyName" :value="p.proxyId" />
  195. </el-select>
  196. </el-form-item>
  197. </el-col>
  198. <el-col :span="12">
  199. <el-form-item label="状态" prop="status">
  200. <el-radio-group v-model="addDialog.form.status">
  201. <el-radio :label="1">启用</el-radio>
  202. <el-radio :label="0">禁用</el-radio>
  203. </el-radio-group>
  204. </el-form-item>
  205. </el-col>
  206. </el-row>
  207. </el-form>
  208. <div slot="footer">
  209. <el-button @click="addDialog.visible = false">取 消</el-button>
  210. <el-button type="primary" :loading="addDialog.submitting" @click="submitAdd">确 定</el-button>
  211. </div>
  212. </el-dialog>
  213. <!-- ===== 充值/扣款弹窗 ===== -->
  214. <el-dialog :title="rechargeTitle" :visible.sync="rechargeOpen" width="420px" append-to-body>
  215. <el-form ref="rechargeForm" :model="rechargeForm" :rules="rechargeRules" label-width="90px">
  216. <el-form-item label="租户名称">
  217. <span>{{ rechargeForm.companyName }}</span>
  218. </el-form-item>
  219. <el-form-item label="当前余额">
  220. <span style="color:#1890ff;font-weight:bold">¥{{ rechargeForm.currentBalance || '0.00' }}</span>
  221. </el-form-item>
  222. <el-form-item label="操作类型" prop="operateType">
  223. <el-radio-group v-model="rechargeForm.operateType">
  224. <el-radio label="recharge">充值</el-radio>
  225. <el-radio label="deduct">扣款</el-radio>
  226. </el-radio-group>
  227. </el-form-item>
  228. <el-form-item label="金额" prop="amount">
  229. <el-input-number v-model="rechargeForm.amount" :min="0.01" :precision="2" style="width:200px" />
  230. </el-form-item>
  231. <el-form-item label="备注" prop="remark">
  232. <el-input v-model="rechargeForm.remark" type="textarea" :rows="2" placeholder="请输入备注" />
  233. </el-form-item>
  234. </el-form>
  235. <div slot="footer">
  236. <el-button @click="rechargeOpen = false">取 消</el-button>
  237. <el-button type="primary" @click="submitRecharge">确 定</el-button>
  238. </div>
  239. </el-dialog>
  240. <!-- ===== 菜单编辑弹窗 ===== -->
  241. <el-dialog :title="menuDialog.title" :visible.sync="menuDialog.visible" width="560px" append-to-body destroy-on-close>
  242. <div v-loading="menuDialog.loading" style="min-height:100px">
  243. <el-tree
  244. v-if="menuDialog.treeData.length"
  245. ref="menuTree"
  246. :data="menuDialog.treeData"
  247. show-checkbox
  248. node-key="id"
  249. :default-checked-keys="menuDialog.checkedKeys"
  250. :props="{ label: 'label', children: 'children' }"
  251. default-expand-all
  252. >
  253. <span slot-scope="{ data }" class="custom-tree-node">
  254. <span>{{ data.label }}</span>
  255. <el-tag v-if="data.visible === '0'" type="success" size="mini" style="margin-left:8px">显示</el-tag>
  256. <el-tag v-else type="info" size="mini" style="margin-left:8px">隐藏</el-tag>
  257. </span>
  258. </el-tree>
  259. <el-empty v-if="!menuDialog.loading && !menuDialog.treeData.length" description="暂无菜单数据" />
  260. </div>
  261. <div slot="footer">
  262. <el-button @click="menuDialog.visible = false">取 消</el-button>
  263. <el-button type="primary" :loading="menuDialog.submitting" @click="submitMenuEdit">确 定</el-button>
  264. </div>
  265. </el-dialog>
  266. <!-- ===== 模块定价弹窗 ===== -->
  267. <el-dialog :title="'模块定价 - ' + pricingDialog.tenantName" :visible.sync="pricingDialog.visible" width="800px" append-to-body destroy-on-close>
  268. <div v-loading="pricingDialog.loading">
  269. <el-table :data="pricingDialog.modules" border size="small" style="width:100%">
  270. <el-table-column label="模块" align="center" prop="moduleName" min-width="120" />
  271. <el-table-column label="全局售价" align="center" min-width="100">
  272. <template slot-scope="scope">
  273. <span style="color:#909399">{{ scope.row.globalPrice != null ? scope.row.globalPrice : '-' }}</span>
  274. </template>
  275. </el-table-column>
  276. <el-table-column label="全局成本" align="center" min-width="100">
  277. <template slot-scope="scope">
  278. <span style="color:#909399">{{ scope.row.globalCost != null ? scope.row.globalCost : '-' }}</span>
  279. </template>
  280. </el-table-column>
  281. <el-table-column label="租户售价" align="center" min-width="130">
  282. <template slot-scope="scope">
  283. <el-input-number v-model="scope.row.price" :precision="4" :min="0" :step="0.01" size="small" style="width:150px" placeholder="跟随全局" />
  284. </template>
  285. </el-table-column>
  286. <el-table-column label="成本价" align="center" min-width="130">
  287. <template slot-scope="scope">
  288. <el-input-number v-model="scope.row.costPrice" :precision="4" :min="0" :step="0.01" size="small" style="width:150px" placeholder="跟随全局" />
  289. </template>
  290. </el-table-column>
  291. <el-table-column label="状态" align="center" min-width="80">
  292. <template slot-scope="scope">
  293. <el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁" size="small" />
  294. </template>
  295. </el-table-column>
  296. </el-table>
  297. <div style="color:#909399;font-size:12px;margin-top:8px">* 租户售价/成本价留空或填0则使用全局定价</div>
  298. </div>
  299. <div slot="footer">
  300. <el-button @click="pricingDialog.visible = false">取 消</el-button>
  301. <el-button type="primary" :loading="pricingDialog.submitting" @click="submitModulePricing">保 存</el-button>
  302. </div>
  303. </el-dialog>
  304. </div>
  305. </template>
  306. <script>
  307. import { listAllCompanies, getCompanyInfo, addCompany, updateTenant, disableCompany, enableCompany, rechargeCompany, exportCompany, getTenantMenuTree, editTenantMenu, resetTenantPwd } from '@/api/admin/sysCompany'
  308. import { listTrafficPricing, addTrafficPricing, updateTrafficPricing } from '@/api/admin/trafficPricing'
  309. import { allEnabledProxies } from '@/api/admin/proxy'
  310. import { listServiceCost } from '@/api/admin/serviceCost'
  311. import { generateRandomPwd } from '@/utils/index'
  312. export default {
  313. name: 'SysCompanyAdmin',
  314. data() {
  315. return {
  316. loading: true,
  317. exportLoading: false,
  318. showSearch: true,
  319. total: 0,
  320. companyList: [],
  321. statusOptions: [
  322. { value: 1, label: '正常' },
  323. { value: 0, label: '禁用' }
  324. ],
  325. queryParams: {
  326. pageNum: 1,
  327. pageSize: 10,
  328. tenantName: null,
  329. status: null
  330. },
  331. // 详情弹窗
  332. viewOpen: false,
  333. viewForm: {},
  334. editSubmitting: false,
  335. editRules: {
  336. tenantName: [{ required: true, message: '请输入租户名称', trigger: 'blur' }],
  337. contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }]
  338. },
  339. // 新增租户弹窗
  340. addDialog: {
  341. visible: false,
  342. submitting: false,
  343. form: {
  344. tenantName: '',
  345. contactPhone: '',
  346. contactName: '',
  347. userName: '',
  348. password: '',
  349. manager: '',
  350. startTime: null,
  351. expireTime: null,
  352. status: 1
  353. },
  354. rules: {
  355. tenantName: [{ required: true, message: '请输入企业名称', trigger: 'blur' }],
  356. contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
  357. userName: [{ required: true, message: '请输入管理员账号', trigger: 'blur' }],
  358. password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
  359. }
  360. },
  361. // 充值/扣款弹窗
  362. rechargeOpen: false,
  363. rechargeTitle: '',
  364. rechargeForm: { companyId: null, companyName: '', currentBalance: 0, operateType: 'recharge', amount: null, remark: '' },
  365. rechargeRules: {
  366. operateType: [{ required: true, message: '请选择操作类型', trigger: 'change' }],
  367. amount: [{ required: true, message: '请输入金额', trigger: 'blur' }]
  368. },
  369. // 菜单编辑弹窗
  370. menuDialog: {
  371. visible: false,
  372. title: '',
  373. flag: 'sys', // sys=管理端菜单, com=销售菜单
  374. companyId: null,
  375. companyName: '',
  376. treeData: [],
  377. checkedKeys: [], // 初始勾选的菜单ID(visible=0)
  378. allMenuIds: [], // 所有菜单ID
  379. loading: false,
  380. submitting: false
  381. },
  382. // 模块定价弹窗
  383. pricingDialog: {
  384. visible: false,
  385. tenantId: null,
  386. tenantName: '',
  387. loading: false,
  388. submitting: false,
  389. modules: []
  390. },
  391. // 代理列表(下拉框用)
  392. proxyOptions: []
  393. }
  394. },
  395. created() {
  396. this.getList()
  397. this.loadProxyOptions()
  398. },
  399. methods: {
  400. loadProxyOptions() {
  401. allEnabledProxies().then(res => {
  402. this.proxyOptions = res.data || []
  403. }).catch(() => {})
  404. },
  405. getList() {
  406. this.loading = true
  407. listAllCompanies(this.queryParams).then(response => {
  408. this.companyList = response.rows
  409. this.total = response.total
  410. this.loading = false
  411. })
  412. },
  413. handleQuery() {
  414. this.queryParams.pageNum = 1
  415. this.getList()
  416. },
  417. resetQuery() {
  418. this.resetForm('queryForm')
  419. this.handleQuery()
  420. },
  421. /** 新增租户 */
  422. handleAdd() {
  423. this.addDialog.form = {
  424. tenantName: '',
  425. contactPhone: '',
  426. contactName: '',
  427. userName: '',
  428. password: generateRandomPwd(12),
  429. manager: '',
  430. proxyId: null,
  431. startTime: null,
  432. expireTime: null,
  433. status: 1
  434. }
  435. this.addDialog.visible = true
  436. this.$nextTick(() => {
  437. if (this.$refs.addForm) this.$refs.addForm.clearValidate()
  438. })
  439. },
  440. /** 随机生成新增密码 */
  441. generateAddPwd() {
  442. this.addDialog.form.password = generateRandomPwd(12)
  443. },
  444. /** 重置租户管理员密码 */
  445. handleResetPwd(row) {
  446. const newPwd = generateRandomPwd(12)
  447. this.$prompt('为租户 "' + row.tenantName + '" 重置管理员密码', '重置密码', {
  448. confirmButtonText: '确定',
  449. cancelButtonText: '取消',
  450. inputValue: newPwd,
  451. inputPattern: /^.{6,30}$/,
  452. inputErrorMessage: '密码长度必须介于 6 和 30 之间',
  453. inputType: 'text'
  454. }).then(({ value }) => {
  455. resetTenantPwd(row.id, value).then(() => {
  456. this.$message.success('密码重置成功,新密码为:' + value)
  457. }).catch(() => {
  458. this.$message.error('密码重置失败')
  459. })
  460. }).catch(() => {})
  461. },
  462. /** 提交新增租户 */
  463. submitAdd() {
  464. this.$refs['addForm'].validate(valid => {
  465. if (!valid) return
  466. this.addDialog.submitting = true
  467. addCompany(this.addDialog.form).then(() => {
  468. this.$message.success('新增成功')
  469. this.addDialog.visible = false
  470. this.getList()
  471. }).catch(() => {
  472. this.$message.error('新增失败,请重试')
  473. }).finally(() => {
  474. this.addDialog.submitting = false
  475. })
  476. })
  477. },
  478. /** 编辑租户 */
  479. handleView(row) {
  480. getCompanyInfo(row.id).then(response => {
  481. this.viewForm = response.data
  482. // 为 el-switch 提供 number 类型的 status
  483. this.$set(this.viewForm, 'statusBool', this.viewForm.status)
  484. this.viewOpen = true
  485. this.$nextTick(() => {
  486. if (this.$refs.editForm) this.$refs.editForm.clearValidate()
  487. })
  488. }).catch(() => {
  489. this.$message.error('获取租户信息失败')
  490. })
  491. },
  492. /** 提交编辑租户 */
  493. submitEdit() {
  494. this.$refs['editForm'].validate(valid => {
  495. if (!valid) return
  496. this.editSubmitting = true
  497. const data = {
  498. id: this.viewForm.id,
  499. tenantName: this.viewForm.tenantName,
  500. contactName: this.viewForm.contactName,
  501. contactPhone: this.viewForm.contactPhone,
  502. expireTime: this.viewForm.expireTime,
  503. status: this.viewForm.statusBool,
  504. proxyId: this.viewForm.proxyId
  505. }
  506. updateTenant(data).then(() => {
  507. this.$message.success('保存成功')
  508. this.viewOpen = false
  509. this.getList()
  510. }).catch(() => {
  511. this.$message.error('保存失败,请重试')
  512. }).finally(() => {
  513. this.editSubmitting = false
  514. })
  515. })
  516. },
  517. /** 充值/扣款 */
  518. handleRecharge(row) {
  519. this.rechargeForm = {
  520. companyId: row.id,
  521. companyName: row.tenantName,
  522. currentBalance: row.balance || 0,
  523. operateType: 'recharge',
  524. amount: null,
  525. remark: ''
  526. }
  527. this.rechargeTitle = `充值/扣款 - ${row.tenantName}`
  528. this.rechargeOpen = true
  529. },
  530. submitRecharge() {
  531. this.$refs['rechargeForm'].validate(valid => {
  532. if (!valid) return
  533. const { companyId, operateType, amount, remark } = this.rechargeForm
  534. rechargeCompany(companyId, { operateType, amount, remark }).then(res => {
  535. if (res.code === 200) {
  536. this.$message.success(res.msg || (operateType === 'recharge' ? '充值成功' : '扣款成功'))
  537. this.rechargeOpen = false
  538. this.getList()
  539. } else {
  540. this.$message.error(res.msg || '操作失败')
  541. }
  542. }).catch(() => {
  543. this.$message.error('操作失败,请重试')
  544. })
  545. })
  546. },
  547. /** 禁用租户 */
  548. handleDisable(row) {
  549. this.$confirm('确定禁用租户 [' + row.tenantName + '] 吗?', '提示', {
  550. confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
  551. }).then(() => {
  552. disableCompany(row.id).then(() => {
  553. this.$message.success('禁用成功')
  554. this.getList()
  555. })
  556. })
  557. },
  558. /** 启用租户 */
  559. handleEnable(row) {
  560. this.$confirm('确定启用租户 [' + row.tenantName + '] 吗?', '提示', {
  561. confirmButtonText: '确定', cancelButtonText: '取消', type: 'info'
  562. }).then(() => {
  563. enableCompany(row.id).then(() => {
  564. this.$message.success('启用成功')
  565. this.getList()
  566. })
  567. })
  568. },
  569. handleExport() {
  570. this.exportLoading = true
  571. exportCompany(this.queryParams).then(response => {
  572. this.download(response.msg)
  573. this.exportLoading = false
  574. }).catch(() => { this.exportLoading = false })
  575. },
  576. /** 编辑菜单(管理端/销售端) */
  577. handleEditMenu(row, flag) {
  578. this.menuDialog = {
  579. visible: true,
  580. title: (flag === 'sys' ? '编辑管理端菜单 - ' : '编辑销售菜单 - ') + row.tenantName,
  581. flag: flag,
  582. companyId: row.id,
  583. companyName: row.tenantName,
  584. treeData: [],
  585. checkedKeys: [],
  586. allMenuIds: [],
  587. loading: true,
  588. submitting: false
  589. }
  590. getTenantMenuTree(row.id, flag).then(res => {
  591. if (res.code === 200) {
  592. const menus = res.menus || []
  593. this.menuDialog.treeData = menus
  594. // 递归提取所有菜单ID + visible=0的ID(勾选状态)
  595. const allIds = []
  596. const checkedIds = []
  597. const walk = (nodes) => {
  598. if (!nodes) return
  599. nodes.forEach(n => {
  600. allIds.push(n.id)
  601. if (n.visible === '0') checkedIds.push(n.id)
  602. if (n.children && n.children.length) walk(n.children)
  603. })
  604. }
  605. walk(menus)
  606. this.menuDialog.checkedKeys = checkedIds
  607. this.menuDialog.allMenuIds = allIds
  608. // 等tree渲染后捕获级联展开的实际勾选状态作为基准
  609. this.$nextTick(() => {
  610. const tree = this.$refs.menuTree
  611. if (tree) {
  612. this.menuDialog.checkedKeys = tree.getCheckedKeys().concat(tree.getHalfCheckedKeys())
  613. }
  614. })
  615. } else {
  616. this.$message.error(res.msg || '加载菜单失败')
  617. }
  618. }).catch(() => {
  619. this.$message.error('加载菜单失败')
  620. }).finally(() => {
  621. this.menuDialog.loading = false
  622. })
  623. },
  624. /** 模块定价 */
  625. handleModulePricing(row) {
  626. this.pricingDialog = {
  627. visible: true,
  628. tenantId: row.id,
  629. tenantName: row.tenantName,
  630. loading: true,
  631. submitting: false,
  632. modules: []
  633. }
  634. // 从收费配置API获取启用的模块列表(排除手拨外呼serviceType=6)
  635. Promise.all([
  636. listServiceCost(),
  637. listTrafficPricing({ tenantId: row.id, pageSize: 100 })
  638. ]).then(([costRes, pricingRes]) => {
  639. const costList = (costRes.data || []).filter(c => c.enabled === 1 && c.serviceType !== 6)
  640. const tenantPricings = pricingRes.rows || []
  641. const pricingMap = {}
  642. tenantPricings.forEach(p => { pricingMap[p.serviceType] = p })
  643. this.pricingDialog.modules = costList.map(c => {
  644. const existing = pricingMap[c.serviceType]
  645. return {
  646. serviceType: c.serviceType,
  647. moduleName: c.configName,
  648. unit: c.feeUnit,
  649. globalPrice: c.feeStandard,
  650. globalCost: c.platformCost,
  651. price: existing ? existing.price : null,
  652. costPrice: existing ? existing.costPrice : null,
  653. status: existing ? existing.status : 1,
  654. id: existing ? existing.id : null
  655. }
  656. })
  657. this.pricingDialog.loading = false
  658. }).catch(() => {
  659. this.pricingDialog.loading = false
  660. this.$message.error('加载定价数据失败')
  661. })
  662. },
  663. /** 提交模块定价 */
  664. submitModulePricing() {
  665. this.pricingDialog.submitting = true
  666. const tenantId = this.pricingDialog.tenantId
  667. const promises = this.pricingDialog.modules.map(m => {
  668. const data = {
  669. tenantId: tenantId,
  670. serviceType: m.serviceType,
  671. price: m.price,
  672. costPrice: m.costPrice,
  673. status: m.status
  674. }
  675. if (m.id) {
  676. data.id = m.id
  677. return updateTrafficPricing(data)
  678. } else {
  679. return addTrafficPricing(data)
  680. }
  681. })
  682. Promise.all(promises).then(() => {
  683. this.$message.success('模块定价保存成功')
  684. this.pricingDialog.visible = false
  685. }).catch(() => {
  686. this.$message.error('部分定价保存失败,请重试')
  687. }).finally(() => {
  688. this.pricingDialog.submitting = false
  689. })
  690. },
  691. /** 提交菜单编辑 */
  692. submitMenuEdit() {
  693. const tree = this.$refs.menuTree
  694. if (!tree) return
  695. // 级联模式:纳入全选+半选节点作为当前勾选状态
  696. const currentChecked = tree.getCheckedKeys().concat(tree.getHalfCheckedKeys())
  697. const originalChecked = this.menuDialog.checkedKeys
  698. // selected = 新勾选的(当前有,原来没有)
  699. const selected = currentChecked.filter(id => !originalChecked.includes(id))
  700. // unSelected = 新取消的(原来有,当前没有)
  701. const unSelected = originalChecked.filter(id => !currentChecked.includes(id))
  702. if (selected.length === 0 && unSelected.length === 0) {
  703. this.$message.info('菜单无变化')
  704. this.menuDialog.visible = false
  705. return
  706. }
  707. this.menuDialog.submitting = true
  708. editTenantMenu(this.menuDialog.companyId, {
  709. flag: this.menuDialog.flag,
  710. selected: selected,
  711. unSelected: unSelected
  712. }).then(res => {
  713. if (res.code === 200) {
  714. this.$message.success('菜单更新成功')
  715. this.menuDialog.visible = false
  716. } else {
  717. this.$message.error(res.msg || '菜单更新失败')
  718. }
  719. }).catch(() => {
  720. this.$message.error('菜单更新失败')
  721. }).finally(() => {
  722. this.menuDialog.submitting = false
  723. })
  724. }
  725. }
  726. }
  727. </script>
  728. <style scoped>
  729. .mb8 { margin-bottom: 8px; }
  730. .mb16 { margin-bottom: 16px; }
  731. .filter-card { padding-bottom: 0; }
  732. .custom-tree-node { display: flex; align-items: center; font-size: 13px; }
  733. </style>