Browse Source

feat:添加客户关管理页面

microrain 5 months ago
parent
commit
94d27e58cf
2 changed files with 460 additions and 0 deletions
  1. 8 0
      src/api/modules/apiHub.ts
  2. 452 0
      src/views/apihub/client.vue

+ 8 - 0
src/api/modules/apiHub.ts

@@ -23,5 +23,13 @@ export default {
     edit: (data: object) => put('/datasource/edit', data),
     delete: (ids: number[]) => del('/datasource/delete', { ids }),
     test: (data: object) => post('/datasource/test', data),
+  },
+  client: {
+    list: (params: object) => get('/client/list', params),
+    get: (id: number) => get(`/client/get`, { id }),
+    add: (data: object) => post('/client/add', data),
+    edit: (data: object) => put('/client/edit', data),
+    delete: (ids: number[]) => del('/client/delete', { ids }),
+    resetSecret: (id: number) => put('/client/reset-secret', { id })
   }
 }

+ 452 - 0
src/views/apihub/client.vue

@@ -0,0 +1,452 @@
+<template>
+  <div class="page">
+    <div class="client-container">
+      <el-card shadow="never">
+        <div class="client-header">
+          <h3>API客户端管理</h3>
+        </div>
+        <el-form :model="params" inline ref="queryRef">
+          <el-form-item label="关键字" prop="keyWord">
+            <el-input v-model="params.keyWord" placeholder="输入客户端名称或标识" clearable style="width: 170px" @keyup.enter.native="getList(1)" />
+          </el-form-item>
+          <el-form-item label="状态" prop="status">
+            <el-select v-model="params.status" placeholder="请选择状态" clearable style="width: 120px">
+              <el-option label="全部" value="" />
+              <el-option label="激活" value="Active" />
+              <el-option label="未激活" value="Inactive" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="日期范围" prop="dateRange">
+            <el-date-picker v-model="params.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 240px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" class="ml10" @click="getList(1)">
+              <el-icon>
+                <ele-Search />
+              </el-icon>
+              查询
+            </el-button>
+<!--            <el-button @click="resetQuery()">-->
+<!--              <el-icon>-->
+<!--                <ele-Refresh />-->
+<!--              </el-icon>-->
+<!--              重置-->
+<!--            </el-button>-->
+            <el-button type="primary" @click="addOrEdit()" v-auth="'add'">
+              <el-icon>
+                <ele-Plus />
+              </el-icon>
+              新增客户端
+            </el-button>
+          </el-form-item>
+        </el-form>
+        <el-table :data="tableData" style="width: 100%" v-loading="loading" row-key="id">
+          <el-table-column type="selection" width="40" align="center" />
+          <el-table-column prop="clientId" label="客户端标识" min-width="140" show-overflow-tooltip></el-table-column>
+          <el-table-column prop="name" label="客户端名称" min-width="140" show-overflow-tooltip></el-table-column>
+          <el-table-column prop="remark" label="备注"  show-overflow-tooltip></el-table-column>
+          <el-table-column prop="status" label="状态" width="100" align="center">
+            <template #default="scope">
+              <el-tag size="small" type="success" v-if="scope.row.status === 'Active'">激活</el-tag>
+              <el-tag size="small" type="info" v-else-if="scope.row.status === 'Inactive'">未激活</el-tag>
+              <span v-else>{{ scope.row.status }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="createdAt" label="创建时间" width="160" align="center"></el-table-column>
+          <el-table-column label="操作" width="220" align="center" fixed="right">
+            <template #default="scope">
+              <div class="flex-row">
+                <el-button size="small" text type="primary" @click="viewDetail(scope.row)" v-auth="'view'">查看</el-button>
+                <el-button size="small" text type="warning" @click="addOrEdit(scope.row)" v-auth="'edit'">编辑</el-button>
+                <el-button size="small" text type="success" @click="resetSecret(scope.row)" v-auth="'reset_secret'">重置密钥</el-button>
+                <el-button size="small" text type="danger" @click="deleteClient(scope.row)" v-auth="'delete'">删除</el-button>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <pagination v-if="params.total" :total="params.total" v-model:page="params.pageNum" v-model:limit="params.pageSize" @pagination="getList()" />
+      </el-card>
+    </div>
+
+    <!-- 客户端表单弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="formTitle" width="600px" destroy-on-close>
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+        <el-form-item label="客户端标识" prop="clientId">
+          <el-input v-model="form.clientId" placeholder="留空默认由系统自动生成" :disabled="!!form.id" />
+        </el-form-item>
+        <el-form-item label="客户端名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入客户端名称" />
+        </el-form-item>
+
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio label="Active">激活</el-radio>
+            <el-radio label="Inactive">未激活</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="IP白名单" prop="ipWhitelist">
+          <el-input v-model="form.ipWhitelist" placeholder="多个IP用逗号分隔" />
+          <div class="form-tip">多个IP用逗号分隔,留空表示不限制</div>
+        </el-form-item>
+        <el-form-item label="IP黑名单" prop="ipBlacklist">
+          <el-input v-model="form.ipBlacklist" placeholder="多个IP用逗号分隔" />
+          <div class="form-tip">多个IP用逗号分隔,留空表示不限制</div>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 客户端详情弹窗 -->
+    <el-dialog v-model="detailVisible" title="客户端详情" width="600px" destroy-on-close>
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="客户端标识">{{ detail.clientId }}</el-descriptions-item>
+        <el-descriptions-item label="客户端名称">{{ detail.name }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag size="small" type="success" v-if="detail.status === 'Active'">激活</el-tag>
+          <el-tag size="small" type="info" v-else-if="detail.status === 'Inactive'">未激活</el-tag>
+          <span v-else>{{ detail.status }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="允许访问的API">
+          <div v-if="detail.apiNames && detail.apiNames.length > 0">
+            <el-tag v-for="(api, index) in detail.apiNames" :key="index" size="small" class="api-tag">
+              {{ api }}
+            </el-tag>
+          </div>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="IP白名单">{{ detail.ipWhitelist || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="IP黑名单">{{ detail.ipBlacklist || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="备注">{{ detail.remark || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ detail.createdAt }}</el-descriptions-item>
+        <el-descriptions-item label="更新时间">{{ detail.updatedAt }}</el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="detailVisible = false">关闭</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 密钥重置结果弹窗 -->
+    <el-dialog v-model="secretVisible" title="客户端密钥" width="500px" destroy-on-close>
+      <div class="secret-result">
+        <el-alert
+          title="请妥善保管以下密钥信息,它仅会显示一次!"
+          type="warning"
+          :closable="false"
+          show-icon
+        />
+        <div class="secret-info" v-if="secretResult.clientSecret">
+          <div class="secret-item">
+            <div class="secret-label">客户端密钥:</div>
+            <div class="secret-value">{{ secretResult.clientSecret }}</div>
+            <el-button size="small" type="primary" @click="copyText(secretResult.clientSecret)">复制</el-button>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="secretVisible = false">关闭</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, onMounted } from "vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import api from "/@/api/modules/apiHub";
+
+// 定义客户端接口类型
+interface ClientInfo {
+  id?: number;
+  clientId: string;
+  name: string;
+  apiIds?: number[];
+  apiNames?: string[];
+  status: string;
+  ipWhitelist?: string;
+  ipBlacklist?: string;
+  remark?: string;
+  createdAt?: string;
+  updatedAt?: string;
+}
+
+// 引用组件
+const queryRef = ref();
+const formRef = ref();
+
+// 表格数据和加载状态
+const loading = ref(false);
+const params = reactive({
+  keyWord: '',
+  status: '',
+  dateRange: [],
+  pageNum: 1,
+  pageSize: 20,
+  total: 0
+});
+const tableData = ref<ClientInfo[]>([]);
+
+// 获取列表数据
+const getList = async (pageNum?: number) => {
+  if (typeof pageNum === 'number') {
+    params.pageNum = pageNum;
+  }
+  loading.value = true;
+  try {
+    const res = await api.client.list(params);
+    // 根据request.ts中的响应拦截器处理,直接使用返回的数据
+    if (res && res.list) {
+      tableData.value = res.list || [];
+      params.total = res.total || 0;
+    } else {
+      tableData.value = [];
+      params.total = 0;
+    }
+  } catch (error) {
+    ElMessage.error('获取客户端列表失败');
+    tableData.value = [];
+    params.total = 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 表单相关
+const dialogVisible = ref(false);
+const formTitle = ref('');
+const submitLoading = ref(false);
+const form = reactive<ClientInfo>({
+  clientId: '',
+  name: '',
+  apiIds: [],
+  status: 'Active',
+  ipWhitelist: '',
+  ipBlacklist: '',
+  remark: ''
+});
+const rules = {
+  clientId: [{ required: false, message: '请输入客户端标识', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入客户端名称', trigger: 'blur' }],
+  status: [{ required: true, message: '请选择状态', trigger: 'change' }]
+};
+
+// 详情弹窗
+const detailVisible = ref(false);
+const detail = ref<ClientInfo>({
+  clientId: '',
+  name: '',
+  status: '',
+});
+
+// 密钥结果弹窗
+const secretVisible = ref(false);
+const secretResult = ref({
+  clientSecret: ''
+});
+
+// 初始化
+onMounted(() => {
+  getList(1);
+});
+
+// 重置查询表单
+const resetQuery = () => {
+  queryRef.value?.resetFields();
+  params.keyWord = '';
+  params.status = '';
+  params.dateRange = [];
+  getList(1);
+};
+
+// 新增或编辑客户端
+const addOrEdit = (row?: ClientInfo) => {
+  resetForm();
+  if (row && row.id) {
+    formTitle.value = '编辑客户端';
+    // 获取详细信息
+    api.client.get(row.id).then((res: any) => {
+      if (res) {
+        Object.assign(form, res);
+        dialogVisible.value = true;
+      }
+    });
+  } else {
+    formTitle.value = '新增客户端';
+    dialogVisible.value = true;
+  }
+};
+
+// 重置表单
+const resetForm = () => {
+  if (formRef.value) {
+    formRef.value.resetFields();
+  }
+  Object.assign(form, {
+    id: undefined,
+    clientId: '',
+    name: '',
+    status: 'Active',
+    ipWhitelist: '',
+    ipBlacklist: '',
+    remark: ''
+  });
+};
+
+// 提交表单
+const submitForm = () => {
+  formRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      submitLoading.value = true;
+      try {
+        const apiMethod = form.id ? api.client.edit : api.client.add;
+        const res = await apiMethod(form);
+        if (res.code === 0) {
+          ElMessage.success(form.id ? '编辑成功' : '添加成功');
+          dialogVisible.value = false;
+          getList(1);
+
+          // 如果是新增,显示密钥信息
+          if (!form.id && res.data && res.data.clientSecret) {
+            secretResult.value = {
+              clientSecret: res.data.clientSecret
+            };
+            secretVisible.value = true;
+          }
+        }
+      } catch (error) {
+        ElMessage.error(form.id ? '编辑失败' : '添加失败');
+      } finally {
+        submitLoading.value = false;
+      }
+    }
+  });
+};
+
+// 查看客户端详情
+const viewDetail = (row: ClientInfo) => {
+  api.client.get(row.id as number).then((res: any) => {
+    if (res) {
+      detail.value = res;
+      detailVisible.value = true;
+    }
+  });
+};
+
+// 重置客户端密钥
+const resetSecret = (row: ClientInfo) => {
+  ElMessageBox.confirm(`确定要重置客户端"${row.name}"的密钥吗?重置后原密钥将失效。`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    api.client.resetSecret(row.id as number).then((res: any) => {
+      if (res.code === 0) {
+        ElMessage.success('密钥重置成功');
+        secretResult.value = {
+          clientSecret: res.data.clientSecret
+        };
+        secretVisible.value = true;
+      }
+    });
+  }).catch(() => {});
+};
+
+// 删除客户端
+const deleteClient = (row: ClientInfo) => {
+  ElMessageBox.confirm(`确定要删除客户端"${row.name}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    api.client.delete([row.id as number]).then((res: any) => {
+      if (res) {
+        ElMessage.success('删除成功');
+        getList(params.pageNum);
+      }
+    });
+  }).catch(() => {});
+};
+
+// 复制文本
+const copyText = (text: string) => {
+  navigator.clipboard.writeText(text).then(() => {
+    ElMessage.success('复制成功');
+  }).catch(() => {
+    ElMessage.error('复制失败,请手动复制');
+  });
+};
+</script>
+
+<style scoped>
+.client-container {
+  width: 100%;
+}
+
+.client-header {
+  margin-bottom: 20px;
+}
+
+.ml10 {
+  margin-left: 10px;
+}
+
+.flex-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.api-tag {
+  margin-right: 5px;
+  margin-bottom: 5px;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 5px;
+}
+
+.secret-result {
+  padding: 10px 0;
+}
+
+.secret-info {
+  margin-top: 20px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  padding: 15px;
+}
+
+.secret-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.secret-label {
+  width: 100px;
+  font-weight: bold;
+}
+
+.secret-value {
+  flex: 1;
+  word-break: break-all;
+  font-family: monospace;
+  background-color: #ebeef5;
+  padding: 5px 10px;
+  border-radius: 3px;
+  margin-right: 10px;
+}
+</style>