|
@@ -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>
|