123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- <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 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="客户端标识" v-col="'clientId'" min-width="140" show-overflow-tooltip></el-table-column>
- <el-table-column prop="name" label="客户端名称" v-col="'name'" min-width="140" show-overflow-tooltip></el-table-column>
- <el-table-column prop="remark" label="备注" v-col="'remark'" show-overflow-tooltip></el-table-column>
- <el-table-column prop="status" label="状态" v-col="'status'" 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="300" align="center" fixed="right" v-col="'handle'">
- <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="primary" @click="manageApiAuth(scope.row)" v-auth="'edit'">API权限</el-button>
- <el-button size="small" text type="success" @click="resetSecret(scope.row)" v-auth="'reset'">重置密钥</el-button>
- <el-button size="small" text type="danger" @click="deleteClient(scope.row)" v-auth="'del'">删除</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>
- <!-- API权限管理对话框 -->
- <el-dialog v-model="apiAuthVisible" :title="`客户端 '${currentClient.name}' 的API权限管理`" width="1100px" append-to-body destroy-on-close class="api-permission-dialog">
- <client-api-relation :client-id="currentClient.clientId" @update:apiKeys="handleApiKeysUpdate" />
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="apiAuthVisible = false">取消</el-button>
- <el-button type="primary" @click="saveApiRelations" :loading="apiSaveLoading">保存</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="600px" 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";
- import ClientApiRelation from "./component/ClientApiRelation.vue";
- // 定义客户端接口类型
- 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 apiSaveLoading = ref(false);
- const selectedApiKeys = ref<string[]>([]);
- const apiAuthVisible = ref(false);
- const currentClient = ref<ClientInfo>({} as ClientInfo);
- 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) {
- ElMessage.success(form.id ? "编辑成功" : "添加成功");
- dialogVisible.value = false;
- getList(1);
- // 如果是新增,显示密钥信息
- if (!form.id && res.clientSecret) {
- secretResult.value = {
- clientSecret: res.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 = async (row: ClientInfo) => {
- try {
- await ElMessageBox.confirm(`确定要重置客户端"${row.name}"的密钥吗?\n注意:重置后原密钥将失效!`, "提示", {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- type: "warning",
- });
- const res = await api.client.resetSecret(row.id as number);
- if (res) {
- ElMessage.success("密钥重置成功");
- secretResult.value = {
- clientSecret: res.clientSecret,
- };
- secretVisible.value = true;
- }
- } catch (error) {
- // 用户取消操作
- }
- };
- // 删除客户端
- const deleteClient = (row: ClientInfo) => {
- ElMessageBox.confirm(`确定要删除客户端「${row.name}」吗?`, "警告", {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- type: "warning",
- })
- .then(async () => {
- loading.value = true;
- try {
- await api.client.delete([row.id as number]);
- ElMessage.success("删除成功");
- getList();
- } catch (error) {
- ElMessage.error("删除失败");
- } finally {
- loading.value = false;
- }
- })
- .catch(() => {
- // 用户取消删除操作
- });
- };
- // 复制文本
- const copyText = (text: string) => {
- navigator.clipboard
- .writeText(text)
- .then(() => {
- ElMessage.success("复制成功");
- })
- .catch(() => {
- ElMessage.error("复制失败,请手动复制");
- });
- };
- // 处理API Keys更新
- const handleApiKeysUpdate = (apiKeys: string[]) => {
- selectedApiKeys.value = apiKeys;
- };
- // 打开API权限管理窗口
- const manageApiAuth = (row: ClientInfo) => {
- currentClient.value = { ...row };
- apiAuthVisible.value = true;
- };
- // 保存API关联
- const saveApiRelations = async () => {
- if (!currentClient.value.clientId) {
- ElMessage.warning("客户端信息不完整");
- return;
- }
- apiSaveLoading.value = true;
- try {
- await api.client.save_apis(currentClient.value.clientId, selectedApiKeys.value);
- ElMessage.success("API权限关联保存成功");
- apiAuthVisible.value = false;
- } catch (error) {
- ElMessage.error("API权限关联保存失败");
- } finally {
- apiSaveLoading.value = false;
- }
- };
- </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;
- }
- /* 深色主题下的样式 */
- [data-theme="dark"] .secret-info {
- margin-top: 20px;
- background-color: #2f3030;
- border-radius: 4px;
- padding: 15px;
- color: white;
- }
- .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;
- }
- /* 深色主题下的样式 */
- [data-theme="dark"] .secret-value {
- flex: 1;
- word-break: break-all;
- font-family: monospace;
- background-color: #575656;
- padding: 10px;
- border-radius: 3px;
- margin-right: 10px;
- }
- .client-detail-tabs {
- margin-top: 20px;
- }
- .custom-upload {
- margin-left: 10px;
- color: #409eff;
- cursor: pointer;
- }
- .dialog-header {
- padding: 0;
- margin: 0;
- display: flex;
- align-items: center;
- justify-content: space-between;
- h4 {
- margin: 0;
- font-weight: 500;
- }
- .close-btn {
- padding: 0;
- margin: 0;
- cursor: pointer;
- }
- }
- /* API权限对话框样式优化 */
- :deep(.api-permission-dialog) {
- .el-dialog__header {
- background-color: var(--el-bg-color);
- padding: 15px 20px;
- margin-right: 0;
- border-bottom: 1px solid var(--el-border-color-lighter);
- }
- .el-dialog__title {
- font-weight: bold;
- color: var(--el-text-color-primary);
- }
- .el-dialog__body {
- background-color: var(--el-bg-color-page);
- padding: 20px;
- }
- .el-dialog__footer {
- background-color: var(--el-bg-color);
- border-top: 1px solid var(--el-border-color-lighter);
- padding: 10px 20px;
- }
- }
- </style>
|