ClientApiRelation.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. <template>
  2. <div class="client-api-relation">
  3. <div class="filter-container">
  4. <el-row :gutter="20">
  5. <el-col :span="6">
  6. <el-input v-model="searchKey" placeholder="搜索API名称或路径" clearable @keyup.enter="handleSearch" />
  7. </el-col>
  8. <el-col :span="5">
  9. <el-select v-model="selectedGroup" placeholder="API分组" clearable @change="handleSearch">
  10. <el-option v-for="group in apiGroups" :key="group.id" :label="group.name" :value="group.id" />
  11. </el-select>
  12. </el-col>
  13. <el-col :span="5">
  14. <el-select v-model="selectedMethod" placeholder="请求方法" clearable @change="handleSearch">
  15. <el-option v-for="method in methods" :key="method" :label="method" :value="method" />
  16. </el-select>
  17. </el-col>
  18. <el-col :span="5">
  19. <el-select v-model="selectedStatus" placeholder="状态" clearable @change="handleSearch">
  20. <el-option label="已发布" value="Published" />
  21. <el-option label="已弃用" value="Deprecated" />
  22. <el-option label="未发布" value="Draft" />
  23. </el-select>
  24. </el-col>
  25. <el-col :span="3">
  26. <el-button type="primary" @click="handleSearch">
  27. <el-icon><ele-Search /></el-icon>
  28. 搜索
  29. </el-button>
  30. </el-col>
  31. </el-row>
  32. </div>
  33. <div class="relation-container">
  34. <el-row :gutter="20">
  35. <el-col :span="12">
  36. <div class="api-list-container">
  37. <div class="api-list-header">
  38. <h4>可选API列表</h4>
  39. <div class="api-list-actions">
  40. <el-button type="primary" size="small" @click="addSelectedApis">添加选中</el-button>
  41. <el-button size="small" @click="selectAllVisible">选择当前页</el-button>
  42. <!-- <el-button size="small" @click="applyTemplate('readonly')">应用只读模板</el-button>
  43. <el-button size="small" @click="applyTemplate('fullaccess')">应用完全访问</el-button> -->
  44. </div>
  45. </div>
  46. <el-table
  47. ref="apiTable"
  48. :data="availableApis"
  49. style="width: 100%"
  50. height="450px"
  51. v-loading="loading"
  52. @selection-change="handleSelectionChange">
  53. <el-table-column type="selection" width="55" />
  54. <el-table-column prop="name" label="API名称" show-overflow-tooltip />
  55. <el-table-column prop="method" label="请求方法" width="100">
  56. <template #default="scope">
  57. <el-tag
  58. :type="getMethodTagType(scope.row.method)"
  59. size="small">
  60. {{ scope.row.method }}
  61. </el-tag>
  62. </template>
  63. </el-table-column>
  64. <el-table-column prop="path" label="API路径" show-overflow-tooltip />
  65. <el-table-column prop="status" label="状态" width="80">
  66. <template #default="scope">
  67. <el-tag
  68. :type="getStatusTagType(scope.row.status)"
  69. size="small">
  70. {{ getStatusText(scope.row.status) }}
  71. </el-tag>
  72. </template>
  73. </el-table-column>
  74. <el-table-column label="操作" width="80" fixed="right">
  75. <template #default="scope">
  76. <el-button
  77. type="primary"
  78. size="small"
  79. @click="addApi(scope.row)"
  80. :disabled="isApiSelected(scope.row)">
  81. 添加
  82. </el-button>
  83. </template>
  84. </el-table-column>
  85. </el-table>
  86. <div class="pagination-container">
  87. <el-pagination
  88. v-model:current-page="pageNum"
  89. v-model:page-size="pageSize"
  90. :page-sizes="[10, 20, 50, 100]"
  91. layout="total, sizes, prev, pager, next, jumper"
  92. :total="total"
  93. @size-change="handleSizeChange"
  94. @current-change="handleCurrentChange"
  95. />
  96. </div>
  97. </div>
  98. </el-col>
  99. <el-col :span="12">
  100. <div class="selected-api-container">
  101. <div class="selected-api-header">
  102. <h4>已授权API ({{ selectedApis.length }})</h4>
  103. <el-button type="danger" size="small" @click="removeAllSelected">移除全部</el-button>
  104. </div>
  105. <el-table
  106. :data="selectedApis"
  107. style="width: 100%"
  108. height="500px">
  109. <el-table-column prop="name" label="API名称" show-overflow-tooltip />
  110. <el-table-column prop="method" label="请求方法" width="100">
  111. <template #default="scope">
  112. <el-tag
  113. :type="getMethodTagType(scope.row.method)"
  114. size="small">
  115. {{ scope.row.method }}
  116. </el-tag>
  117. </template>
  118. </el-table-column>
  119. <el-table-column prop="path" label="API路径" show-overflow-tooltip />
  120. <el-table-column label="操作" width="80" fixed="right">
  121. <template #default="scope">
  122. <el-button
  123. type="danger"
  124. size="small"
  125. @click="removeApi(scope.row)">
  126. 移除
  127. </el-button>
  128. </template>
  129. </el-table-column>
  130. </el-table>
  131. </div>
  132. </el-col>
  133. </el-row>
  134. </div>
  135. </div>
  136. </template>
  137. <script lang="ts" setup>
  138. import { ref, onMounted, watch, defineProps, defineEmits } from 'vue';
  139. import { ElMessage, ElMessageBox } from 'element-plus';
  140. import api from '/@/api/modules/apiHub';
  141. // 定义API数据类型
  142. interface ApiInfo {
  143. id: number;
  144. key: string;
  145. apiKey: string; // 添加apiKey字段
  146. name: string;
  147. method: string;
  148. path: string;
  149. status: string;
  150. groupId?: number;
  151. groupName?: string;
  152. }
  153. const props = defineProps({
  154. clientId: {
  155. type: String,
  156. required: true
  157. }
  158. });
  159. const emit = defineEmits(['update:apiKeys']);
  160. // 数据状态
  161. const loading = ref(false);
  162. const searchKey = ref('');
  163. const selectedGroup = ref('');
  164. const selectedMethod = ref('');
  165. const selectedStatus = ref('');
  166. const pageNum = ref(1);
  167. const pageSize = ref(20);
  168. const total = ref(0);
  169. // API 数据
  170. const availableApis = ref<ApiInfo[]>([]);
  171. const apiGroups = ref<{id: number, name: string}[]>([]);
  172. const selectedApis = ref<ApiInfo[]>([]);
  173. const selectedApiKeys = ref<string[]>([]);
  174. const apiTable = ref();
  175. const selectedRows = ref<ApiInfo[]>([]);
  176. // 预设模板
  177. const permissionTemplates = {
  178. readonly: {
  179. name: '只读访问',
  180. filter: (api: ApiInfo) => api.method === 'GET'
  181. },
  182. fullaccess: {
  183. name: '完全访问',
  184. filter: (api: ApiInfo) => true
  185. }
  186. };
  187. // 请求方法列表
  188. const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
  189. // 监听选中的API变化,通知父组件
  190. watch(selectedApiKeys, (newKeys) => {
  191. emit('update:apiKeys', newKeys);
  192. });
  193. // 获取API分组
  194. const getApiGroups = async () => {
  195. try {
  196. const res = await api.group.tree();
  197. if (res) {
  198. apiGroups.value = res;
  199. }
  200. } catch (error) {
  201. ElMessage.error('获取API分组失败');
  202. }
  203. };
  204. // 获取API列表
  205. const getApiList = async () => {
  206. loading.value = true;
  207. try {
  208. const params = {
  209. pageNum: pageNum.value,
  210. pageSize: pageSize.value,
  211. keyWord: searchKey.value,
  212. groupId: selectedGroup.value,
  213. method: selectedMethod.value,
  214. status: selectedStatus.value
  215. };
  216. const res = await api.list(params);
  217. if (res) {
  218. availableApis.value = res.list.map((item: any) => {
  219. // 使用APIKey作为唐一标识符
  220. return {
  221. id: item.id,
  222. key: item.key || `${item.id}`,
  223. apiKey: item.apiKey || item.key || `${item.id}`, // 使用apiKey或备选值
  224. name: item.name,
  225. method: item.method,
  226. path: item.path,
  227. status: item.status,
  228. groupId: item.groupId,
  229. groupName: item.groupName
  230. };
  231. });
  232. total.value = res.total;
  233. }
  234. } catch (error) {
  235. ElMessage.error('获取API列表失败');
  236. } finally {
  237. loading.value = false;
  238. }
  239. };
  240. // 获取客户端已关联的API
  241. const getClientApis = async () => {
  242. if (!props.clientId) return;
  243. try {
  244. const res = await api.client.getApis(props.clientId);
  245. if (res && Array.isArray(res)) {
  246. selectedApis.value = res;
  247. // 使用apiKey字段或备选值
  248. selectedApiKeys.value = res.map(item => item.apiKey || item.key || `${item.id}`);
  249. }
  250. } catch (error) {
  251. ElMessage.error('获取客户端API失败');
  252. }
  253. };
  254. // 处理表格选择变化
  255. const handleSelectionChange = (selection: ApiInfo[]) => {
  256. selectedRows.value = selection;
  257. };
  258. // 选择当前页所有API
  259. const selectAllVisible = () => {
  260. apiTable.value?.toggleAllSelection();
  261. };
  262. // 应用权限模板
  263. const applyTemplate = async (templateId: string) => {
  264. const template = permissionTemplates[templateId];
  265. if (!template) return;
  266. loading.value = true;
  267. try {
  268. // 获取所有API
  269. const params = {
  270. pageNum: 1,
  271. pageSize: 1000, // 获取较大数量的API
  272. };
  273. const res = await api.list(params);
  274. if (res && res.list) {
  275. const filteredApis = res.list
  276. .filter((api: any) => template.filter({
  277. id: api.id,
  278. key: api.key || `${api.id}`,
  279. name: api.name,
  280. method: api.method,
  281. path: api.path,
  282. status: api.status
  283. }))
  284. .map((api: any) => ({
  285. id: api.id,
  286. key: api.key || `${api.id}`,
  287. name: api.name,
  288. method: api.method,
  289. path: api.path,
  290. status: api.status,
  291. groupId: api.groupId,
  292. groupName: api.groupName
  293. }));
  294. // 确认是否应用模板
  295. await ElMessageBox.confirm(
  296. `确定要应用"${template.name}"模板吗?这将会添加${filteredApis.length}个API到客户端权限。`,
  297. '确认应用模板',
  298. {
  299. confirmButtonText: '确定',
  300. cancelButtonText: '取消',
  301. type: 'warning'
  302. }
  303. );
  304. // 合并已有的API和模板中的API
  305. const existingApiIds = new Set(selectedApis.value.map(api => api.id));
  306. const newApis = filteredApis.filter(api => !existingApiIds.has(api.id));
  307. selectedApis.value = [...selectedApis.value, ...newApis];
  308. selectedApiKeys.value = selectedApis.value.map(api => api.apiKey);
  309. ElMessage.success(`成功应用"${template.name}"模板,添加了${newApis.length}个API`);
  310. }
  311. } catch (error) {
  312. if (error !== 'cancel') {
  313. ElMessage.error('应用模板失败');
  314. }
  315. } finally {
  316. loading.value = false;
  317. }
  318. };
  319. // 批量添加选中的API
  320. const addSelectedApis = () => {
  321. if (selectedRows.value.length === 0) {
  322. ElMessage.warning('请先选择要添加的API');
  323. return;
  324. }
  325. // 过滤掉已经选择的API
  326. const existingApiIds = new Set(selectedApis.value.map(api => api.id));
  327. const newApis = selectedRows.value.filter(api => !existingApiIds.has(api.id));
  328. if (newApis.length === 0) {
  329. ElMessage.warning('所选API已经全部添加');
  330. return;
  331. }
  332. selectedApis.value = [...selectedApis.value, ...newApis];
  333. selectedApiKeys.value = selectedApis.value.map(api => api.apiKey);
  334. ElMessage.success(`成功添加${newApis.length}个API`);
  335. // 清除表格选择
  336. apiTable.value?.clearSelection();
  337. };
  338. // 添加单个API
  339. const addApi = (api: ApiInfo) => {
  340. if (isApiSelected(api)) {
  341. ElMessage.warning('该API已添加');
  342. return;
  343. }
  344. selectedApis.value.push(api);
  345. selectedApiKeys.value = selectedApis.value.map(item => item.apiKey);
  346. ElMessage.success('添加成功');
  347. };
  348. // 移除单个API
  349. const removeApi = (api: ApiInfo) => {
  350. // 获取要移除的API唯一标识符
  351. const apiIdentifier = api.apiKey || api.key || `${api.id}`;
  352. // 在已选API中查找匹配项
  353. const index = selectedApis.value.findIndex(item => {
  354. const itemIdentifier = item.apiKey || item.key || `${item.id}`;
  355. return itemIdentifier === apiIdentifier;
  356. });
  357. if (index !== -1) {
  358. selectedApis.value.splice(index, 1);
  359. selectedApiKeys.value = selectedApis.value.map(item => item.apiKey || item.key || `${item.id}`);
  360. ElMessage.success('移除成功');
  361. } else {
  362. ElMessage.warning('未找到要移除的API');
  363. }
  364. };
  365. // 移除所有已选API
  366. const removeAllSelected = async () => {
  367. if (selectedApis.value.length === 0) {
  368. ElMessage.warning('暂无已授权的API');
  369. return;
  370. }
  371. try {
  372. await ElMessageBox.confirm('确定要移除所有已授权的API吗?', '警告', {
  373. confirmButtonText: '确定',
  374. cancelButtonText: '取消',
  375. type: 'warning'
  376. });
  377. selectedApis.value = [];
  378. selectedApiKeys.value = [];
  379. ElMessage.success('已移除所有API授权');
  380. } catch (error) {
  381. // 用户取消操作
  382. }
  383. };
  384. // 检查API是否已选中
  385. const isApiSelected = (api: ApiInfo) => {
  386. // 获取API的apiKey或备选标识符
  387. const apiIdentifier = api.apiKey || api.key || `${api.id}`;
  388. // 遍历已选中的API数组,检查是否已存在相同的apiKey
  389. return selectedApis.value.some(selectedApi => {
  390. const selectedApiIdentifier = selectedApi.apiKey || selectedApi.key || `${selectedApi.id}`;
  391. return apiIdentifier === selectedApiIdentifier;
  392. });
  393. };
  394. // 搜索API
  395. const handleSearch = () => {
  396. pageNum.value = 1;
  397. getApiList();
  398. };
  399. // 处理分页大小变化
  400. const handleSizeChange = (size: number) => {
  401. pageSize.value = size;
  402. getApiList();
  403. };
  404. // 处理页码变化
  405. const handleCurrentChange = (page: number) => {
  406. pageNum.value = page;
  407. getApiList();
  408. };
  409. // 获取请求方法对应的标签类型
  410. const getMethodTagType = (method: string) => {
  411. const map: Record<string, string> = {
  412. 'GET': 'success',
  413. 'POST': 'primary',
  414. 'PUT': 'warning',
  415. 'DELETE': 'danger',
  416. 'PATCH': 'info'
  417. };
  418. return map[method] || '';
  419. };
  420. // 获取状态对应的标签类型
  421. const getStatusTagType = (status: string) => {
  422. const map: Record<string, string> = {
  423. 'Published': 'success',
  424. 'Deprecated': 'warning',
  425. 'Draft': 'info'
  426. };
  427. return map[status] || '';
  428. };
  429. // 获取状态文本
  430. const getStatusText = (status: string) => {
  431. const map: Record<string, string> = {
  432. 'Published': '已发布',
  433. 'Deprecated': '已弃用',
  434. 'Draft': '未发布'
  435. };
  436. return map[status] || status;
  437. };
  438. // 初始化
  439. onMounted(() => {
  440. getApiGroups();
  441. getApiList();
  442. getClientApis();
  443. });
  444. </script>
  445. <style scoped>
  446. .client-api-relation {
  447. padding: 15px 0;
  448. }
  449. .filter-container {
  450. margin-bottom: 20px;
  451. }
  452. .relation-container {
  453. margin-top: 20px;
  454. }
  455. .api-list-header,
  456. .selected-api-header {
  457. display: flex;
  458. justify-content: space-between;
  459. align-items: center;
  460. margin-bottom: 15px;
  461. padding: 0 10px;
  462. }
  463. .api-list-header h4,
  464. .selected-api-header h4 {
  465. margin: 0;
  466. font-size: 16px;
  467. font-weight: 500;
  468. color: var(--el-text-color-primary);
  469. }
  470. .api-list-actions {
  471. display: flex;
  472. gap: 8px;
  473. }
  474. .api-list-container,
  475. .selected-api-container {
  476. padding: 15px;
  477. background-color: var(--el-bg-color-overlay);
  478. border: 1px solid var(--el-border-color-light);
  479. border-radius: 4px;
  480. height: 600px;
  481. display: flex;
  482. flex-direction: column;
  483. box-shadow: var(--el-box-shadow-light);
  484. }
  485. .pagination-container {
  486. margin-top: 15px;
  487. display: flex;
  488. justify-content: center;
  489. }
  490. /* 优化深色主题下的表格样式 */
  491. :deep(.el-table) {
  492. --el-table-border-color: var(--el-border-color-lighter);
  493. --el-table-header-bg-color: var(--el-fill-color-dark);
  494. --el-table-row-hover-bg-color: var(--el-fill-color-dark);
  495. background-color: var(--el-bg-color-overlay);
  496. }
  497. :deep(.el-table th) {
  498. background-color: var(--el-fill-color-dark);
  499. color: var(--el-text-color-primary);
  500. font-weight: bold;
  501. }
  502. :deep(.el-table--enable-row-hover .el-table__body tr:hover > td) {
  503. background-color: var(--el-color-primary-light-9);
  504. }
  505. :deep(.el-table td) {
  506. color: var(--el-text-color-regular);
  507. }
  508. :deep(.el-table__empty-text) {
  509. color: var(--el-text-color-secondary);
  510. }
  511. /* 表格状态标识 */
  512. :deep(.el-tag) {
  513. border: 1px solid transparent;
  514. }
  515. :deep(.el-tag--success) {
  516. background-color: var(--el-color-success-light-9);
  517. border-color: var(--el-color-success-light-7);
  518. color: var(--el-color-success);
  519. }
  520. :deep(.el-tag--info) {
  521. background-color: var(--el-color-info-light-9);
  522. border-color: var(--el-color-info-light-7);
  523. color: var(--el-color-info-dark-2);
  524. }
  525. /* 增强表格单元格边框 */
  526. :deep(.el-table--border .el-table__cell) {
  527. border-right: 1px solid var(--el-border-color-lighter);
  528. }
  529. /* 边框分隔线 */
  530. .relation-container :deep(.el-col:first-child) {
  531. position: relative;
  532. }
  533. .relation-container :deep(.el-col:first-child)::after {
  534. content: '';
  535. position: absolute;
  536. top: 0;
  537. right: 0;
  538. height: 100%;
  539. width: 1px;
  540. background-color: var(--el-border-color-lighter);
  541. }
  542. /* 搜索框样式优化 */
  543. .filter-container :deep(.el-input__inner) {
  544. background-color: var(--el-fill-color-blank);
  545. border-color: var(--el-border-color);
  546. }
  547. .filter-container :deep(.el-input__inner:hover) {
  548. border-color: var(--el-color-primary-light-3);
  549. }
  550. .filter-container :deep(.el-input__inner:focus) {
  551. border-color: var(--el-color-primary);
  552. }
  553. /* 按钮样式增强 */
  554. :deep(.el-button--primary) {
  555. box-shadow: 0 2px 6px rgba(var(--el-color-primary-rgb), 0.4);
  556. }
  557. :deep(.el-button--danger) {
  558. box-shadow: 0 2px 6px rgba(var(--el-color-danger-rgb), 0.4);
  559. }
  560. </style>