|
@@ -0,0 +1,521 @@
|
|
|
+<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="500px"
|
|
|
+ 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.key)">
|
|
|
+ 添加
|
|
|
+ </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;
|
|
|
+ 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) => ({
|
|
|
+ id: item.id,
|
|
|
+ key: item.key || `${item.id}`,
|
|
|
+ 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;
|
|
|
+ selectedApiKeys.value = res.map(item => item.key);
|
|
|
+ }
|
|
|
+ } 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 existingKeys = new Set(selectedApiKeys.value);
|
|
|
+ const newApis = filteredApis.filter(api => !existingKeys.has(api.key));
|
|
|
+
|
|
|
+ selectedApis.value = [...selectedApis.value, ...newApis];
|
|
|
+ selectedApiKeys.value = selectedApis.value.map(api => api.key);
|
|
|
+
|
|
|
+ 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 existingKeys = new Set(selectedApiKeys.value);
|
|
|
+ const newApis = selectedRows.value.filter(api => !existingKeys.has(api.key));
|
|
|
+
|
|
|
+ if (newApis.length === 0) {
|
|
|
+ ElMessage.warning('所选API已经全部添加');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedApis.value = [...selectedApis.value, ...newApis];
|
|
|
+ selectedApiKeys.value = selectedApis.value.map(api => api.key);
|
|
|
+ ElMessage.success(`成功添加${newApis.length}个API`);
|
|
|
+
|
|
|
+ // 清除表格选择
|
|
|
+ apiTable.value?.clearSelection();
|
|
|
+};
|
|
|
+
|
|
|
+// 添加单个API
|
|
|
+const addApi = (api: ApiInfo) => {
|
|
|
+ if (isApiSelected(api.key)) {
|
|
|
+ ElMessage.warning('该API已添加');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedApis.value.push(api);
|
|
|
+ selectedApiKeys.value = selectedApis.value.map(item => item.key);
|
|
|
+ ElMessage.success('添加成功');
|
|
|
+};
|
|
|
+
|
|
|
+// 移除单个API
|
|
|
+const removeApi = (api: ApiInfo) => {
|
|
|
+ const index = selectedApis.value.findIndex(item => item.key === api.key);
|
|
|
+ if (index !== -1) {
|
|
|
+ selectedApis.value.splice(index, 1);
|
|
|
+ selectedApiKeys.value = selectedApis.value.map(item => item.key);
|
|
|
+ ElMessage.success('移除成功');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 移除所有已选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 = (apiKey: string) => {
|
|
|
+ return selectedApiKeys.value.includes(apiKey);
|
|
|
+};
|
|
|
+
|
|
|
+// 搜索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;
|
|
|
+}
|
|
|
+
|
|
|
+.api-list-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.api-list-container,
|
|
|
+.selected-api-container {
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ height: 600px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-container {
|
|
|
+ margin-top: 15px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+</style>
|