123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632 |
- <template>
- <div class="client-api-relation">
- <div class="filter-container">
- <el-row :gutter="20">
- <el-col :span="6">
- <el-input v-model="searchKey" placeholder="搜索API名称或路径" clearable @keyup.enter="handleSearch" />
- </el-col>
- <el-col :span="5">
- <el-select v-model="selectedGroup" placeholder="API分组" clearable @change="handleSearch">
- <el-option v-for="group in apiGroups" :key="group.id" :label="group.name" :value="group.id" />
- </el-select>
- </el-col>
- <el-col :span="5">
- <el-select v-model="selectedMethod" placeholder="请求方法" clearable @change="handleSearch">
- <el-option v-for="method in methods" :key="method" :label="method" :value="method" />
- </el-select>
- </el-col>
- <el-col :span="5">
- <el-select v-model="selectedStatus" placeholder="状态" clearable @change="handleSearch">
- <el-option label="已发布" value="Published" />
- <el-option label="已弃用" value="Deprecated" />
- <el-option label="未发布" value="Draft" />
- </el-select>
- </el-col>
- <el-col :span="3">
- <el-button type="primary" @click="handleSearch">
- <el-icon><ele-Search /></el-icon>
- 搜索
- </el-button>
- </el-col>
- </el-row>
- </div>
- <div class="relation-container">
- <el-row :gutter="20">
- <el-col :span="12">
- <div class="api-list-container">
- <div class="api-list-header">
- <h4>可选API列表</h4>
- <div class="api-list-actions">
- <el-button type="primary" size="small" @click="addSelectedApis">添加选中</el-button>
- <el-button size="small" @click="selectAllVisible">选择当前页</el-button>
- <!-- <el-button size="small" @click="applyTemplate('readonly')">应用只读模板</el-button>
- <el-button size="small" @click="applyTemplate('fullaccess')">应用完全访问</el-button> -->
- </div>
- </div>
- <el-table
- ref="apiTable"
- :data="availableApis"
- style="width: 100%"
- height="450px"
- v-loading="loading"
- @selection-change="handleSelectionChange">
- <el-table-column type="selection" width="55" />
- <el-table-column prop="name" label="API名称" show-overflow-tooltip />
- <el-table-column prop="method" label="请求方法" width="100">
- <template #default="scope">
- <el-tag
- :type="getMethodTagType(scope.row.method)"
- size="small">
- {{ scope.row.method }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="path" label="API路径" show-overflow-tooltip />
- <el-table-column prop="status" label="状态" width="80">
- <template #default="scope">
- <el-tag
- :type="getStatusTagType(scope.row.status)"
- size="small">
- {{ getStatusText(scope.row.status) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="80" fixed="right">
- <template #default="scope">
- <el-button
- type="primary"
- size="small"
- @click="addApi(scope.row)"
- :disabled="isApiSelected(scope.row)">
- 添加
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-container">
- <el-pagination
- v-model:current-page="pageNum"
- v-model:page-size="pageSize"
- :page-sizes="[10, 20, 50, 100]"
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </div>
- </el-col>
- <el-col :span="12">
- <div class="selected-api-container">
- <div class="selected-api-header">
- <h4>已授权API ({{ selectedApis.length }})</h4>
- <el-button type="danger" size="small" @click="removeAllSelected">移除全部</el-button>
- </div>
- <el-table
- :data="selectedApis"
- style="width: 100%"
- height="500px">
- <el-table-column prop="name" label="API名称" show-overflow-tooltip />
- <el-table-column prop="method" label="请求方法" width="100">
- <template #default="scope">
- <el-tag
- :type="getMethodTagType(scope.row.method)"
- size="small">
- {{ scope.row.method }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="path" label="API路径" show-overflow-tooltip />
- <el-table-column label="操作" width="80" fixed="right">
- <template #default="scope">
- <el-button
- type="danger"
- size="small"
- @click="removeApi(scope.row)">
- 移除
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </el-col>
- </el-row>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { ref, onMounted, watch, defineProps, defineEmits } from 'vue';
- import { ElMessage, ElMessageBox } from 'element-plus';
- import api from '/@/api/modules/apiHub';
- // 定义API数据类型
- interface ApiInfo {
- id: number;
- key: string;
- apiKey: string; // 添加apiKey字段
- name: string;
- method: string;
- path: string;
- status: string;
- groupId?: number;
- groupName?: string;
- }
- const props = defineProps({
- clientId: {
- type: String,
- required: true
- }
- });
- const emit = defineEmits(['update:apiKeys']);
- // 数据状态
- const loading = ref(false);
- const searchKey = ref('');
- const selectedGroup = ref('');
- const selectedMethod = ref('');
- const selectedStatus = ref('');
- const pageNum = ref(1);
- const pageSize = ref(20);
- const total = ref(0);
- // API 数据
- const availableApis = ref<ApiInfo[]>([]);
- const apiGroups = ref<{id: number, name: string}[]>([]);
- const selectedApis = ref<ApiInfo[]>([]);
- const selectedApiKeys = ref<string[]>([]);
- const apiTable = ref();
- const selectedRows = ref<ApiInfo[]>([]);
- // 预设模板
- const permissionTemplates = {
- readonly: {
- name: '只读访问',
- filter: (api: ApiInfo) => api.method === 'GET'
- },
- fullaccess: {
- name: '完全访问',
- filter: (api: ApiInfo) => true
- }
- };
- // 请求方法列表
- const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
- // 监听选中的API变化,通知父组件
- watch(selectedApiKeys, (newKeys) => {
- emit('update:apiKeys', newKeys);
- });
- // 获取API分组
- const getApiGroups = async () => {
- try {
- const res = await api.group.tree();
- if (res) {
- apiGroups.value = res;
- }
- } catch (error) {
- ElMessage.error('获取API分组失败');
- }
- };
- // 获取API列表
- const getApiList = async () => {
- loading.value = true;
- try {
- const params = {
- pageNum: pageNum.value,
- pageSize: pageSize.value,
- keyWord: searchKey.value,
- groupId: selectedGroup.value,
- method: selectedMethod.value,
- status: selectedStatus.value
- };
-
- const res = await api.list(params);
- if (res) {
- availableApis.value = res.list.map((item: any) => {
- // 使用APIKey作为唐一标识符
- return {
- id: item.id,
- key: item.key || `${item.id}`,
- apiKey: item.apiKey || item.key || `${item.id}`, // 使用apiKey或备选值
- name: item.name,
- method: item.method,
- path: item.path,
- status: item.status,
- groupId: item.groupId,
- groupName: item.groupName
- };
- });
- total.value = res.total;
- }
- } catch (error) {
- ElMessage.error('获取API列表失败');
- } finally {
- loading.value = false;
- }
- };
- // 获取客户端已关联的API
- const getClientApis = async () => {
- if (!props.clientId) return;
-
- try {
- const res = await api.client.getApis(props.clientId);
- if (res && Array.isArray(res)) {
- selectedApis.value = res;
- // 使用apiKey字段或备选值
- selectedApiKeys.value = res.map(item => item.apiKey || item.key || `${item.id}`);
- }
- } catch (error) {
- ElMessage.error('获取客户端API失败');
- }
- };
- // 处理表格选择变化
- const handleSelectionChange = (selection: ApiInfo[]) => {
- selectedRows.value = selection;
- };
- // 选择当前页所有API
- const selectAllVisible = () => {
- apiTable.value?.toggleAllSelection();
- };
- // 应用权限模板
- const applyTemplate = async (templateId: string) => {
- const template = permissionTemplates[templateId];
- if (!template) return;
-
- loading.value = true;
- try {
- // 获取所有API
- const params = {
- pageNum: 1,
- pageSize: 1000, // 获取较大数量的API
- };
-
- const res = await api.list(params);
- if (res && res.list) {
- const filteredApis = res.list
- .filter((api: any) => template.filter({
- id: api.id,
- key: api.key || `${api.id}`,
- name: api.name,
- method: api.method,
- path: api.path,
- status: api.status
- }))
- .map((api: any) => ({
- id: api.id,
- key: api.key || `${api.id}`,
- name: api.name,
- method: api.method,
- path: api.path,
- status: api.status,
- groupId: api.groupId,
- groupName: api.groupName
- }));
-
- // 确认是否应用模板
- await ElMessageBox.confirm(
- `确定要应用"${template.name}"模板吗?这将会添加${filteredApis.length}个API到客户端权限。`,
- '确认应用模板',
- {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }
- );
-
- // 合并已有的API和模板中的API
- const existingApiIds = new Set(selectedApis.value.map(api => api.id));
- const newApis = filteredApis.filter(api => !existingApiIds.has(api.id));
-
- selectedApis.value = [...selectedApis.value, ...newApis];
- selectedApiKeys.value = selectedApis.value.map(api => api.apiKey);
-
- ElMessage.success(`成功应用"${template.name}"模板,添加了${newApis.length}个API`);
- }
- } catch (error) {
- if (error !== 'cancel') {
- ElMessage.error('应用模板失败');
- }
- } finally {
- loading.value = false;
- }
- };
- // 批量添加选中的API
- const addSelectedApis = () => {
- if (selectedRows.value.length === 0) {
- ElMessage.warning('请先选择要添加的API');
- return;
- }
-
- // 过滤掉已经选择的API
- const existingApiIds = new Set(selectedApis.value.map(api => api.id));
- const newApis = selectedRows.value.filter(api => !existingApiIds.has(api.id));
-
- if (newApis.length === 0) {
- ElMessage.warning('所选API已经全部添加');
- return;
- }
-
- selectedApis.value = [...selectedApis.value, ...newApis];
- selectedApiKeys.value = selectedApis.value.map(api => api.apiKey);
- ElMessage.success(`成功添加${newApis.length}个API`);
-
- // 清除表格选择
- apiTable.value?.clearSelection();
- };
- // 添加单个API
- const addApi = (api: ApiInfo) => {
- if (isApiSelected(api)) {
- ElMessage.warning('该API已添加');
- return;
- }
-
- selectedApis.value.push(api);
- selectedApiKeys.value = selectedApis.value.map(item => item.apiKey);
- ElMessage.success('添加成功');
- };
- // 移除单个API
- const removeApi = (api: ApiInfo) => {
- // 获取要移除的API唯一标识符
- const apiIdentifier = api.apiKey || api.key || `${api.id}`;
-
- // 在已选API中查找匹配项
- const index = selectedApis.value.findIndex(item => {
- const itemIdentifier = item.apiKey || item.key || `${item.id}`;
- return itemIdentifier === apiIdentifier;
- });
-
- if (index !== -1) {
- selectedApis.value.splice(index, 1);
- selectedApiKeys.value = selectedApis.value.map(item => item.apiKey || item.key || `${item.id}`);
- ElMessage.success('移除成功');
- } else {
- ElMessage.warning('未找到要移除的API');
- }
- };
- // 移除所有已选API
- const removeAllSelected = async () => {
- if (selectedApis.value.length === 0) {
- ElMessage.warning('暂无已授权的API');
- return;
- }
-
- try {
- await ElMessageBox.confirm('确定要移除所有已授权的API吗?', '警告', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- });
-
- selectedApis.value = [];
- selectedApiKeys.value = [];
- ElMessage.success('已移除所有API授权');
- } catch (error) {
- // 用户取消操作
- }
- };
- // 检查API是否已选中
- const isApiSelected = (api: ApiInfo) => {
- // 获取API的apiKey或备选标识符
- const apiIdentifier = api.apiKey || api.key || `${api.id}`;
- // 遍历已选中的API数组,检查是否已存在相同的apiKey
- return selectedApis.value.some(selectedApi => {
- const selectedApiIdentifier = selectedApi.apiKey || selectedApi.key || `${selectedApi.id}`;
- return apiIdentifier === selectedApiIdentifier;
- });
- };
- // 搜索API
- const handleSearch = () => {
- pageNum.value = 1;
- getApiList();
- };
- // 处理分页大小变化
- const handleSizeChange = (size: number) => {
- pageSize.value = size;
- getApiList();
- };
- // 处理页码变化
- const handleCurrentChange = (page: number) => {
- pageNum.value = page;
- getApiList();
- };
- // 获取请求方法对应的标签类型
- const getMethodTagType = (method: string) => {
- const map: Record<string, string> = {
- 'GET': 'success',
- 'POST': 'primary',
- 'PUT': 'warning',
- 'DELETE': 'danger',
- 'PATCH': 'info'
- };
-
- return map[method] || '';
- };
- // 获取状态对应的标签类型
- const getStatusTagType = (status: string) => {
- const map: Record<string, string> = {
- 'Published': 'success',
- 'Deprecated': 'warning',
- 'Draft': 'info'
- };
-
- return map[status] || '';
- };
- // 获取状态文本
- const getStatusText = (status: string) => {
- const map: Record<string, string> = {
- 'Published': '已发布',
- 'Deprecated': '已弃用',
- 'Draft': '未发布'
- };
-
- return map[status] || status;
- };
- // 初始化
- onMounted(() => {
- getApiGroups();
- getApiList();
- getClientApis();
- });
- </script>
- <style scoped>
- .client-api-relation {
- padding: 15px 0;
- }
- .filter-container {
- margin-bottom: 20px;
- }
- .relation-container {
- margin-top: 20px;
- }
- .api-list-header,
- .selected-api-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
- padding: 0 10px;
- }
- .api-list-header h4,
- .selected-api-header h4 {
- margin: 0;
- font-size: 16px;
- font-weight: 500;
- color: var(--el-text-color-primary);
- }
- .api-list-actions {
- display: flex;
- gap: 8px;
- }
- .api-list-container,
- .selected-api-container {
- padding: 15px;
- background-color: var(--el-bg-color-overlay);
- border: 1px solid var(--el-border-color-light);
- border-radius: 4px;
- height: 600px;
- display: flex;
- flex-direction: column;
- box-shadow: var(--el-box-shadow-light);
- }
- .pagination-container {
- margin-top: 15px;
- display: flex;
- justify-content: center;
- }
- /* 优化深色主题下的表格样式 */
- :deep(.el-table) {
- --el-table-border-color: var(--el-border-color-lighter);
- --el-table-header-bg-color: var(--el-fill-color-dark);
- --el-table-row-hover-bg-color: var(--el-fill-color-dark);
- background-color: var(--el-bg-color-overlay);
- }
- :deep(.el-table th) {
- background-color: var(--el-fill-color-dark);
- color: var(--el-text-color-primary);
- font-weight: bold;
- }
- :deep(.el-table--enable-row-hover .el-table__body tr:hover > td) {
- background-color: var(--el-color-primary-light-9);
- }
- :deep(.el-table td) {
- color: var(--el-text-color-regular);
- }
- :deep(.el-table__empty-text) {
- color: var(--el-text-color-secondary);
- }
- /* 表格状态标识 */
- :deep(.el-tag) {
- border: 1px solid transparent;
- }
- :deep(.el-tag--success) {
- background-color: var(--el-color-success-light-9);
- border-color: var(--el-color-success-light-7);
- color: var(--el-color-success);
- }
- :deep(.el-tag--info) {
- background-color: var(--el-color-info-light-9);
- border-color: var(--el-color-info-light-7);
- color: var(--el-color-info-dark-2);
- }
- /* 增强表格单元格边框 */
- :deep(.el-table--border .el-table__cell) {
- border-right: 1px solid var(--el-border-color-lighter);
- }
- /* 边框分隔线 */
- .relation-container :deep(.el-col:first-child) {
- position: relative;
- }
- .relation-container :deep(.el-col:first-child)::after {
- content: '';
- position: absolute;
- top: 0;
- right: 0;
- height: 100%;
- width: 1px;
- background-color: var(--el-border-color-lighter);
- }
- /* 搜索框样式优化 */
- .filter-container :deep(.el-input__inner) {
- background-color: var(--el-fill-color-blank);
- border-color: var(--el-border-color);
- }
- .filter-container :deep(.el-input__inner:hover) {
- border-color: var(--el-color-primary-light-3);
- }
- .filter-container :deep(.el-input__inner:focus) {
- border-color: var(--el-color-primary);
- }
- /* 按钮样式增强 */
- :deep(.el-button--primary) {
- box-shadow: 0 2px 6px rgba(var(--el-color-primary-rgb), 0.4);
- }
- :deep(.el-button--danger) {
- box-shadow: 0 2px 6px rgba(var(--el-color-danger-rgb), 0.4);
- }
- </style>
|