Skip to main content
Glama
OpenAPIManager.vue85.3 kB
<template> <div class="openapi-manager"> <!-- 认证检查加载状态 --> <div v-if="authChecking" class="auth-loading" style=" height: 100vh; display: flex; align-items: center; justify-content: center; " > <div style="text-align: center"> <el-icon size="48" class="is-loading" style="margin-bottom: 16px"> <Loading /> </el-icon> <p style="color: #606266; margin: 0">{{ t("openapi.checkingAuth") }}</p> </div> </div> <!-- 未认证状态 --> <div v-else-if="!isAuthenticated" class="auth-required" style=" height: 100vh; display: flex; align-items: center; justify-content: center; " > <el-result icon="warning" :title="t('openapi.authRequired')" :sub-title="t('openapi.pleaseLogin')" > <template #extra> <el-button type="primary" @click="checkAuthentication"> {{ t("openapi.retryAuth") }} </el-button> </template> </el-result> </div> <!-- 已认证状态 - 正常页面内容 --> <template v-else> <!-- 页面头部 --> <div class="header-section"> <div class="header-content"> <h1> <el-icon><Document /></el-icon> {{ t("openapi.title") }} </h1> <p class="header-description">{{ t("openapi.description") }}</p> </div> <div class="header-actions"> <el-button type="primary" @click="showUploadDialog = true" :icon="Upload" > {{ t("openapi.uploadFile") }} </el-button> <el-button type="success" @click="showUrlDialog = true" :icon="Link"> {{ t("openapi.importFromUrl") }} </el-button> <el-button @click="refreshDocuments" :loading="loading" :icon="Refresh" > {{ t("common.refresh") }} </el-button> </div> </div> <!-- 主要内容区域 --> <div class="manager-content"> <el-row :gutter="24" style="height: calc(100vh - 80px)"> <!-- 左侧:文档列表 --> <el-col :span="8"> <el-card shadow="always" class="document-list-card" style="height: 100%" > <template #header> <div class="list-header"> <span> <el-icon><Folder /></el-icon> {{ t("openapi.specList") }} ({{ documents.length }}) </span> </div> </template> <!-- 搜索框 --> <div class="search-container"> <el-input v-model="searchQuery" :placeholder="t('openapi.searchPlaceholder')" clearable size="small" > <template #prefix> <el-icon><Search /></el-icon> </template> </el-input> </div> <!-- 文档列表 --> <div class="document-list"> <el-empty v-if="!filteredDocuments.length" :description="t('openapi.noSpecs')" /> <div v-else class="document-items"> <div v-for="doc in filteredDocuments" :key="doc.id" class="document-item" :class="{ active: selectedDocument?.id === doc.id }" @click="selectDocument(doc)" > <div class="document-info"> <div class="document-header"> <div class="document-name">{{ doc.name }}</div> <span class="upload-time">{{ formatDate(doc.updatedAt) }}</span> </div> <div class="document-meta"> <DocumentStatusProgress :status=" doc.status as | 'valid' | 'invalid' | 'pending' | 'unknown' " size="small" /> </div> </div> <div class="document-actions"> <div class="action-buttons"> <el-button size="small" @click.stop="editDocument(doc)"> <el-icon><Edit /></el-icon> </el-button> <el-button size="small" @click.stop="deleteDocument(doc.id)" > <el-icon><Delete /></el-icon> </el-button> </div> </div> </div> </div> </div> </el-card> </el-col> <!-- 右侧:文档详情 --> <el-col :span="16"> <el-card shadow="always" class="detail-card" style="height: 100%"> <template #header> <div class="detail-header" v-if="selectedDocument"> <span> <el-icon><Document /></el-icon> {{ selectedDocument.name }} </span> <div class="detail-controls"> <el-button-group> <el-button :type="activeTab === 'editor' ? 'primary' : ''" size="small" @click="activeTab = 'editor'" > <el-icon><Edit /></el-icon> {{ t("common.edit") }} </el-button> <el-button :type="activeTab === 'apis' ? 'primary' : ''" size="small" @click="activeTab = 'apis'" :disabled="selectedDocument.status !== 'valid'" > <el-icon><Operation /></el-icon> {{ t("openapi.paths") }} </el-button> <el-button :type="activeTab === 'tools' ? 'primary' : ''" size="small" @click="activeTab = 'tools'" :disabled="selectedDocument.status !== 'valid'" > <el-icon><Tools /></el-icon> {{ t("openapi.tools") }} </el-button> </el-button-group> <el-divider direction="vertical" /> <el-button size="small" @click="validateSpec" :disabled="!selectedDocument || !editorContent.trim()" :loading="validating" > <el-icon><Check /></el-icon> {{ t("openapi.validateSpec") }} </el-button> <el-button size="small" type="success" @click="saveDocumentContent" :disabled=" !selectedDocument || !editorContent.trim() || !isAuthenticated " :loading="saving" > <el-icon><DocumentCopy /></el-icon> {{ t("openapi.saveDocument") }} </el-button> <el-button type="primary" size="small" @click="convertToMCP" :disabled="selectedDocument.status !== 'valid'" > <el-icon><Setting /></el-icon> {{ t("openapi.convertToMcp") }} </el-button> <el-button type="primary" size="small" @click="downloadSpec" :disabled="selectedDocument.status !== 'valid'" > <el-icon><Download /></el-icon> {{ t("common.download") }} </el-button> </div> </div> <div v-else class="empty-header"> <span> <el-icon><Document /></el-icon> {{ t("openapi.selectDocument") }} </span> </div> </template> <div class="detail-content" style="height: calc(100% - 60px); overflow: hidden" > <!-- 空状态 --> <div v-if="!selectedDocument" class="empty-state" style=" height: 100%; display: flex; align-items: center; justify-content: center; " > <el-empty :description="t('openapi.selectDocument')" :image-size="150" > <el-button type="primary" @click="showUploadDialog = true" :icon="Upload" > {{ t("openapi.uploadFile") }} </el-button> </el-empty> </div> <!-- 错误状态 --> <div v-else-if=" selectedDocument.status === 'invalid' && activeTab !== 'editor' " class="error-state" style=" height: 100%; display: flex; align-items: center; justify-content: center; " > <el-result icon="error" :title="t('openapi.parseFailed')" :sub-title=" selectedDocument.metadata?.validationErrors?.[0] || t('openapi.validationFailed') " > <template #extra> <el-button type="primary" @click="activeTab = 'editor'"> <el-icon><Edit /></el-icon> {{ t("openapi.editDocument") }} </el-button> <el-button @click="deleteDocument(selectedDocument.id)"> <el-icon><Delete /></el-icon> {{ t("openapi.deleteSpec") }} </el-button> </template> </el-result> </div> <!-- 加载状态 --> <div v-else-if="selectedDocument.status === 'pending'" class="loading-state" style=" height: 100%; display: flex; align-items: center; justify-content: center; " > <div style="text-align: center"> <el-icon size="48" class="is-loading" style="margin-bottom: 16px" > <Loading /> </el-icon> <p style="color: #606266; margin: 0"> {{ t("openapi.parsing") }} </p> </div> </div> <!-- 正常状态或编辑状态 --> <div v-else class="content-tabs" style="height: 100%"> <div v-show="activeTab === 'editor'" ref="editorContainerRef" class="editor-container" style="height: 100%; position: relative" > <!-- 验证错误提示 --> <div v-if=" selectedDocument.status === 'invalid' && validationResults && !validationResults.valid " class="validation-errors" style=" position: absolute; top: 0; left: 0; right: 0; z-index: 10; background: #fff; border-bottom: 1px solid #dcdfe6; padding: 12px; " > <el-alert :title="t('openapi.validationErrors')" type="error" :closable="false" show-icon > <template #default> <div class="error-list" style="max-height: 120px; overflow-y: auto" > <div v-for="(error, index) in validationResults.errors" :key="index" class="error-item" style="margin-bottom: 4px; font-size: 12px" > {{ error }} </div> </div> </template> </el-alert> </div> <MonacoEditor v-model="editorContent" :language="detectLanguage(editorContent)" :height=" selectedDocument.status === 'invalid' && validationResults?.valid === false ? Math.max(editorHeight - 160, 300) : editorHeight " :options="editorOptions" @change="handleContentChange" :style="{ marginTop: selectedDocument.status === 'invalid' && validationResults?.valid === false ? '160px' : '0', }" /> </div> <div v-show="activeTab === 'apis'" class="apis-container" style="height: 100%; overflow-y: auto" > <el-empty v-if="!parsedApis.length" :description="t('openapi.noParseResult')" /> <div v-else class="api-list"> <!-- API卡片列表 --> <div v-for="(api, index) in parsedApis" :key="`${api.method}-${api.path}-${index}`" class="api-card" :class="`api-card--${api.method.toLowerCase()}`" > <div class="api-card__header" @click="toggleApiDetail(index)" > <div class="api-card__method-info"> <span class="api-method-tag" :class="`method-${api.method.toLowerCase()}`" > {{ api.method.toUpperCase() }} </span> <span class="api-path">{{ api.path }}</span> </div> <div class="api-card__summary"> <span class="api-summary">{{ api.summary || api.description || t("openapi.noDescription") }}</span> <el-icon class="expand-icon" :class="{ expanded: expandedApis.includes(index), }" > <ArrowDown /> </el-icon> </div> </div> <!-- 可展开的详细信息 --> <el-collapse-transition> <div v-show="expandedApis.includes(index)" class="api-card__details" > <div class="api-detail-section"> <div v-if=" api.description && api.description !== api.summary " class="detail-item" > <label>{{ t("openapi.description") }}:</label> <span>{{ api.description }}</span> </div> <div v-if="api.operationId" class="detail-item"> <label>{{ t("openapi.operationId") }}:</label> <span>{{ api.operationId }}</span> </div> <div v-if="api.tags && api.tags.length" class="detail-item" > <label>{{ t("openapi.tags") }}:</label> <div class="tag-list"> <el-tag v-for="tag in api.tags" :key="tag" size="small" effect="plain" class="api-tag" > {{ tag }} </el-tag> </div> </div> <div v-if="api.parameters && api.parameters.length" class="detail-item" > <label>{{ t("openapi.parameters") }}:</label> <div class="parameter-list"> <div v-for="param in api.parameters" :key="param.name" class="parameter-item" > <span class="param-name">{{ param.name }}</span> <span class="param-type">{{ param.type || param.schema?.type || "unknown" }}</span> <span v-if="param.required" class="param-required" >{{ t("tester.required") }}</span > </div> </div> </div> <div v-if="api.responses" class="detail-item"> <label>{{ t("openapi.responses") }}:</label> <div class="response-list"> <div v-for="(response, code) in api.responses" :key="code" class="response-item" > <span class="response-code" :class=" getResponseCodeClass(String(code)) " >{{ code }}</span > <span class="response-desc">{{ response.description || t("openapi.noDescription") }}</span> </div> </div> </div> </div> </div> </el-collapse-transition> </div> </div> </div> <div v-show="activeTab === 'tools'" class="tools-container" style="height: 100%; overflow-y: auto" > <MCPToolPreview :tools="mcpTools" :serverUrl="mcpServerUrl" /> </div> </div> </div> </el-card> </el-col> </el-row> </div> <!-- 创建规范对话框 --> <el-dialog v-model="showCreateDialog" :title="t('openapi.createSpec')" width="600px" align-center > <el-form ref="createFormRef" :model="createForm" :rules="createFormRules" label-width="100px" > <el-form-item :label="t('openapi.specName')" prop="name"> <el-input v-model="createForm.name" :placeholder="t('openapi.enterSpecName')" clearable /> </el-form-item> <el-form-item :label="t('openapi.specVersion')" prop="version"> <el-input v-model="createForm.version" :placeholder="t('openapi.enterVersion')" clearable /> </el-form-item> <el-form-item :label="t('openapi.specDescription')" prop="description" > <el-input v-model="createForm.description" type="textarea" :rows="3" :placeholder="t('openapi.enterDescription')" /> </el-form-item> <el-form-item :label="t('openapi.template')"> <el-select v-model="createForm.template" :placeholder="t('openapi.selectTemplate')" clearable style="width: 100%" > <el-option :label="t('openapi.blankTemplate')" value="blank" /> <el-option :label="t('openapi.basicRestTemplate')" value="basic-rest" /> <el-option :label="t('openapi.ecommerceTemplate')" value="ecommerce" /> <el-option :label="t('openapi.userManagementTemplate')" value="user-management" /> </el-select> </el-form-item> </el-form> <template #footer> <el-button @click="showCreateDialog = false">{{ t("common.cancel") }}</el-button> <el-button type="primary" @click="createNewSpec" :loading="creating"> {{ t("common.create") }} </el-button> </template> </el-dialog> <!-- 上传对话框 --> <el-dialog v-model="showUploadDialog" :title="t('openapi.uploadFile')" width="600px" :before-close="handleUploadDialogClose" > <div class="upload-container"> <el-upload ref="uploadRef" class="upload-demo" drag :auto-upload="false" :on-change="handleFileChange" :on-remove="handleFileRemove" :file-list="uploadFileList" :accept="'.json,.yaml,.yml'" :limit="1" > <el-icon class="el-icon--upload" size="48" style="color: #409eff" ><UploadFilled /></el-icon> <div class="el-upload__text" style="font-size: 16px; margin-top: 16px" > {{ t("openapi.dragOrClick") }} </div> <template #tip> <div class="el-upload__tip" style="margin-top: 12px; color: #909399" > {{ t("openapi.supportedFormats") }} </div> </template> </el-upload> <el-form v-if="uploadFile" ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="100px" class="upload-form" > <el-form-item :label="t('openapi.docName')" prop="name"> <el-input v-model="uploadForm.name" :placeholder="t('openapi.enterDocName')" size="large" > <template #prefix> <el-icon><Document /></el-icon> </template> </el-input> </el-form-item> <el-form-item :label="t('openapi.docDescription')" prop="description" > <el-input v-model="uploadForm.description" type="textarea" :rows="3" :placeholder="t('openapi.enterDocDescription')" size="large" /> </el-form-item> </el-form> </div> <template #footer> <div class="dialog-footer"> <el-button @click="handleUploadDialogClose" size="large">{{ t("common.cancel") }}</el-button> <el-button type="primary" @click="confirmUpload" :disabled="!uploadFile" size="large" :loading="uploading" > {{ uploading ? t("openapi.uploading") : t("openapi.confirmUpload") }} </el-button> </div> </template> </el-dialog> <!-- URL导入对话框 --> <el-dialog v-model="showUrlDialog" :title="t('openapi.importFromUrl')" width="600px" align-center > <el-form ref="urlFormRef" :model="urlForm" :rules="urlFormRules" label-width="100px" > <el-form-item :label="t('openapi.docUrl')" prop="url"> <el-input v-model="urlForm.url" :placeholder="t('openapi.enterDocUrl')" clearable size="large" > <template #prefix> <el-icon><Link /></el-icon> </template> </el-input> <div class="form-tip"> {{ t("openapi.urlFormats") }} </div> </el-form-item> <el-form-item :label="t('openapi.docName')" prop="name"> <el-input v-model="urlForm.name" :placeholder="t('openapi.enterDocName')" clearable size="large" > <template #prefix> <el-icon><Edit /></el-icon> </template> </el-input> <div class="form-tip"> {{ t("openapi.autoNameTip") }} </div> </el-form-item> <el-form-item :label="t('auth.authType')"> <el-select v-model="urlForm.authType" :placeholder="t('auth.selectAuthType')" size="large" style="width: 100%" > <el-option :label="t('auth.noAuth')" value="none" /> <el-option :label="t('auth.bearerToken')" value="bearer" /> <el-option :label="t('auth.basicAuth')" value="basic" /> </el-select> </el-form-item> <el-form-item v-if="urlForm.authType === 'bearer'" :label="t('auth.token')" > <el-input v-model="urlForm.token" type="password" :placeholder="t('auth.enterToken')" show-password size="large" /> </el-form-item> <template v-if="urlForm.authType === 'basic'"> <el-form-item :label="t('auth.username')"> <el-input v-model="urlForm.username" :placeholder="t('auth.enterUsername')" size="large" /> </el-form-item> <el-form-item :label="t('auth.password')"> <el-input v-model="urlForm.password" type="password" :placeholder="t('auth.enterPassword')" show-password size="large" /> </el-form-item> </template> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="showUrlDialog = false" size="large">{{ t("common.cancel") }}</el-button> <el-button type="primary" @click="importFromUrl" :loading="importing" size="large" > {{ importing ? t("openapi.importing") : t("openapi.startImport") }} </el-button> </div> </template> </el-dialog> <!-- 编辑文档对话框 --> <el-dialog v-model="showEditDialog" :title="t('openapi.editDocument')" width="600px" align-center > <el-form ref="editFormRef" :model="editForm" :rules="editFormRules" label-width="100px" > <el-form-item :label="t('openapi.docName')" prop="name"> <el-input v-model="editForm.name" :placeholder="t('openapi.enterDocName')" clearable size="large" > <template #prefix> <el-icon><Document /></el-icon> </template> </el-input> </el-form-item> <el-form-item :label="t('openapi.docDescription')" prop="description"> <el-input v-model="editForm.description" type="textarea" :rows="4" :placeholder="t('openapi.enterDocDescription')" size="large" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="showEditDialog = false" size="large">{{ t("common.cancel") }}</el-button> <el-button type="primary" @click="saveEditDocument" :loading="editing" size="large" > {{ editing ? t("openapi.saving") : t("common.save") }} </el-button> </div> </template> </el-dialog> </template> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted, onUnmounted, nextTick } from "vue"; import { ElMessage, ElMessageBox } from "element-plus"; import { useI18n } from "vue-i18n"; import { Plus, Upload, Link, Search, MoreFilled, Edit, DocumentCopy, Download, Delete, Operation, Tools, Check, DocumentChecked, UploadFilled, Folder, Setting, Refresh, ArrowDown, } from "@element-plus/icons-vue"; import type { UploadFile, FormInstance } from "element-plus"; import MonacoEditor from "../../shared/components/monaco/MonacoEditor.vue"; import DocumentStatusProgress from "../../components/DocumentStatusProgress.vue"; import MCPToolPreview from "./components/openapi/MCPToolPreview.vue"; import type { OpenAPISpec, ValidationResult, MCPTool } from "../../types"; import { useOpenAPIStore } from "../../stores/openapi"; import { parseOpenAPI, validateOpenAPI, extractApiPaths, } from "../../utils/openapi"; import { documentsApi, type Document, type CreateDocumentDto, type UpdateDocumentDto, } from "../../api/documents"; // 导入全局功能 import { useConfirmation } from "../../composables/useConfirmation"; import { useFormValidation } from "../../composables/useFormValidation"; import { usePerformanceMonitor } from "../../composables/usePerformance"; // 国际化 const { t } = useI18n(); // import LoadingOverlay from '@/shared/components/ui/LoadingOverlay.vue' // 暂时注释掉,如果需要可以创建这个组件 // 状态管理 const openApiStore = useOpenAPIStore(); // 全局功能 const { confirmDelete: globalConfirmDelete, confirmDangerousAction, confirmSave, } = useConfirmation(); const { startMonitoring, stopMonitoring, measureFunction } = usePerformanceMonitor(); // 响应式数据 const specsLoading = ref(false); const loading = ref(false); const searchQuery = ref(""); const selectedDocument = ref<Document | null>(null); const activeTab = ref<"editor" | "apis" | "tools">("editor"); const editorContent = ref(""); const saving = ref(false); const validating = ref(false); const converting = ref(false); const validationResults = ref<ValidationResult | null>(null); const mcpTools = ref<any[]>([]); // MCP工具列表 const mcpServerUrl = ref(""); const documents = ref<Document[]>([]); // 文档列表 const parsedApis = ref<any[]>([]); // 解析的API列表 const uploadFile = ref<File | null>(null); const editorContainerRef = ref<HTMLElement>(); // 编辑器容器引用 const expandedApis = ref<number[]>([]); // 展开的API卡片索引列表 const isAuthenticated = ref(false); // 用户认证状态 const authChecking = ref(true); // 认证检查状态 // 对话框状态 const showCreateDialog = ref(false); const showUploadDialog = ref(false); const showUrlDialog = ref(false); const showEditDialog = ref(false); const creating = ref(false); const uploading = ref(false); const importing = ref(false); const editing = ref(false); // 表单引用 const createFormRef = ref<FormInstance>(); const urlFormRef = ref<FormInstance>(); const uploadRef = ref(); const editFormRef = ref<FormInstance>(); // 上传文件列表 const uploadFileList = ref<UploadFile[]>([]); // 表单数据 const createForm = ref({ name: "", version: "1.0.0", description: "", template: "", }); const uploadForm = ref({ name: "", description: "", }); const urlForm = ref({ url: "", name: "", authType: "none", token: "", username: "", password: "", }); const editForm = ref({ id: "", name: "", description: "", }); // 表单验证规则 const createFormRules = { name: [ { required: true, message: t("openapi.validation.specNameRequired"), trigger: "blur", }, { min: 2, max: 50, message: t("openapi.validation.nameLength"), trigger: "blur", }, ], version: [ { required: true, message: t("openapi.validation.versionRequired"), trigger: "blur", }, ], }; const uploadRules = { name: [ { required: true, message: t("openapi.validation.docNameRequired"), trigger: "blur", }, { min: 2, max: 50, message: t("openapi.validation.nameLength"), trigger: "blur", }, ], }; const urlFormRules = { url: [ { required: true, message: t("openapi.validation.urlRequired"), trigger: "blur", }, { type: "url", message: t("openapi.validation.urlInvalid"), trigger: "blur", }, ], name: [ { required: true, message: t("openapi.validation.specNameRequired"), trigger: "blur", }, ], }; const editFormRules = { name: [ { required: true, message: t("openapi.validation.docNameRequired"), trigger: "blur", }, { min: 2, max: 50, message: t("openapi.validation.nameLength"), trigger: "blur", }, ], }; // Monaco编辑器选项 const editorOptions: any = { theme: "vs-dark", fontSize: 14, minimap: { enabled: false }, scrollBeyondLastLine: false, automaticLayout: true, tabSize: 2, wordWrap: "on" as const, // 支持大型文件 largeFileOptimizations: false, // 大幅增加内容长度限制 - 提升到更大的值 maxTokenizationLineLength: 500000, // 提高渲染性能 renderValidationDecorations: "on", // 支持更多行数渲染 - 大幅提升行数限制 stopRenderingLineAfter: 200000, // 禁用语法检查以提高性能 validate: false, // 增加滚动性能 smoothScrolling: true, // 禁用不必要的功能以提高性能 folding: false, lineNumbers: "on", // 增加更多大文件支持配置 scrollbar: { vertical: "auto", horizontal: "auto", handleMouseWheel: true, }, // 禁用代码折叠以提高性能 glyphMargin: false, // 优化大文件渲染 renderLineHighlight: "none", }; // 计算属性 const filteredDocuments = computed(() => { if (!searchQuery.value) return documents.value; const query = searchQuery.value.toLowerCase(); return documents.value.filter( (doc) => doc.name.toLowerCase().includes(query) || doc.description?.toLowerCase().includes(query), ); }); // 计算编辑器高度 const editorHeight = computed(() => { // 如果容器引用存在,动态计算可用高度 if (editorContainerRef.value) { const containerHeight = editorContainerRef.value.clientHeight; // 减去一些边距和其他元素的高度 return Math.max(containerHeight - 20, 300); // 最小高度300px } // 默认使用视窗高度的计算值 return Math.max(window.innerHeight - 300, 400); }); // 方法 // 检查用户认证状态 const checkAuthentication = async () => { try { authChecking.value = true; console.log("Starting authentication check..."); isAuthenticated.value = await documentsApi.checkAuth(); console.log("Authentication result:", isAuthenticated.value); if (isAuthenticated.value) { console.log("User authenticated, loading documents..."); await loadDocuments(); } else { console.log("User not authenticated, clearing document list"); documents.value = []; } } catch (error) { console.error("Authentication check failed:", error); isAuthenticated.value = false; documents.value = []; } finally { authChecking.value = false; console.log("Authentication check completed"); } }; // 加载文档列表 const loadDocuments = async () => { try { loading.value = true; console.log("Loading documents..."); const docs = await documentsApi.getDocuments(); console.log(docs); documents.value = docs; //console.log(`Successfully loaded ${docs.length} documents:`, docs.map(d => ({ id: d.id, name: d.name }))) } catch (error: any) { console.error("Failed to load documents:", error); console.error("Error details:", { status: error.response?.status, statusText: error.response?.statusText, message: error.message, url: error.config?.url, }); ElMessage.error(t("openapi.loadDocumentsFailed")); documents.value = []; } finally { loading.value = false; console.log("Document loading completed"); } }; const detectLanguage = (content: string) => { if (!content.trim()) return "yaml"; try { JSON.parse(content); return "json"; } catch { return "yaml"; } }; const formatDate = (date: Date | string) => { const dateObj = typeof date === "string" ? new Date(date) : date; return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }).format(dateObj); }; const selectDocument = async (doc: Document) => { if (!isAuthenticated.value) { ElMessage.error(t("openapi.authRequired")); return; } // 设置加载状态 loading.value = true; selectedDocument.value = doc; editorContent.value = ""; validationResults.value = null; activeTab.value = "editor"; // 清空之前的解析数据 parsedApis.value = []; mcpTools.value = []; mcpServerUrl.value = ""; try { // 调用API获取完整的文档内容 const fullDocument = await documentsApi.getDocument(doc.id); // 更新选中的文档和编辑器内容 selectedDocument.value = fullDocument; editorContent.value = fullDocument.content || ""; // 自动解析OpenAPI内容并填充数据 if (fullDocument.content && fullDocument.content.trim()) { try { // 解析API路径和详细信息 const { data: parsedData } = parseOpenAPI(fullDocument.content); if (parsedData && parsedData.paths) { const apis: any[] = []; Object.entries(parsedData.paths).forEach( ([path, pathItem]: [string, any]) => { if (!pathItem || typeof pathItem !== "object") return; const methods = [ "get", "post", "put", "delete", "patch", "head", "options", ]; methods.forEach((method) => { if (pathItem[method]) { const operation = pathItem[method]; apis.push({ id: apis.length, method: method.toUpperCase(), path, summary: operation.summary || "", description: operation.description || "", operationId: operation.operationId, tags: operation.tags || [], parameters: operation.parameters || [], responses: operation.responses || {}, }); } }); }, ); parsedApis.value = apis; } else { parsedApis.value = []; } // 解析MCP工具 try { const parseResult = await openApiStore.parseOpenAPIContent( fullDocument.content, ); mcpTools.value = parseResult.tools || []; mcpServerUrl.value = parseResult.servers[0]?.url || ""; } catch (mcpError) { console.warn("解析MCP工具失败:", mcpError); mcpTools.value = []; mcpServerUrl.value = ""; } // 保持在编辑标签页,不自动切换 // if (parsedApis.value.length > 0) { // activeTab.value = 'apis' // } } catch (parseError) { console.error("解析OpenAPI内容失败:", parseError); ElMessage.warning(t("openapi.parseContentFailed")); parsedApis.value = []; mcpTools.value = []; mcpServerUrl.value = ""; } } } catch (error) { console.error("获取文档内容失败:", error); ElMessage.error( t("openapi.fetchDocumentFailed", { error: error instanceof Error ? error.message : t("common.unknownError"), }), ); // 重置状态 selectedDocument.value = null; editorContent.value = ""; parsedApis.value = []; mcpTools.value = []; mcpServerUrl.value = ""; } finally { loading.value = false; } }; const editDocument = (doc: Document) => { // 填充编辑表单数据 editForm.value = { id: doc.id, name: doc.name, description: doc.description || "", }; showEditDialog.value = true; }; const deleteDocument = async (docId: string) => { if (!isAuthenticated.value) { ElMessage.error(t("openapi.authRequired")); return; } try { // 查找要删除的文档 const docToDelete = documents.value.find((doc) => doc.id === docId); if (!docToDelete) { ElMessage.error(t("openapi.documentNotFound")); return; } // 显示确认对话框,包含文档名称 const confirmed = await ElMessageBox.confirm( t("openapi.confirmDeleteDocument", { name: docToDelete.name }), t("openapi.confirmDelete"), { confirmButtonText: t("common.confirm"), cancelButtonText: t("common.cancel"), type: "warning", center: true, }, ) .then(() => true) .catch(() => false); if (!confirmed) return; // 调用API删除文档 await documentsApi.deleteDocument(docId); // 从本地文档列表中删除 documents.value = documents.value.filter((doc) => doc.id !== docId); // 如果删除的是当前选中的文档,清空相关状态 if (selectedDocument.value?.id === docId) { selectedDocument.value = null; editorContent.value = ""; validationResults.value = null; parsedApis.value = []; expandedApis.value = []; activeTab.value = "editor"; } ElMessage.success(t("openapi.deleteSuccess", { name: docToDelete.name })); } catch (error) { console.error("删除文档失败:", error); ElMessage.error( t("openapi.deleteFailed", { error: error instanceof Error ? error.message : t("common.unknownError"), }), ); } }; const saveEditDocument = async () => { if (!editFormRef.value || !isAuthenticated.value) { if (!isAuthenticated.value) { ElMessage.error(t("openapi.authRequired")); } return; } try { const valid = await editFormRef.value.validate(); if (!valid) return; editing.value = true; // 准备更新数据 const updateData: UpdateDocumentDto = { name: editForm.value.name, description: editForm.value.description, }; // 调用API更新文档 const updatedDoc = await documentsApi.updateDocument( editForm.value.id, updateData, ); // 更新本地文档列表 const docIndex = documents.value.findIndex( (doc) => doc.id === editForm.value.id, ); if (docIndex !== -1) { documents.value[docIndex] = updatedDoc; } // 如果当前选中的是被编辑的文档,也要更新选中文档的信息 if (selectedDocument.value?.id === editForm.value.id) { selectedDocument.value = updatedDoc; } showEditDialog.value = false; ElMessage.success(t("openapi.updateSuccess")); } catch (error) { console.error("更新文档失败:", error); ElMessage.error( t("openapi.saveFailed", { error: error instanceof Error ? error.message : t("common.unknownError"), }), ); } finally { editing.value = false; } }; const refreshDocuments = async () => { if (!isAuthenticated.value) { ElMessage.error(t("openapi.authRequired")); return; } try { await loadDocuments(); ElMessage.success(t("openapi.refreshSuccess")); } catch (error) { console.error("刷新文档失败:", error); ElMessage.error( t("openapi.refreshFailed", { error: error instanceof Error ? error.message : t("common.unknownError"), }), ); } }; const getStatusText = (status: string) => { const statusMap: Record<string, string> = { valid: t("openapi.status.valid"), invalid: t("openapi.status.invalid"), pending: t("openapi.status.pending"), }; return statusMap[status] || t("openapi.status.unknown"); }; const handleSpecAction = async (command: { action: string; spec: OpenAPISpec; }) => { const { action, spec } = command; switch (action) { case "edit": // 选择文档进行编辑 const doc = documents.value.find((d) => d.id === spec.id); if (doc) { selectDocument(doc); activeTab.value = "editor"; } break; case "duplicate": try { await openApiStore.duplicateSpec(spec.id); ElMessage.success(t("openapi.duplicateSuccess")); } catch (error) { ElMessage.error(t("openapi.duplicateFailed", { error })); } break; case "download": downloadSpec(spec); break; case "delete": try { const confirmed = await ElMessageBox.confirm( t("openapi.confirmDeleteSpec", { name: spec.name }), t("openapi.confirmDelete"), { confirmButtonText: t("common.confirm"), cancelButtonText: t("common.cancel"), type: "warning", center: true, }, ) .then(() => true) .catch(() => false); if (!confirmed) break; await measureFunction("deleteSpec", async () => { await openApiStore.deleteSpec(spec.id); }); // 如果删除的是当前选中的文档,清空选择 if (selectedDocument.value?.id === spec.id) { selectedDocument.value = null; editorContent.value = ""; } // 从文档列表中移除 documents.value = documents.value.filter((doc) => doc.id !== spec.id); ElMessage.success(t("openapi.deleteSpecSuccess")); } catch (error) { ElMessage.error(t("openapi.deleteFailed", { error })); } break; } }; const downloadSpec = (spec?: OpenAPISpec) => { const content = spec?.content || editorContent.value; if (!content) { ElMessage.warning(t("openapi.enterContentFirst")); return; } const blob = new Blob([content], { type: "application/yaml", }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = spec ? `${spec.name}-${spec.version}.yaml` : `openapi-spec-${new Date().getTime()}.yaml`; link.click(); URL.revokeObjectURL(url); ElMessage.success(t("openapi.downloadSuccess")); }; const handleContentChange = (content: string) => { editorContent.value = content; if (selectedDocument.value) { selectedDocument.value.content = content; } validationResults.value = null; }; // 保存文档内容 const saveDocumentContent = async () => { if (!selectedDocument.value || !isAuthenticated.value) { if (!isAuthenticated.value) { ElMessage.error(t("openapi.authRequired")); } return; } try { saving.value = true; // 准备更新数据 const updateData: UpdateDocumentDto = { content: editorContent.value, }; // 调用API更新文档 const updatedDoc = await documentsApi.updateDocument( selectedDocument.value.id, updateData, ); // 更新本地文档列表 const docIndex = documents.value.findIndex( (doc) => doc.id === selectedDocument.value!.id, ); if (docIndex !== -1) { documents.value[docIndex] = updatedDoc; } // 更新选中的文档 selectedDocument.value = updatedDoc; ElMessage.success(t("openapi.saveSuccess")); } catch (error: any) { console.error("保存文档内容失败:", error); // 处理400错误,显示更具体的错误信息 if (error.response && error.response.status === 400) { const errorData = error.response.data; let errorMessage = ""; if (errorData && errorData.message) { // 检查是否是OpenAPI规范验证错误 if ( errorData.message.includes("OpenAPI") || errorData.message.includes("Swagger") ) { errorMessage = `OpenAPI规范验证失败: ${errorData.message}`; } else if (errorData.message.includes("JSON")) { errorMessage = `JSON格式错误: ${errorData.message}`; } else { errorMessage = `请求参数错误: ${errorData.message}`; } } else { errorMessage = "文档内容格式不正确,请检查OpenAPI规范格式"; } ElMessage.error(errorMessage); } else { // 其他错误使用原有逻辑 ElMessage.error( t("openapi.saveFailed", { error: error instanceof Error ? error.message : t("common.unknownError"), }), ); } } finally { saving.value = false; } }; const validateSpec = async () => { if (!editorContent.value.trim()) { ElMessage.warning(t("openapi.enterContentFirst")); return; } validating.value = true; try { const result = await measureFunction("validateSpec", async () => { return await openApiStore.validateSpec(editorContent.value); }); // 更新验证结果 validationResults.value = result; // 更新文档状态 if (selectedDocument.value) { const newStatus = result.valid ? "valid" : "invalid"; selectedDocument.value.status = newStatus; // 调用后端API更新文档状态 try { await documentsApi.updateDocument(selectedDocument.value.id, { status: newStatus as any, }); } catch (statusUpdateError) { console.error("更新文档状态失败:", statusUpdateError); // 状态更新失败不影响验证流程,只记录错误 } } // 如果验证成功,解析API路径并更新parsedApis if (result.valid) { try { // 使用 extractApiPaths 函数从内容中提取API路径 const apiPaths = extractApiPaths(editorContent.value); parsedApis.value = apiPaths.map((api, index) => ({ id: index, method: api.method.toUpperCase(), path: api.path, summary: api.summary || "", description: api.description || "", })); // 自动切换到APIs标签页显示API列表 activeTab.value = "apis"; } catch (parseError) { console.error("解析API路径失败:", parseError); parsedApis.value = []; } } else { // 验证失败时清空API列表 parsedApis.value = []; } // 显示验证结果消息 if (result.valid) { const warningCount = result.warnings?.length || 0; const apiCount = parsedApis.value.length; if (warningCount > 0) { ElMessage.success( t("openapi.validationSuccessWithWarnings", { apiCount, warningCount, }), ); } else { ElMessage.success(t("openapi.validationSuccessDetail", { apiCount })); } } else { const errorCount = result.errors?.length || 0; const warningCount = result.warnings?.length || 0; if (warningCount > 0) { ElMessage.error( t("openapi.validationFailedWithWarnings", { errorCount, warningCount, }), ); } else { ElMessage.error(t("openapi.validationFailedDetail", { errorCount })); } // 如果有验证错误,切换到编辑器标签页显示错误信息 activeTab.value = "editor"; } } catch (error) { console.error("验证失败:", error); ElMessage.error( t("openapi.validationError", { error: error instanceof Error ? error.message : String(error), }), ); validationResults.value = null; parsedApis.value = []; if (selectedDocument.value) { selectedDocument.value.status = "invalid"; // 调用后端API更新文档状态为invalid try { await documentsApi.updateDocument(selectedDocument.value.id, { status: "invalid" as any, }); } catch (statusUpdateError) { console.error("更新文档状态失败:", statusUpdateError); // 状态更新失败不影响验证流程,只记录错误 } } } finally { validating.value = false; } }; const createNewSpec = async () => { if (!createFormRef.value || !isAuthenticated.value) { if (!isAuthenticated.value) { ElMessage.error(t("openapi.authRequired")); } return; } try { await createFormRef.value.validate(); creating.value = true; // 准备创建数据 const createData: CreateDocumentDto = { name: createForm.value.name, description: createForm.value.description, content: generateTemplateContent(createForm.value.template), }; // 调用API创建文档 const newDoc = await documentsApi.createDocument(createData); // 添加到本地文档列表 documents.value.push(newDoc); selectDocument(newDoc); showCreateDialog.value = false; // 重置表单 createForm.value = { name: "", version: "1.0.0", description: "", template: "", }; ElMessage.success(t("openapi.createSuccess")); } catch (error) { console.error("创建文档失败:", error); ElMessage.error( t("openapi.createFailed", { error: error instanceof Error ? error.message : t("common.unknownError"), }), ); } finally { creating.value = false; } }; const generateTemplateContent = (template: string) => { const templates: Record<string, string> = { blank: `openapi: 3.0.0 info: title: ${createForm.value.name} version: ${createForm.value.version} description: ${createForm.value.description} paths: {} `, "basic-rest": `openapi: 3.0.0 info: title: ${createForm.value.name} version: ${createForm.value.version} description: ${createForm.value.description} paths: /users: get: summary: ${t("openapi.templates.getUserList")} responses: '200': description: ${t("openapi.templates.success")} `, ecommerce: `openapi: 3.0.0 info: title: ${createForm.value.name} version: ${createForm.value.version} description: ${createForm.value.description} paths: /products: get: summary: ${t("openapi.templates.getProductList")} responses: '200': description: ${t("openapi.templates.success")} `, "user-management": `openapi: 3.0.0 info: title: ${createForm.value.name} version: ${createForm.value.version} description: ${createForm.value.description} paths: /auth/login: post: summary: ${t("openapi.templates.userLogin")} responses: '200': description: ${t("openapi.templates.loginSuccess")} `, }; return templates[template] || templates["blank"]; }; const handleFileChange = (file: UploadFile, fileList: UploadFile[]) => { uploadFile.value = file.raw || null; uploadFileList.value = fileList; if (uploadFile.value) { console.log("uploadFile.value", uploadFile.value); uploadForm.value.name = uploadFile.value.name.replace(/\.[^/.]+$/, ""); } }; const handleFileRemove = (file: UploadFile, fileList: UploadFile[]) => { uploadFile.value = null; uploadFileList.value = fileList; uploadForm.value = { name: "", description: "" }; }; const handleUploadDialogClose = () => { showUploadDialog.value = false; uploadFile.value = null; uploadForm.value = { name: "", description: "" }; uploadFileList.value = []; // 清空上传组件的文件列表 if (uploadRef.value) { uploadRef.value.clearFiles(); } }; const confirmUpload = async () => { if (!uploadFile.value || !isAuthenticated.value) { if (!isAuthenticated.value) { ElMessage.error(t("openapi.authRequired")); } return; } uploading.value = true; try { // 读取文件内容 const rawContent = await readFileContent(uploadFile.value); // 准备创建数据 const createData: CreateDocumentDto = { name: uploadForm.value.name, description: uploadForm.value.description, content: rawContent, }; // 调用API创建文档 const newDoc = await documentsApi.createDocument(createData); // 添加到本地文档列表 documents.value.push(newDoc); selectDocument(newDoc); // 确保对话框关闭 handleUploadDialogClose(); ElMessage.success(t("openapi.uploadSuccessValidate")); } catch (error) { console.error("上传文档失败:", error); ElMessage.error( t("openapi.uploadFailed", { error: error instanceof Error ? error.message : String(error), }), ); } finally { uploading.value = false; } }; // 工具函数 // measureFunction 已在 usePerformanceMonitor() 中定义 // globalConfirmDelete 已在 useConfirmation() 中定义 const readFileContent = (file: File): Promise<string> => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { resolve(e.target?.result as string); }; reader.onerror = () => { reject(new Error(t("openapi.fileReadFailed"))); }; reader.readAsText(file); }); }; // MCP相关方法 const convertToMCP = async () => { if (!editorContent.value) { ElMessage.warning(t("openapi.selectDocumentFirst")); return; } converting.value = true; try { // 首先验证当前规范 const validation = await openApiStore.validateSpec(editorContent.value); if (!validation.valid) { ElMessage.error(t("openapi.fixErrorsFirst")); return; } // 解析内容并获取工具 const parseResult = await openApiStore.parseOpenAPIContent( editorContent.value, ); mcpTools.value = parseResult.tools || []; mcpServerUrl.value = parseResult.servers[0]?.url || ""; activeTab.value = "tools"; ElMessage.success( t("openapi.convertSuccess", { count: mcpTools.value.length }), ); } catch (error) { ElMessage.error( t("openapi.convertFailed", { error: error instanceof Error ? error.message : error, }), ); } finally { converting.value = false; } }; const importFromUrl = async () => { if (!urlFormRef.value || !isAuthenticated.value) { if (!isAuthenticated.value) { ElMessage.error(t("openapi.authRequired")); } return; } try { await urlFormRef.value.validate(); importing.value = true; // 获取URL内容 const rawContentResponse = await fetch(urlForm.value.url); if (!rawContentResponse.ok) { throw new Error( `HTTP ${rawContentResponse.status}: ${rawContentResponse.statusText}`, ); } let rawContent = await rawContentResponse.text(); // 尝试格式化JSON内容 try { const jsonContent = JSON.parse(rawContent); rawContent = JSON.stringify(jsonContent, null, 2); } catch (e) { // 如果不是JSON格式,保持原样(可能是YAML) console.log("Content is not JSON, keeping original format"); } // 准备创建数据 const createData: CreateDocumentDto = { name: urlForm.value.name || "imported_spec", description: t("openapi.importedFromUrl"), content: rawContent, }; // 调用API创建文档 const newDoc = await documentsApi.createDocument(createData); // 添加到本地文档列表 documents.value.push(newDoc); selectDocument(newDoc); // 重置表单 urlForm.value = { url: "", name: "", authType: "none", token: "", username: "", password: "", }; // 重置表单验证状态 if (urlFormRef.value) { urlFormRef.value.resetFields(); urlFormRef.value.clearValidate(); } // 关闭对话框 showUrlDialog.value = false; ElMessage.success(t("openapi.importSuccessValidate")); } catch (error) { console.error("从URL导入文档失败:", error); ElMessage.error( t("openapi.importFailed", { error: error instanceof Error ? error.message : String(error), }), ); } finally { importing.value = false; } }; const handleTestTool = async (tool: MCPTool, params: Record<string, any>) => { try { ElMessage.info(t("openapi.testingTool", { name: tool.name })); // 这里可以集成实际的工具测试逻辑 console.log("Testing tool:", tool, "with params:", params); } catch (error) { ElMessage.error( t("openapi.testToolFailed", { error: error instanceof Error ? error.message : error, }), ); } }; // 接口相关方法 const getMethodTagType = (method: string) => { const methodMap: Record<string, string> = { get: "success", post: "primary", put: "warning", delete: "danger", patch: "info", head: "", options: "", }; return methodMap[method.toLowerCase()] || ""; }; const viewApiDetail = (api: any) => { ElMessageBox.alert( `<div> <p><strong>${t("openapi.path")}:</strong> ${api.path}</p> <p><strong>${t("openapi.method")}:</strong> ${api.method.toUpperCase()}</p> <p><strong>${t("openapi.summary")}:</strong> ${api.summary || t("openapi.none")}</p> <p><strong>${t("openapi.description")}:</strong> ${api.description || t("openapi.none")}</p> <p><strong>${t("openapi.operationId")}:</strong> ${api.operationId || t("openapi.none")}</p> <p><strong>${t("openapi.tags")}:</strong> ${api.tags?.join(", ") || t("openapi.none")}</p> </div>`, t("openapi.apiDetail"), { dangerouslyUseHTMLString: true, confirmButtonText: t("common.confirm"), }, ); }; // API卡片展开/收起处理 const toggleApiDetail = (index: number) => { const expandedIndex = expandedApis.value.indexOf(index); if (expandedIndex > -1) { expandedApis.value.splice(expandedIndex, 1); } else { expandedApis.value.push(index); } }; // 响应状态码样式类 const getResponseCodeClass = (code: string) => { const codeNum = parseInt(code); if (codeNum >= 200 && codeNum < 300) { return "response-success"; } else if (codeNum >= 300 && codeNum < 400) { return "response-redirect"; } else if (codeNum >= 400 && codeNum < 500) { return "response-client-error"; } else if (codeNum >= 500) { return "response-server-error"; } return "response-default"; }; // 窗口大小变化处理 const handleResize = () => { // 触发计算属性重新计算 if (editorContainerRef.value) { nextTick(() => { // 强制重新计算高度 editorHeight.value; }); } }; // 生命周期 onMounted(async () => { // 检查用户认证状态并加载文档 await checkAuthentication(); // 添加窗口大小变化监听器 window.addEventListener("resize", handleResize); }); onUnmounted(() => { // 清理窗口大小变化监听器 window.removeEventListener("resize", handleResize); }); </script> <style scoped> .openapi-manager { height: 100vh; background: linear-gradient( 180deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100% ); display: flex; flex-direction: column; overflow: hidden; } /* 顶部工具栏样式 */ /* 页面头部样式 */ .header-section { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--el-border-color-light); } .header-content h1 { margin: 0 0 8px 0; color: var(--el-text-color-primary); display: flex; align-items: center; gap: 8px; font-size: 24px; font-weight: 600; } .header-description { margin: 0; color: var(--el-text-color-regular); font-size: 14px; } .header-actions { display: flex; gap: 12px; } /* 主要内容区域 */ .manager-content { flex: 1; padding: 24px 32px; overflow: hidden; } /* 左侧文档列表样式 */ .document-list-card { border: 1px solid var(--border-color); border-radius: var(--radius-large); box-shadow: var(--shadow-light); backdrop-filter: blur(20px) saturate(180%); background: var(--bg-primary); overflow: hidden; } .list-header { display: flex; justify-content: space-between; align-items: center; font-weight: 600; color: var(--text-primary); background: var(--bg-secondary); padding: var(--spacing-lg) var(--spacing-lg); border-bottom: 1px solid var(--border-color); } .list-header span { display: flex; align-items: center; gap: 8px; font-size: 16px; } .search-container { margin: 16px; margin-bottom: 16px; } .search-container .el-input { border-radius: var(--radius-medium); } .document-list { height: calc(100vh - 200px); overflow-y: auto; padding: 0 16px 16px; } .document-items { display: flex; flex-direction: column; gap: 12px; } .document-item { padding: var(--spacing-lg); border: 2px solid transparent; border-radius: var(--radius-medium); background: var(--bg-primary); cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: flex; justify-content: space-between; align-items: flex-start; position: relative; overflow: hidden; min-height: 120px; } .document-item::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 135deg, rgba(0, 122, 255, 0.1) 0%, rgba(0, 81, 208, 0.1) 100% ); opacity: 0; transition: opacity 0.3s ease; } .document-item:hover { border-color: var(--apple-blue); transform: translateY(-2px); box-shadow: var(--shadow-medium); } .document-item:hover::before { opacity: 1; } .document-item.active { border-color: var(--apple-blue); background: var(--bg-quaternary); box-shadow: var(--shadow-light); transform: translateY(-1px); } .document-item.active::before { opacity: 1; } .document-info { flex: 1; position: relative; z-index: 1; } .document-name { font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-sm); font-size: 15px; line-height: 1.4; } .document-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 8px; } .document-meta { display: flex; flex-direction: column; gap: 8px; } .upload-time { font-size: 12px; color: var(--text-secondary); background: var(--bg-quaternary); padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius-small); white-space: nowrap; } .document-actions { position: relative; z-index: 1; opacity: 1; width: 80px; height: 100%; display: flex; align-items: center; justify-content: center; } .action-buttons { margin-top: 15px; display: flex; flex-direction: column; gap: 6px; align-items: center; justify-content: center; } .action-buttons .el-button { width: 32px; height: 28px; padding: 0; margin: 0px; display: flex; align-items: center; justify-content: center; } /* 右侧详情区域样式 */ .detail-card { border: 1px solid var(--border-color); border-radius: var(--radius-large); box-shadow: var(--shadow-light); backdrop-filter: blur(20px) saturate(180%); background: var(--bg-primary); overflow: hidden; } .detail-header { display: flex; justify-content: space-between; align-items: center; font-weight: 600; color: var(--text-primary); background: var(--bg-secondary); padding: var(--spacing-lg) var(--spacing-lg); border-bottom: 1px solid var(--border-color); } .detail-header span { display: flex; align-items: center; gap: 8px; font-size: 16px; } .empty-header { color: var(--text-secondary); font-weight: 500; display: flex; align-items: center; gap: var(--spacing-sm); } .detail-controls { display: flex; align-items: center; gap: 8px; } .detail-controls .el-button { border-radius: var(--radius-small); font-weight: 500; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .detail-controls .el-button:hover { transform: translateY(-1px); } .detail-content { height: calc(100vh - 200px); overflow: hidden; } .empty-state { height: 100%; display: flex; align-items: center; justify-content: center; background: var(--bg-secondary); border-radius: var(--radius-medium); margin: var(--spacing-lg); } .error-state { background: var(--bg-error); border: 1px solid var(--border-error); border-radius: var(--radius-medium); margin: var(--spacing-lg); } .loading-state { background: var(--bg-quaternary); border-radius: var(--radius-medium); margin: var(--spacing-lg); } .content-tabs { height: 100%; } .editor-container { height: 100%; border: 1px solid var(--border-color); border-radius: var(--radius-medium); overflow: hidden; box-shadow: var(--shadow-light); } .apis-container, .tools-container { height: 100%; padding: var(--spacing-lg); background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-medium); overflow-y: auto; box-shadow: var(--shadow-light); } /* 上传对话框样式 */ .upload-container { padding: 20px 0; } .upload-demo { margin-bottom: 20px; } .upload-demo .el-upload-dragger { border: 2px dashed var(--border-color); border-radius: var(--radius-large); background: var(--bg-secondary); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; } .upload-demo .el-upload-dragger::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 135deg, rgba(0, 122, 255, 0.1) 0%, rgba(0, 81, 208, 0.1) 100% ); opacity: 0; transition: opacity 0.3s ease; } .upload-demo .el-upload-dragger:hover { border-color: var(--apple-blue); background: var(--bg-quaternary); transform: translateY(-2px); box-shadow: var(--shadow-medium); } .upload-demo .el-upload-dragger:hover::before { opacity: 1; } .upload-form { margin-top: var(--spacing-lg); padding-top: var(--spacing-lg); border-top: 1px solid var(--border-color); } /* 对话框样式 */ .el-dialog { border-radius: var(--radius-xl); overflow: hidden; box-shadow: var(--shadow-heavy); backdrop-filter: blur(20px) saturate(180%); } .el-dialog__header { background: linear-gradient( 135deg, var(--apple-blue) 0%, var(--apple-blue-dark) 100% ); color: white; padding: var(--spacing-lg) var(--spacing-xl); } .el-dialog__title { color: white; font-weight: 600; font-size: 18px; } .el-dialog__body { padding: var(--spacing-xl); background: var(--bg-primary); } .dialog-footer { display: flex; justify-content: flex-end; gap: var(--spacing-md); padding: 0 var(--spacing-xl) var(--spacing-xl); background: var(--bg-primary); } /* 表单样式增强 */ .el-form-item { margin-bottom: var(--spacing-lg); } .el-input__wrapper { border-radius: var(--radius-medium); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: var(--shadow-light); } .el-input__wrapper:hover { box-shadow: var(--shadow-medium); transform: translateY(-1px); } .el-input__wrapper.is-focus { box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2); } .el-select { width: 100%; } .el-select .el-input__wrapper { border-radius: 12px; } .el-textarea__inner { border-radius: 12px; transition: all 0.3s ease; } .form-tip { font-size: 12px; color: #909399; margin-top: 6px; line-height: 1.4; background: rgba(144, 147, 153, 0.1); padding: 6px 10px; border-radius: 6px; } /* 按钮样式增强 */ .el-button { border-radius: 10px; font-weight: 500; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; } .el-button::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.1) 100% ); opacity: 0; transition: opacity 0.3s ease; } .el-button:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } .el-button:hover::before { opacity: 1; } .el-button--primary { background: linear-gradient(135deg, #409eff 0%, #36a3f7 100%); border: none; } .el-button--primary:hover { background: linear-gradient(135deg, #36a3f7 0%, #2b85e4 100%); } .el-button.active { background: linear-gradient(135deg, #409eff 0%, #36a3f7 100%); color: white; border: none; box-shadow: 0 4px 16px rgba(64, 158, 255, 0.3); } /* 标签样式 */ .el-tag { border-radius: 8px; font-weight: 500; backdrop-filter: blur(10px); border: none; } .el-tag--success { background: linear-gradient( 135deg, rgba(103, 194, 58, 0.2) 0%, rgba(139, 195, 74, 0.2) 100% ); color: #67c23a; } .el-tag--danger { background: linear-gradient( 135deg, rgba(245, 108, 108, 0.2) 0%, rgba(244, 67, 54, 0.2) 100% ); color: #f56c6c; } .el-tag--warning { background: linear-gradient( 135deg, rgba(230, 162, 60, 0.2) 0%, rgba(255, 193, 7, 0.2) 100% ); color: #e6a23c; } /* API表格样式 */ .el-table { border-radius: 12px; overflow: hidden; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05); } .el-table th { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); font-weight: 600; } /* 响应式设计 */ @media (max-width: 1200px) { .manager-header { padding: 16px 24px; } .manager-content { padding: 16px 24px; } .page-title { font-size: 24px; } .el-col:first-child { margin-bottom: 20px; } } @media (max-width: 768px) { .manager-header { flex-direction: column; gap: 16px; align-items: flex-start; } .header-actions { width: 100%; justify-content: flex-end; } .el-col { margin-bottom: 16px; } } /* 滚动条样式 */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: linear-gradient(135deg, #f1f1f1 0%, #e0e0e0 100%); border-radius: 4px; } ::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #c1c1c1 0%, #a8a8a8 100%); border-radius: 4px; transition: all 0.3s ease; } ::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #a8a8a8 0%, #909090 100%); } /* 动画效果 */ @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes slideInRight { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } .document-item { animation: fadeInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1); } .detail-card { animation: slideInRight 0.4s cubic-bezier(0.4, 0, 0.2, 1); } /* 卡片阴影效果 */ .document-list-card, .detail-card { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .document-list-card:hover, .detail-card:hover { box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15); transform: translateY(-2px); } /* API列表样式 */ .api-list { padding: 24px; } .api-summary { margin-bottom: 24px; } .api-summary .el-alert { border-radius: 12px; border: none; background: linear-gradient( 135deg, rgba(103, 194, 58, 0.1) 0%, rgba(139, 195, 74, 0.1) 100% ); } .error-item { margin-bottom: 12px; padding: 12px 16px; background: linear-gradient(135deg, #fef0f0 0%, #fde2e2 100%); border-radius: 8px; border-left: 4px solid #f56c6c; font-size: 14px; box-shadow: 0 2px 8px rgba(245, 108, 108, 0.1); } .validation-results { margin-top: 20px; } .validation-results .el-alert { border-radius: 12px; border: none; } /* 文件列表样式 */ .file-list { max-height: 200px; overflow-y: auto; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-radius: 12px; padding: 16px; margin-top: 16px; } .file-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: white; border-radius: 8px; margin-bottom: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); transition: all 0.3s ease; } .file-item:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); } .file-name { flex: 1; font-weight: 500; color: #303133; } .file-size { color: #909399; font-size: 12px; background: rgba(144, 147, 153, 0.1); padding: 4px 8px; border-radius: 6px; } /* API卡片样式 - Swagger UI风格 */ .api-list { padding: 16px; background: var(--bg-secondary); } .api-card { margin-bottom: 12px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: all 0.2s ease; } .api-card:hover { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } .api-card__header { padding: 12px 16px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid transparent; transition: all 0.2s ease; } .api-card__header:hover { background: var(--bg-hover); } .api-card__method-info { display: flex; align-items: center; gap: 12px; flex: 1; } .api-method-tag { display: inline-block; padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 600; text-transform: uppercase; min-width: 60px; text-align: center; color: white; } .method-get { background: #61affe; } .method-post { background: #49cc90; } .method-put { background: #fca130; } .method-delete { background: #f93e3e; } .method-patch { background: #50e3c2; } .method-head { background: #9012fe; } .method-options { background: #0d5aa7; } .api-path { font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; font-size: 14px; font-weight: 600; color: var(--text-primary); } .api-card__summary { display: flex; align-items: center; gap: 8px; } .api-summary { color: var(--text-secondary); font-size: 14px; flex: 1; text-align: right; } .expand-icon { transition: transform 0.2s ease; color: var(--text-primary); } .expand-icon.expanded { transform: rotate(180deg); } .api-card__details { border-top: 1px solid var(--border-color); background: var(--bg-secondary); } .api-detail-section { padding: 16px; } .detail-item { margin-bottom: 16px; } .detail-item:last-child { margin-bottom: 0; } .detail-item label { display: inline-block; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; font-size: 14px; } .detail-item span { color: var(--text-primary); font-size: 14px; } .tag-list { display: flex; flex-wrap: wrap; gap: 8px; } .api-tag { background: var(--bg-tertiary) !important; color: var(--text-primary) !important; border: 1px solid var(--border-color) !important; } .parameter-list { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; } .parameter-item { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border-color); } .parameter-item:last-child { border-bottom: none; } .param-name { font-weight: 600; color: var(--text-primary); min-width: 120px; } .param-type { color: var(--text-primary); font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; font-size: 12px; background: var(--bg-tertiary); padding: 2px 6px; border-radius: 3px; } .param-required { color: #f93e3e; font-size: 12px; font-weight: 600; } .response-list { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; } .response-item { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border-color); } .response-item:last-child { border-bottom: none; } .response-code { font-weight: 600; font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; font-size: 12px; padding: 2px 6px; border-radius: 3px; min-width: 50px; text-align: center; color: white; } .response-success { background: #49cc90; } .response-redirect { background: #61affe; } .response-client-error { background: #fca130; } .response-server-error { background: #f93e3e; } .response-default { background: #9012fe; } .response-desc { color: var(--text-primary); font-size: 14px; flex: 1; } /* API卡片不同方法的边框颜色 */ .api-card--get { border-left: 4px solid #61affe; } .api-card--post { border-left: 4px solid #49cc90; } .api-card--put { border-left: 4px solid #fca130; } .api-card--delete { border-left: 4px solid #f93e3e; } .api-card--patch { border-left: 4px solid #50e3c2; } .api-card--head { border-left: 4px solid #9012fe; } .api-card--options { border-left: 4px solid #0d5aa7; } </style>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zaizaizhao/mcp-swagger-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server