Skip to main content
Glama
ConfigManagerNew.vue39.5 kB
<template> <div class="config-manager"> <!-- 页面头部 --> <div class="page-header"> <h2>配置管理</h2> <div class="header-actions"> <el-button type="primary" @click="showExportDialog" :icon="Download"> 导出配置 </el-button> <el-button type="success" @click="showImportDialog" :icon="Upload"> 导入配置 </el-button> <el-button @click="refreshData" :icon="Refresh" :loading="loading"> 刷新 </el-button> </div> </div> <!-- 主要内容区域 --> <el-tabs v-model="activeTab" class="config-tabs"> <!-- 配置管理标签页 --> <el-tab-pane label="配置管理" name="config"> <div class="config-content"> <!-- 配置概览卡片 --> <el-card shadow="never" class="overview-card"> <template #header> <div class="card-header"> <span>配置概览</span> <div class="header-actions"> <el-select v-model="selectedConfigType" placeholder="筛选配置类型" clearable style="width: 180px" > <el-option label="服务器配置" value="servers" /> <el-option label="认证配置" value="auth" /> <el-option label="OpenAPI规范" value="openapi" /> <el-option label="测试用例" value="testcases" /> <el-option label="系统设置" value="settings" /> </el-select> </div> </div> </template> <div class="config-grid" v-loading="loading"> <div v-for="config in filteredConfigs" :key="config.type" class="config-card" > <div class="config-header"> <div class="config-title"> <el-icon class="config-icon" :class="getConfigIcon(config.type)" > <Setting /> </el-icon> <span>{{ getConfigLabel(config.type) }}</span> </div> <el-tag :type="getConfigStatus(config)" size="small"> {{ config.count }} 项 </el-tag> </div> <div class="config-details"> <div class="detail-row"> <span class="label">最后更新:</span> <span class="value">{{ formatDateTime(config.lastUpdated) }}</span> </div> <div class="detail-row"> <span class="label">大小:</span> <span class="value">{{ formatFileSize(config.size) }}</span> </div> </div> <div class="config-actions"> <el-button size="small" @click="previewConfig(config.type)" :icon="View" > 预览 </el-button> <el-button size="small" @click="exportSingleConfig(config.type)" :icon="Download" > 导出 </el-button> </div> </div> </div> </el-card> <!-- 操作历史卡片 --> <el-card shadow="never" class="history-card"> <template #header> <span>最近操作记录</span> </template> <div v-if="operationHistory.length === 0" class="empty-history"> <el-empty description="暂无操作记录" /> </div> <el-timeline v-else> <el-timeline-item v-for="item in operationHistory.slice(0, 5)" :key="item.id" :type="item.status === 'success' ? 'success' : 'danger'" :timestamp="formatDateTime(item.timestamp)" > <div class="history-item"> <h4>{{ item.description }}</h4> <p>{{ item.details }}</p> <el-button link type="primary" size="small" @click="showHistoryDetails(item)" > 查看详情 </el-button> </div> </el-timeline-item> </el-timeline> </el-card> </div> </el-tab-pane> <!-- 备份管理标签页 --> <el-tab-pane label="备份管理" name="backup"> <BackupManager /> </el-tab-pane> </el-tabs> <!-- 其他对话框和组件保持不变... --> <!-- 导出配置对话框 --> <el-dialog v-model="exportDialogVisible" title="导出配置" width="600px"> <el-form ref="exportFormRef" :model="exportForm" label-width="120px"> <el-form-item label="导出类型"> <el-checkbox-group v-model="exportForm.types"> <el-checkbox label="servers">服务器配置</el-checkbox> <el-checkbox label="auth">认证配置</el-checkbox> <el-checkbox label="openapi">OpenAPI规范</el-checkbox> <el-checkbox label="testcases">测试用例</el-checkbox> <el-checkbox label="settings">系统设置</el-checkbox> </el-checkbox-group> </el-form-item> <el-form-item label="导出格式"> <el-radio-group v-model="exportForm.format"> <el-radio label="json">JSON</el-radio> <el-radio label="yaml">YAML</el-radio> <el-radio label="zip">ZIP压缩包</el-radio> </el-radio-group> </el-form-item> <el-form-item label="敏感数据"> <el-radio-group v-model="exportForm.sensitiveData"> <el-radio label="exclude">排除敏感信息</el-radio> <el-radio label="encrypt">加密敏感信息</el-radio> <el-radio label="include">包含敏感信息</el-radio> </el-radio-group> </el-form-item> <el-form-item v-if="exportForm.sensitiveData === 'encrypt'" label="加密密码" > <el-input v-model="exportForm.password" type="password" show-password placeholder="请输入加密密码" /> </el-form-item> <el-form-item label="备注"> <el-input v-model="exportForm.notes" type="textarea" :rows="3" placeholder="可选:添加导出说明" /> </el-form-item> </el-form> <template #footer> <el-button @click="exportDialogVisible = false">取消</el-button> <el-button type="primary" @click="executeExport" :loading="configStore.loading" > 导出配置 </el-button> </template> </el-dialog> <!-- 导入配置对话框 --> <el-dialog v-model="importDialogVisible" title="导入配置" width="800px" :close-on-click-modal="false" > <!-- 导入步骤指示器 --> <el-steps :active="importStep" finish-status="success" class="import-steps" > <el-step title="选择文件" /> <el-step title="验证配置" /> <el-step title="解决冲突" /> <el-step title="导入完成" /> </el-steps> <!-- 步骤1: 文件选择 --> <div v-if="importStep === 0" class="import-step"> <div class="upload-section"> <el-upload ref="uploadRef" drag :auto-upload="false" :on-change="handleFileUpload" :show-file-list="false" accept=".json,.yaml,.yml,.zip" > <el-icon class="el-icon--upload"><UploadFilled /></el-icon> <div class="el-upload__text"> 将配置文件拖拽到此处,或<em>点击选择文件</em> </div> <template #tip> <div class="el-upload__tip"> 支持 JSON、YAML、ZIP 格式的配置文件 </div> </template> </el-upload> </div> <div v-if="importForm.file" class="file-info"> <h4>已选择文件</h4> <div class="file-details"> <div class="detail-item"> <span class="label">文件名:</span> <span class="value">{{ importForm.file.name }}</span> </div> <div class="detail-item"> <span class="label">文件大小:</span> <span class="value">{{ formatFileSize(importForm.file.size) }}</span> </div> <div class="detail-item"> <span class="label">文件类型:</span> <span class="value">{{ getFileType(importForm.file.name) }}</span> </div> </div> </div> </div> <!-- 步骤2: 配置验证 --> <div v-if="importStep === 1" class="import-step"> <div class="validation-result" v-loading="configStore.loading"> <div v-if="validationResult"> <div class="validation-summary"> <div class="summary-item" :class="validationResult.valid ? 'success' : 'error'" > <el-icon ><Check v-if="validationResult.valid" /><Close v-else /></el-icon> <span >配置验证{{ validationResult.valid ? "成功" : "失败" }}</span > </div> </div> <div class="validation-details"> <el-collapse> <el-collapse-item title="配置内容概览" name="1"> <div class="config-overview"> <div v-for="(count, type) in validationResult.configCounts" :key="type" class="config-count-item" > <span class="type-label">{{ getConfigLabel(type) }}</span> <span class="count-value">{{ count }} 项</span> </div> </div> </el-collapse-item> <el-collapse-item v-if="validationResult.errors.length > 0" title="验证错误" name="2" > <div class="validation-errors"> <div v-for="error in validationResult.errors" :key="error.id" class="error-item" > <el-text type="danger">{{ error.message }}</el-text> <div class="error-details">{{ error.details }}</div> </div> </div> </el-collapse-item> <el-collapse-item v-if="validationResult.warnings.length > 0" title="验证警告" name="3" > <div class="validation-warnings"> <div v-for="warning in validationResult.warnings" :key="warning.id" class="warning-item" > <el-text type="warning">{{ warning.message }}</el-text> <div class="warning-details">{{ warning.details }}</div> </div> </div> </el-collapse-item> </el-collapse> </div> </div> </div> </div> <!-- 步骤3: 冲突解决 --> <div v-if="importStep === 2" class="import-step"> <div v-if="conflicts.length > 0" class="conflicts-section"> <h4>检测到配置冲突</h4> <p class="conflicts-description"> 以下配置项与现有配置存在冲突,请选择处理方式: </p> <div class="conflicts-list"> <div v-for="conflict in conflicts" :key="conflict.id" class="conflict-item" > <div class="conflict-header"> <h5>{{ conflict.title }}</h5> <el-tag size="small">{{ getConfigLabel(conflict.type) }}</el-tag> </div> <div class="conflict-content"> <div class="conflict-side"> <h6>当前配置</h6> <pre>{{ JSON.stringify(conflict.current, null, 2) }}</pre> </div> <div class="conflict-side"> <h6>导入配置</h6> <pre>{{ JSON.stringify(conflict.incoming, null, 2) }}</pre> </div> </div> <div class="conflict-resolution"> <el-radio-group v-model="conflict.resolution"> <el-radio label="keep">保留当前配置</el-radio> <el-radio label="replace">使用导入配置</el-radio> <el-radio label="merge">合并配置</el-radio> </el-radio-group> </div> </div> </div> </div> <div v-else class="no-conflicts"> <el-result icon="success" title="没有发现冲突" sub-title="可以直接导入配置" > </el-result> </div> </div> <!-- 步骤4: 导入结果 --> <div v-if="importStep === 3" class="import-step"> <div v-if="importing" class="importing-progress"> <el-progress :percentage="80" :show-text="false" /> <p>正在导入配置,请稍候...</p> </div> <div v-if="!configStore.loading && importResult"> <el-result :icon="importResult.success ? 'success' : 'error'" :title="importResult.success ? '导入成功' : '导入失败'" :sub-title="importResult.message" > <template #extra> <div class="import-summary"> <div v-if="importResult.success" class="success-details"> <h4>导入统计</h4> <div v-for="(count, type) in importResult.appliedCounts" :key="type" class="count-item" > <span>{{ getConfigLabel(type) }}: {{ count }}项</span> </div> </div> <div v-else class="error-details"> <div class="error-message">{{ importResult.error }}</div> </div> </div> </template> </el-result> </div> </div> <template #footer> <div class="dialog-footer"> <el-button @click="cancelImport">取消</el-button> <el-button v-if="importStep > 0" @click="prevStep"> 上一步 </el-button> <el-button v-if="importStep < 3" type="primary" @click="nextStep" :disabled="!canProceed" :loading="configStore.loading || importing" > {{ getNextStepText() }} </el-button> </div> </template> </el-dialog> <!-- 配置预览对话框 --> <el-dialog v-model="previewDialogVisible" :title="`${getConfigLabel(previewConfigType)} 预览`" width="800px" > <div class="preview-content"> <div class="preview-toolbar"> <el-radio-group v-model="previewFormat" size="small"> <el-radio-button label="json">JSON</el-radio-button> <el-radio-button label="yaml">YAML</el-radio-button> </el-radio-group> <el-button size="small" :icon="CopyDocument" @click="copyPreviewContent" > 复制 </el-button> </div> <div class="preview-body"> <pre>{{ previewContent[previewFormat] }}</pre> </div> </div> </el-dialog> <!-- 操作历史详情对话框 --> <el-dialog v-model="historyDetailsVisible" title="操作详情" width="600px"> <div v-if="selectedHistoryItem" class="history-details"> <div class="basic-info"> <h4>基本信息</h4> <div class="info-grid"> <div class="info-item"> <span class="label">操作类型:</span> <span class="value">{{ selectedHistoryItem.description }}</span> </div> <div class="info-item"> <span class="label">执行时间:</span> <span class="value">{{ formatDateTime(selectedHistoryItem.timestamp) }}</span> </div> <div class="info-item"> <span class="label">操作状态:</span> <span class="value" :class=" selectedHistoryItem.status === 'success' ? 'success' : 'error' " > {{ selectedHistoryItem.status === "success" ? "成功" : "失败" }} </span> </div> </div> </div> <div class="detail-section" v-if="selectedHistoryItem.metadata"> <h4>详细信息</h4> <pre>{{ JSON.stringify(selectedHistoryItem.metadata, null, 2) }}</pre> </div> </div> </el-dialog> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted } from "vue"; import { ElMessage, ElMessageBox, type FormInstance, type UploadInstance, type UploadFile, } from "element-plus"; import { Setting, Download, Upload, Refresh, View, Check, Close, UploadFilled, CopyDocument, } from "@element-plus/icons-vue"; // 引入stores import { useAppStore } from "@/stores/app"; import { useConfigStore, type ConfigExportOptions, type ConfigValidationResult, type ConfigConflict, } from "@/stores/config"; // 引入组件 import BackupManager from "./components/BackupManager.vue"; // Types interface ConfigItem { type: string; count: number; lastUpdated: Date; size: number; valid: boolean; } interface OperationHistory { id: string; type: "import" | "export" | "backup" | "restore"; timestamp: Date; status: "success" | "failed"; description: string; details: string; metadata?: any; } interface ImportForm { file: File | null; } interface ImportResult { success: boolean; message: string; appliedCounts?: Record<string, number>; error?: string; } // Stores const appStore = useAppStore(); const configStore = useConfigStore(); // Reactive data const loading = ref(false); const selectedConfigType = ref(""); const activeTab = ref("config"); // 对话框状态 const exportDialogVisible = ref(false); const importDialogVisible = ref(false); const previewDialogVisible = ref(false); const historyDetailsVisible = ref(false); // 表单数据 const exportFormRef = ref<FormInstance>(); const uploadRef = ref<UploadInstance>(); const exportForm = ref<ConfigExportOptions>({ types: ["servers", "auth", "openapi", "testcases", "settings"], format: "json", sensitiveData: "exclude", password: "", notes: "", }); const importForm = ref<ImportForm>({ file: null, }); // 导入流程状态 const importStep = ref(0); const importing = ref(false); const validationResult = ref<ConfigValidationResult | null>(null); const conflicts = ref<ConfigConflict[]>([]); const importResult = ref<ImportResult | null>(null); // 预览相关 const previewConfigType = ref(""); const previewFormat = ref<"json" | "yaml">("json"); const previewContent = ref({ json: "", yaml: "" }); // 历史记录 const operationHistory = ref<OperationHistory[]>([]); const selectedHistoryItem = ref<OperationHistory | null>(null); // 模拟配置数据 const configItems = ref<ConfigItem[]>([ { type: "servers", count: 3, lastUpdated: new Date(Date.now() - 2 * 60 * 60 * 1000), size: 2048, valid: true, }, { type: "auth", count: 2, lastUpdated: new Date(Date.now() - 1 * 60 * 60 * 1000), size: 512, valid: true, }, { type: "openapi", count: 5, lastUpdated: new Date(Date.now() - 30 * 60 * 1000), size: 15360, valid: true, }, { type: "testcases", count: 12, lastUpdated: new Date(Date.now() - 10 * 60 * 1000), size: 8192, valid: true, }, { type: "settings", count: 1, lastUpdated: new Date(Date.now() - 5 * 60 * 1000), size: 256, valid: true, }, ]); // Computed properties const filteredConfigs = computed(() => { if (!selectedConfigType.value) return configItems.value; return configItems.value.filter( (config) => config.type === selectedConfigType.value, ); }); const canProceed = computed(() => { switch (importStep.value) { case 0: return importForm.value.file !== null; case 1: return validationResult.value?.valid === true; case 2: return true; // 冲突解决步骤总是可以继续 case 3: return false; // 最后一步不需要继续按钮 default: return false; } }); // Methods const showExportDialog = () => { exportDialogVisible.value = true; }; const showImportDialog = () => { importStep.value = 0; importForm.value.file = null; validationResult.value = null; conflicts.value = []; importResult.value = null; importDialogVisible.value = true; }; const refreshData = async () => { loading.value = true; try { // 模拟数据刷新 await new Promise((resolve) => setTimeout(resolve, 1000)); // 更新配置项的时间戳 configItems.value.forEach((config) => { config.lastUpdated = new Date( Date.now() - Math.random() * 60 * 60 * 1000, ); }); ElMessage.success("数据刷新成功"); } catch (error) { ElMessage.error("数据刷新失败"); } finally { loading.value = false; } }; const getConfigIcon = (type: string): string => { const icons: Record<string, string> = { servers: "el-icon-server", auth: "el-icon-lock", openapi: "el-icon-document", testcases: "el-icon-data-analysis", settings: "el-icon-setting", }; return icons[type] || "el-icon-files"; }; const getConfigLabel = (type: string): string => { const labels: Record<string, string> = { servers: "服务器配置", auth: "认证配置", openapi: "OpenAPI规范", testcases: "测试用例", settings: "系统设置", }; return labels[type] || type; }; const getConfigStatus = (config: ConfigItem): string => { if (!config.valid) return "danger"; if (config.count === 0) return "info"; return "success"; }; const formatDateTime = (date: Date): string => { return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }).format(new Date(date)); }; const formatFileSize = (bytes: number): string => { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; const getFileType = (filename: string): string => { const ext = filename.split(".").pop()?.toLowerCase(); const types: Record<string, string> = { json: "JSON配置文件", yaml: "YAML配置文件", yml: "YAML配置文件", zip: "ZIP压缩包", }; return types[ext || ""] || "未知格式"; }; const previewConfig = async (type: string) => { previewConfigType.value = type; // 模拟获取配置数据 const mockConfig = generateMockConfig(type); previewContent.value = { json: JSON.stringify(mockConfig, null, 2), yaml: convertToYaml(mockConfig), }; previewDialogVisible.value = true; }; const exportSingleConfig = async (type: string) => { try { const config = generateMockConfig(type); const filename = `${type}-config-${Date.now()}.json`; downloadFile(JSON.stringify(config, null, 2), filename, "application/json"); addOperationHistory( "export", `导出${getConfigLabel(type)}`, `已导出 ${filename}`, true, { type, filename }, ); ElMessage.success("配置导出成功"); } catch (error) { ElMessage.error("配置导出失败"); } }; const executeExport = async () => { if (exportForm.value.types.length === 0) { ElMessage.warning("请选择要导出的配置类型"); return; } const success = await configStore.exportConfig(exportForm.value); if (success) { addOperationHistory( "export", "导出配置", `已导出 ${exportForm.value.types.length} 种配置类型`, true, { types: exportForm.value.types, format: exportForm.value.format }, ); exportDialogVisible.value = false; } }; const handleFileUpload = async (file: UploadFile) => { if (file.raw) { importForm.value.file = file.raw; // 自动进行验证 validationResult.value = await configStore.validateImportConfig(file.raw); if (validationResult.value?.valid) { // 检测冲突 try { const content = await readFileContent(file.raw); const parsedConfig = parseConfigFile(content, file.raw.name); conflicts.value = await configStore.detectConflicts(parsedConfig); } catch (error) { console.error("冲突检测失败:", error); } } } }; const nextStep = async () => { switch (importStep.value) { case 0: if (importForm.value.file) { importStep.value = 1; await validateConfig(); } break; case 1: if (validationResult.value?.valid) { importStep.value = 2; await detectConflicts(); } break; case 2: importStep.value = 3; await applyConfig(); break; } }; const prevStep = () => { if (importStep.value > 0) { importStep.value--; } }; const validateConfig = async () => { if (!importForm.value.file) return; validationResult.value = await configStore.validateImportConfig( importForm.value.file, ); }; const detectConflicts = async () => { if (!importForm.value.file) return; try { const fileContent = await readFileContent(importForm.value.file); const parsedConfig = parseConfigFile( fileContent, importForm.value.file.name, ); conflicts.value = await configStore.detectConflicts(parsedConfig); } catch (error) { console.error("冲突检测失败:", error); ElMessage.error("冲突检测失败"); } }; const applyConfig = async () => { importing.value = true; try { if (!importForm.value.file) { throw new Error("没有选择文件"); } const fileContent = await readFileContent(importForm.value.file); const parsedConfig = parseConfigFile( fileContent, importForm.value.file.name, ); const result = await configStore.applyImportConfig( parsedConfig, conflicts.value, ); if (result.success) { importResult.value = result; addOperationHistory( "import", "导入配置", `成功导入 ${importForm.value.file.name}`, true, { filename: importForm.value.file.name, conflicts: conflicts.value.length, }, ); } else { importResult.value = { success: false, message: result.message, error: result.errors?.[0] || "导入失败", }; } } catch (error) { importResult.value = { success: false, message: "配置导入失败", error: error instanceof Error ? error.message : "未知错误", }; addOperationHistory( "import", "导入配置", `导入失败: ${importForm.value.file?.name}`, false, { filename: importForm.value.file?.name, error: error instanceof Error ? error.message : "未知错误", }, ); ElMessage.error("配置导入失败"); } finally { importing.value = false; } }; const cancelImport = () => { importDialogVisible.value = false; importStep.value = 0; importForm.value.file = null; }; const getNextStepText = (): string => { switch (importStep.value) { case 0: return "验证配置"; case 1: return "检测冲突"; case 2: return "开始导入"; default: return "下一步"; } }; const copyPreviewContent = () => { navigator.clipboard.writeText(previewContent.value[previewFormat.value]); ElMessage.success("已复制到剪贴板"); }; const showHistoryDetails = (item: OperationHistory) => { selectedHistoryItem.value = item; historyDetailsVisible.value = true; }; const addOperationHistory = ( type: "import" | "export" | "backup" | "restore", description: string, details: string, success: boolean, metadata?: any, ) => { const record: OperationHistory = { id: `operation_${Date.now()}`, type, timestamp: new Date(), status: success ? "success" : "failed", description, details, metadata, }; operationHistory.value.unshift(record); // 保留最近50条记录 if (operationHistory.value.length > 50) { operationHistory.value = operationHistory.value.slice(0, 50); } }; // Helper functions const generateMockConfig = (type: string): any => { const mockConfigs: Record<string, any> = { servers: [ { id: "1", name: "API Server", host: "localhost", port: 3000, protocol: "http", }, { id: "2", name: "Database Server", host: "db.example.com", port: 5432, protocol: "tcp", }, ], auth: [ { id: "1", type: "bearer", name: "API Token", description: "Bearer token authentication", }, { id: "2", type: "basic", name: "Basic Auth", description: "Username/password authentication", }, ], openapi: [ { id: "1", name: "User API", version: "1.0.0", path: "/api/v1/users" }, { id: "2", name: "Product API", version: "2.0.0", path: "/api/v2/products", }, ], testcases: [ { id: "1", name: "Login Test", type: "auth", status: "passing" }, { id: "2", name: "CRUD Test", type: "api", status: "passing" }, ], settings: { theme: "light", language: "zh-CN", notifications: true, autoSave: true, }, }; return mockConfigs[type] || {}; }; const convertToYaml = (obj: any): string => { // 简单的YAML转换(实际项目中应使用专门的库) return JSON.stringify(obj, null, 2).replace(/"/g, "").replace(/,$/gm, ""); }; const downloadFile = ( content: string, filename: string, mimeType: string, ): void => { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; 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("文件读取失败")); reader.readAsText(file); }); }; const parseConfigFile = (content: string, filename: string): any => { const ext = filename.split(".").pop()?.toLowerCase(); if (ext === "json") { return JSON.parse(content); } else if (ext === "yaml" || ext === "yml") { // 实际项目中应使用yaml解析库 return JSON.parse(content); // 临时实现 } else { throw new Error("不支持的文件格式"); } }; onMounted(() => { // 初始化一些示例操作历史 addOperationHistory( "export", "导出服务器配置", "成功导出3个服务器配置", true, { type: "servers", count: 3 }, ); addOperationHistory("import", "导入认证配置", "成功导入2个认证配置", true, { type: "auth", count: 2, }); }); </script> <style scoped> .config-manager { padding: 20px; } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid var(--el-border-color); } .page-header h2 { margin: 0; color: var(--el-text-color-primary); } .header-actions { display: flex; gap: 12px; } .config-tabs { margin-top: 20px; } .config-content { display: flex; flex-direction: column; gap: 20px; } .overview-card { margin-bottom: 20px; } .card-header { display: flex; justify-content: space-between; align-items: center; } .header-actions { display: flex; gap: 12px; align-items: center; } .config-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; } .config-card { padding: 20px; border: 1px solid var(--el-border-color); border-radius: 8px; background: var(--el-bg-color); transition: all 0.3s ease; } .config-card:hover { border-color: var(--el-color-primary); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .config-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .config-title { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--el-text-color-primary); } .config-icon { color: var(--el-color-primary); } .config-details { margin-bottom: 16px; } .detail-row { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 13px; } .detail-row .label { color: var(--el-text-color-secondary); } .detail-row .value { color: var(--el-text-color-primary); } .config-actions { display: flex; gap: 8px; } .history-card { margin-top: 20px; } .empty-history { text-align: center; padding: 40px; } .history-item h4 { margin: 0 0 8px 0; color: var(--el-text-color-primary); } .history-item p { margin: 0 0 8px 0; color: var(--el-text-color-secondary); font-size: 13px; } .import-steps { margin-bottom: 30px; } .import-step { min-height: 300px; padding: 20px 0; } .upload-section { margin-bottom: 20px; } .file-info { padding: 20px; background: var(--el-bg-color-page); border-radius: 8px; } .file-details { display: flex; flex-direction: column; gap: 8px; } .detail-item { display: flex; justify-content: space-between; } .detail-item .label { color: var(--el-text-color-secondary); font-weight: 500; } .detail-item .value { color: var(--el-text-color-primary); } .validation-result { min-height: 200px; } .validation-summary { margin-bottom: 20px; } .summary-item { display: flex; align-items: center; gap: 8px; padding: 12px; border-radius: 6px; font-weight: 500; } .summary-item.success { background: var(--el-color-success-light-9); color: var(--el-color-success); } .summary-item.error { background: var(--el-color-danger-light-9); color: var(--el-color-danger); } .config-overview { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; } .config-count-item { display: flex; justify-content: space-between; padding: 12px; background: var(--el-bg-color-page); border-radius: 6px; } .type-label { color: var(--el-text-color-primary); font-weight: 500; } .count-value { color: var(--el-color-primary); font-weight: 600; } .error-item, .warning-item { padding: 12px; margin-bottom: 8px; border-radius: 6px; } .error-item { background: var(--el-color-danger-light-9); } .warning-item { background: var(--el-color-warning-light-9); } .error-details, .warning-details { margin-top: 4px; font-size: 12px; opacity: 0.8; } .conflicts-section { padding: 20px 0; } .conflicts-description { color: var(--el-text-color-secondary); margin-bottom: 20px; } .conflict-item { border: 1px solid var(--el-border-color); border-radius: 8px; margin-bottom: 16px; overflow: hidden; } .conflict-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--el-bg-color-page); border-bottom: 1px solid var(--el-border-color); } .conflict-header h5 { margin: 0; color: var(--el-text-color-primary); } .conflict-content { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--el-border-color); } .conflict-side { padding: 16px; background: var(--el-bg-color); } .conflict-side h6 { margin: 0 0 8px 0; color: var(--el-text-color-secondary); font-size: 12px; text-transform: uppercase; } .conflict-side pre { margin: 0; font-size: 12px; background: var(--el-bg-color-page); padding: 8px; border-radius: 4px; overflow-x: auto; } .conflict-resolution { padding: 16px; background: var(--el-bg-color); } .no-conflicts { text-align: center; padding: 40px; } .importing-progress { text-align: center; padding: 40px; } .importing-progress p { margin-top: 16px; color: var(--el-text-color-secondary); } .import-summary { text-align: left; } .success-details h4 { margin: 0 0 12px 0; color: var(--el-text-color-primary); } .count-item { margin-bottom: 4px; color: var(--el-text-color-secondary); } .error-message { color: var(--el-color-danger); font-weight: 500; } .dialog-footer { display: flex; justify-content: flex-end; gap: 12px; } .preview-content { max-height: 500px; overflow: hidden; } .preview-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--el-border-color); } .preview-body { max-height: 450px; overflow: auto; } .preview-body pre { margin: 0; font-size: 13px; line-height: 1.5; } .history-details { padding: 20px 0; } .basic-info h4 { margin: 0 0 16px 0; color: var(--el-text-color-primary); } .info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; } .info-item { display: flex; flex-direction: column; gap: 4px; } .info-item .label { color: var(--el-text-color-secondary); font-size: 12px; text-transform: uppercase; } .info-item .value { color: var(--el-text-color-primary); font-weight: 500; } .info-item .value.success { color: var(--el-color-success); } .info-item .value.error { color: var(--el-color-danger); } .detail-section { margin-top: 20px; } .detail-section h4 { margin: 0 0 12px 0; color: var(--el-text-color-primary); } .detail-section pre { background: var(--el-bg-color-page); padding: 16px; border-radius: 6px; font-size: 12px; overflow-x: auto; } </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