client.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. <template>
  2. <div class="page">
  3. <div class="client-container">
  4. <el-card shadow="never">
  5. <div class="client-header">
  6. <h3>API客户端管理</h3>
  7. </div>
  8. <el-form :model="params" inline ref="queryRef">
  9. <el-form-item label="关键字" prop="keyWord">
  10. <el-input v-model="params.keyWord" placeholder="输入客户端名称或标识" clearable style="width: 170px" @keyup.enter.native="getList(1)" />
  11. </el-form-item>
  12. <el-form-item label="状态" prop="status">
  13. <el-select v-model="params.status" placeholder="请选择状态" clearable style="width: 120px">
  14. <el-option label="全部" value="" />
  15. <el-option label="激活" value="Active" />
  16. <el-option label="未激活" value="Inactive" />
  17. </el-select>
  18. </el-form-item>
  19. <el-form-item label="日期范围" prop="dateRange">
  20. <el-date-picker v-model="params.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 240px" />
  21. </el-form-item>
  22. <el-form-item>
  23. <el-button type="primary" class="ml10" @click="getList(1)">
  24. <el-icon>
  25. <ele-Search />
  26. </el-icon>
  27. 查询
  28. </el-button>
  29. <el-button type="primary" @click="addOrEdit()" v-auth="'add'">
  30. <el-icon>
  31. <ele-Plus />
  32. </el-icon>
  33. 新增客户端
  34. </el-button>
  35. </el-form-item>
  36. </el-form>
  37. <el-table :data="tableData" style="width: 100%" v-loading="loading" row-key="id">
  38. <el-table-column type="selection" width="40" align="center" />
  39. <el-table-column prop="clientId" label="客户端标识" v-col="'clientId'" min-width="140" show-overflow-tooltip></el-table-column>
  40. <el-table-column prop="name" label="客户端名称" v-col="'name'" min-width="140" show-overflow-tooltip></el-table-column>
  41. <el-table-column prop="remark" label="备注" v-col="'remark'" show-overflow-tooltip></el-table-column>
  42. <el-table-column prop="status" label="状态" v-col="'status'" width="100" align="center">
  43. <template #default="scope">
  44. <el-tag size="small" type="success" v-if="scope.row.status === 'Active'">激活</el-tag>
  45. <el-tag size="small" type="info" v-else-if="scope.row.status === 'Inactive'">未激活</el-tag>
  46. <span v-else>{{ scope.row.status }}</span>
  47. </template>
  48. </el-table-column>
  49. <!-- <el-table-column prop="createdAt" label="创建时间" width="160" align="center"></el-table-column>-->
  50. <el-table-column label="操作" width="300" align="center" fixed="right" v-col="'handle'">
  51. <template #default="scope">
  52. <div class="flex-row">
  53. <el-button size="small" text type="primary" @click="viewDetail(scope.row)" v-auth="'view'">查看</el-button>
  54. <el-button size="small" text type="warning" @click="addOrEdit(scope.row)" v-auth="'edit'">编辑</el-button>
  55. <el-button size="small" text type="primary" @click="manageApiAuth(scope.row)" v-auth="'edit'">API权限</el-button>
  56. <el-button size="small" text type="success" @click="resetSecret(scope.row)" v-auth="'reset'">重置密钥</el-button>
  57. <el-button size="small" text type="danger" @click="deleteClient(scope.row)" v-auth="'del'">删除</el-button>
  58. </div>
  59. </template>
  60. </el-table-column>
  61. </el-table>
  62. <pagination v-if="params.total" :total="params.total" v-model:page="params.pageNum" v-model:limit="params.pageSize" @pagination="getList()" />
  63. </el-card>
  64. </div>
  65. <!-- 客户端表单弹窗 -->
  66. <el-dialog v-model="dialogVisible" :title="formTitle" width="600px" destroy-on-close>
  67. <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
  68. <el-form-item label="客户端标识" prop="clientId">
  69. <el-input v-model="form.clientId" placeholder="留空默认由系统自动生成" :disabled="!!form.id" />
  70. </el-form-item>
  71. <el-form-item label="客户端名称" prop="name">
  72. <el-input v-model="form.name" placeholder="请输入客户端名称" />
  73. </el-form-item>
  74. <el-form-item label="状态" prop="status">
  75. <el-radio-group v-model="form.status">
  76. <el-radio label="Active">激活</el-radio>
  77. <el-radio label="Inactive">未激活</el-radio>
  78. </el-radio-group>
  79. </el-form-item>
  80. <el-form-item label="IP白名单" prop="ipWhitelist">
  81. <el-input v-model="form.ipWhitelist" placeholder="多个IP用逗号分隔" />
  82. <div class="form-tip">多个IP用逗号分隔,留空表示不限制</div>
  83. </el-form-item>
  84. <el-form-item label="IP黑名单" prop="ipBlacklist">
  85. <el-input v-model="form.ipBlacklist" placeholder="多个IP用逗号分隔" />
  86. <div class="form-tip">多个IP用逗号分隔,留空表示不限制</div>
  87. </el-form-item>
  88. <el-form-item label="备注" prop="remark">
  89. <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
  90. </el-form-item>
  91. </el-form>
  92. <template #footer>
  93. <span class="dialog-footer">
  94. <el-button @click="dialogVisible = false">取消</el-button>
  95. <el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
  96. </span>
  97. </template>
  98. </el-dialog>
  99. <!-- API权限管理对话框 -->
  100. <el-dialog v-model="apiAuthVisible" :title="`客户端 '${currentClient.name}' 的API权限管理`" width="1100px" append-to-body destroy-on-close class="api-permission-dialog">
  101. <client-api-relation :client-id="currentClient.clientId" @update:apiKeys="handleApiKeysUpdate" />
  102. <template #footer>
  103. <span class="dialog-footer">
  104. <el-button @click="apiAuthVisible = false">取消</el-button>
  105. <el-button type="primary" @click="saveApiRelations" :loading="apiSaveLoading">保存</el-button>
  106. </span>
  107. </template>
  108. </el-dialog>
  109. <!-- 客户端详情弹窗 -->
  110. <el-dialog v-model="detailVisible" title="客户端详情" width="600px" destroy-on-close>
  111. <el-descriptions :column="1" border>
  112. <el-descriptions-item label="客户端标识">{{ detail.clientId }}</el-descriptions-item>
  113. <el-descriptions-item label="客户端名称">{{ detail.name }}</el-descriptions-item>
  114. <el-descriptions-item label="状态">
  115. <el-tag size="small" type="success" v-if="detail.status === 'Active'">激活</el-tag>
  116. <el-tag size="small" type="info" v-else-if="detail.status === 'Inactive'">未激活</el-tag>
  117. <span v-else>{{ detail.status }}</span>
  118. </el-descriptions-item>
  119. <el-descriptions-item label="允许访问的API">
  120. <div v-if="detail.apiNames && detail.apiNames.length > 0">
  121. <el-tag v-for="(api, index) in detail.apiNames" :key="index" size="small" class="api-tag">
  122. {{ api }}
  123. </el-tag>
  124. </div>
  125. <span v-else>-</span>
  126. </el-descriptions-item>
  127. <el-descriptions-item label="IP白名单">{{ detail.ipWhitelist || "-" }}</el-descriptions-item>
  128. <el-descriptions-item label="IP黑名单">{{ detail.ipBlacklist || "-" }}</el-descriptions-item>
  129. <el-descriptions-item label="备注">{{ detail.remark || "-" }}</el-descriptions-item>
  130. <el-descriptions-item label="创建时间">{{ detail.createdAt }}</el-descriptions-item>
  131. <el-descriptions-item label="更新时间">{{ detail.updatedAt }}</el-descriptions-item>
  132. </el-descriptions>
  133. <template #footer>
  134. <span class="dialog-footer">
  135. <el-button @click="detailVisible = false">关闭</el-button>
  136. </span>
  137. </template>
  138. </el-dialog>
  139. <!-- 密钥重置结果弹窗 -->
  140. <el-dialog v-model="secretVisible" title="客户端密钥" width="600px" destroy-on-close>
  141. <div class="secret-result">
  142. <el-alert title="请妥善保管以下密钥信息,它仅会显示一次!" type="warning" :closable="false" show-icon />
  143. <div class="secret-info" v-if="secretResult.clientSecret">
  144. <div class="secret-item">
  145. <div class="secret-label">客户端密钥:</div>
  146. <div class="secret-value">{{ secretResult.clientSecret }}</div>
  147. <el-button size="small" type="primary" @click="copyText(secretResult.clientSecret)">复制</el-button>
  148. </div>
  149. </div>
  150. </div>
  151. <template #footer>
  152. <span class="dialog-footer">
  153. <el-button @click="secretVisible = false">关闭</el-button>
  154. </span>
  155. </template>
  156. </el-dialog>
  157. </div>
  158. </template>
  159. <script lang="ts" setup>
  160. import { ref, reactive, onMounted } from "vue";
  161. import { ElMessageBox, ElMessage } from "element-plus";
  162. import api from "/@/api/modules/apiHub";
  163. import ClientApiRelation from "./component/ClientApiRelation.vue";
  164. // 定义客户端接口类型
  165. interface ClientInfo {
  166. id?: number;
  167. clientId: string;
  168. name: string;
  169. apiIds?: number[];
  170. apiNames?: string[];
  171. status: string;
  172. ipWhitelist?: string;
  173. ipBlacklist?: string;
  174. remark?: string;
  175. createdAt?: string;
  176. updatedAt?: string;
  177. }
  178. // 引用组件
  179. const queryRef = ref();
  180. const formRef = ref();
  181. // 表格数据和加载状态
  182. const loading = ref(false);
  183. const apiSaveLoading = ref(false);
  184. const selectedApiKeys = ref<string[]>([]);
  185. const apiAuthVisible = ref(false);
  186. const currentClient = ref<ClientInfo>({} as ClientInfo);
  187. const params = reactive({
  188. keyWord: "",
  189. status: "",
  190. dateRange: [],
  191. pageNum: 1,
  192. pageSize: 20,
  193. total: 0,
  194. });
  195. const tableData = ref<ClientInfo[]>([]);
  196. // 获取列表数据
  197. const getList = async (pageNum?: number) => {
  198. if (typeof pageNum === "number") {
  199. params.pageNum = pageNum;
  200. }
  201. loading.value = true;
  202. try {
  203. const res = await api.client.list(params);
  204. // 根据request.ts中的响应拦截器处理,直接使用返回的数据
  205. if (res && res.list) {
  206. tableData.value = res.list || [];
  207. params.total = res.total || 0;
  208. } else {
  209. tableData.value = [];
  210. params.total = 0;
  211. }
  212. } catch (error) {
  213. ElMessage.error("获取客户端列表失败");
  214. tableData.value = [];
  215. params.total = 0;
  216. } finally {
  217. loading.value = false;
  218. }
  219. };
  220. // 表单相关
  221. const dialogVisible = ref(false);
  222. const formTitle = ref("");
  223. const submitLoading = ref(false);
  224. const form = reactive<ClientInfo>({
  225. clientId: "",
  226. name: "",
  227. apiIds: [],
  228. status: "Active",
  229. ipWhitelist: "",
  230. ipBlacklist: "",
  231. remark: "",
  232. });
  233. const rules = {
  234. clientId: [{ required: false, message: "请输入客户端标识", trigger: "blur" }],
  235. name: [{ required: true, message: "请输入客户端名称", trigger: "blur" }],
  236. status: [{ required: true, message: "请选择状态", trigger: "change" }],
  237. };
  238. // 详情弹窗
  239. const detailVisible = ref(false);
  240. const detail = ref<ClientInfo>({
  241. clientId: "",
  242. name: "",
  243. status: "",
  244. });
  245. // 密钥结果弹窗
  246. const secretVisible = ref(false);
  247. const secretResult = ref({
  248. clientSecret: "",
  249. });
  250. // 初始化
  251. onMounted(() => {
  252. getList(1);
  253. });
  254. // 重置查询表单
  255. const resetQuery = () => {
  256. queryRef.value?.resetFields();
  257. params.keyWord = "";
  258. params.status = "";
  259. params.dateRange = [];
  260. getList(1);
  261. };
  262. // 新增或编辑客户端
  263. const addOrEdit = (row?: ClientInfo) => {
  264. resetForm();
  265. if (row && row.id) {
  266. formTitle.value = "编辑客户端";
  267. // 获取详细信息
  268. api.client.get(row.id).then((res: any) => {
  269. if (res) {
  270. Object.assign(form, res);
  271. dialogVisible.value = true;
  272. }
  273. });
  274. } else {
  275. formTitle.value = "新增客户端";
  276. dialogVisible.value = true;
  277. }
  278. };
  279. // 重置表单
  280. const resetForm = () => {
  281. if (formRef.value) {
  282. formRef.value.resetFields();
  283. }
  284. Object.assign(form, {
  285. id: undefined,
  286. clientId: "",
  287. name: "",
  288. status: "Active",
  289. ipWhitelist: "",
  290. ipBlacklist: "",
  291. remark: "",
  292. });
  293. };
  294. // 提交表单
  295. const submitForm = () => {
  296. formRef.value?.validate(async (valid: boolean) => {
  297. if (valid) {
  298. submitLoading.value = true;
  299. try {
  300. const apiMethod = form.id ? api.client.edit : api.client.add;
  301. const res = await apiMethod(form);
  302. if (res) {
  303. ElMessage.success(form.id ? "编辑成功" : "添加成功");
  304. dialogVisible.value = false;
  305. getList(1);
  306. // 如果是新增,显示密钥信息
  307. if (!form.id && res.clientSecret) {
  308. secretResult.value = {
  309. clientSecret: res.clientSecret,
  310. };
  311. secretVisible.value = true;
  312. }
  313. }
  314. } catch (error) {
  315. ElMessage.error(form.id ? "编辑失败" : "添加失败");
  316. } finally {
  317. submitLoading.value = false;
  318. }
  319. }
  320. });
  321. };
  322. // 查看客户端详情
  323. const viewDetail = (row: ClientInfo) => {
  324. api.client.get(row.id as number).then((res: any) => {
  325. if (res) {
  326. detail.value = res;
  327. detailVisible.value = true;
  328. }
  329. });
  330. };
  331. // 重置客户端密钥
  332. const resetSecret = async (row: ClientInfo) => {
  333. try {
  334. await ElMessageBox.confirm(`确定要重置客户端"${row.name}"的密钥吗?\n注意:重置后原密钥将失效!`, "提示", {
  335. confirmButtonText: "确定",
  336. cancelButtonText: "取消",
  337. type: "warning",
  338. });
  339. const res = await api.client.resetSecret(row.id as number);
  340. if (res) {
  341. ElMessage.success("密钥重置成功");
  342. secretResult.value = {
  343. clientSecret: res.clientSecret,
  344. };
  345. secretVisible.value = true;
  346. }
  347. } catch (error) {
  348. // 用户取消操作
  349. }
  350. };
  351. // 删除客户端
  352. const deleteClient = (row: ClientInfo) => {
  353. ElMessageBox.confirm(`确定要删除客户端「${row.name}」吗?`, "警告", {
  354. confirmButtonText: "确定",
  355. cancelButtonText: "取消",
  356. type: "warning",
  357. })
  358. .then(async () => {
  359. loading.value = true;
  360. try {
  361. await api.client.delete([row.id as number]);
  362. ElMessage.success("删除成功");
  363. getList();
  364. } catch (error) {
  365. ElMessage.error("删除失败");
  366. } finally {
  367. loading.value = false;
  368. }
  369. })
  370. .catch(() => {
  371. // 用户取消删除操作
  372. });
  373. };
  374. // 复制文本
  375. const copyText = (text: string) => {
  376. navigator.clipboard
  377. .writeText(text)
  378. .then(() => {
  379. ElMessage.success("复制成功");
  380. })
  381. .catch(() => {
  382. ElMessage.error("复制失败,请手动复制");
  383. });
  384. };
  385. // 处理API Keys更新
  386. const handleApiKeysUpdate = (apiKeys: string[]) => {
  387. selectedApiKeys.value = apiKeys;
  388. };
  389. // 打开API权限管理窗口
  390. const manageApiAuth = (row: ClientInfo) => {
  391. currentClient.value = { ...row };
  392. apiAuthVisible.value = true;
  393. };
  394. // 保存API关联
  395. const saveApiRelations = async () => {
  396. if (!currentClient.value.clientId) {
  397. ElMessage.warning("客户端信息不完整");
  398. return;
  399. }
  400. apiSaveLoading.value = true;
  401. try {
  402. await api.client.save_apis(currentClient.value.clientId, selectedApiKeys.value);
  403. ElMessage.success("API权限关联保存成功");
  404. apiAuthVisible.value = false;
  405. } catch (error) {
  406. ElMessage.error("API权限关联保存失败");
  407. } finally {
  408. apiSaveLoading.value = false;
  409. }
  410. };
  411. </script>
  412. <style scoped>
  413. .client-container {
  414. width: 100%;
  415. }
  416. .client-header {
  417. margin-bottom: 20px;
  418. }
  419. .ml10 {
  420. margin-left: 10px;
  421. }
  422. .flex-row {
  423. display: flex;
  424. flex-wrap: wrap;
  425. gap: 8px;
  426. }
  427. .api-tag {
  428. margin-right: 5px;
  429. margin-bottom: 5px;
  430. }
  431. .form-tip {
  432. font-size: 12px;
  433. color: #909399;
  434. margin-top: 5px;
  435. }
  436. .secret-result {
  437. padding: 10px 0;
  438. }
  439. .secret-info {
  440. margin-top: 20px;
  441. background-color: #f5f7fa;
  442. border-radius: 4px;
  443. padding: 15px;
  444. }
  445. /* 深色主题下的样式 */
  446. [data-theme="dark"] .secret-info {
  447. margin-top: 20px;
  448. background-color: #2f3030;
  449. border-radius: 4px;
  450. padding: 15px;
  451. color: white;
  452. }
  453. .secret-item {
  454. display: flex;
  455. align-items: center;
  456. margin-bottom: 10px;
  457. }
  458. .secret-label {
  459. width: 100px;
  460. font-weight: bold;
  461. }
  462. .secret-value {
  463. flex: 1;
  464. word-break: break-all;
  465. font-family: monospace;
  466. background-color: #ebeef5;
  467. padding: 5px 10px;
  468. border-radius: 3px;
  469. margin-right: 10px;
  470. }
  471. /* 深色主题下的样式 */
  472. [data-theme="dark"] .secret-value {
  473. flex: 1;
  474. word-break: break-all;
  475. font-family: monospace;
  476. background-color: #575656;
  477. padding: 10px;
  478. border-radius: 3px;
  479. margin-right: 10px;
  480. }
  481. .client-detail-tabs {
  482. margin-top: 20px;
  483. }
  484. .custom-upload {
  485. margin-left: 10px;
  486. color: #409eff;
  487. cursor: pointer;
  488. }
  489. .dialog-header {
  490. padding: 0;
  491. margin: 0;
  492. display: flex;
  493. align-items: center;
  494. justify-content: space-between;
  495. h4 {
  496. margin: 0;
  497. font-weight: 500;
  498. }
  499. .close-btn {
  500. padding: 0;
  501. margin: 0;
  502. cursor: pointer;
  503. }
  504. }
  505. /* API权限对话框样式优化 */
  506. :deep(.api-permission-dialog) {
  507. .el-dialog__header {
  508. background-color: var(--el-bg-color);
  509. padding: 15px 20px;
  510. margin-right: 0;
  511. border-bottom: 1px solid var(--el-border-color-lighter);
  512. }
  513. .el-dialog__title {
  514. font-weight: bold;
  515. color: var(--el-text-color-primary);
  516. }
  517. .el-dialog__body {
  518. background-color: var(--el-bg-color-page);
  519. padding: 20px;
  520. }
  521. .el-dialog__footer {
  522. background-color: var(--el-bg-color);
  523. border-top: 1px solid var(--el-border-color-lighter);
  524. padding: 10px 20px;
  525. }
  526. }
  527. </style>