Skip to main content
Glama
ConfigManager.vue48 kB
<template> <div class="config-manager"> <!-- 页面头部 --> <div class="header-section"> <div class="header-content"> <h1> <el-icon><Setting /></el-icon> 配置导入导出 </h1> <p class="header-description"> 管理MCP网关配置的导入导出,支持配置备份和环境迁移 </p> </div> <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 type="warning" @click="showMigrationWizard" :icon="Guide"> 配置迁移 </el-button> <el-button @click="refreshData" :loading="loading" :icon="Refresh"> 刷新 </el-button> </div> </div> <!-- 主要内容区域 --> <el-tabs v-model="activeTab" class="config-tabs"> <el-tab-pane label="配置管理" name="config"> <div class="main-content"> <!-- 配置预览区域 --> <div class="config-preview-section"> <div class="section-header"> <h3>当前配置概览</h3> <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> <div class="config-cards" v-loading="loading"> <div v-for="config in filteredConfigs" :key="config.type" class="config-card" > <div class="card-header"> <div class="config-info"> <div class="config-name"> <el-icon :class="getConfigIcon(config.type)"></el-icon> {{ getConfigLabel(config.type) }} </div> <el-tag :type="getConfigStatus(config)"> {{ config.count }} 项配置 </el-tag> </div> <div class="card-actions"> <el-tooltip content="预览配置"> <el-button size="small" @click="previewConfig(config.type)" :icon="View" circle /> </el-tooltip> <el-tooltip content="单独导出"> <el-button size="small" type="primary" @click="exportSingleConfig(config.type)" :icon="Download" circle /> </el-tooltip> </div> </div> <div class="config-details"> <div class="detail-item"> <span class="label">最后更新:</span> <span class="value">{{ formatDateTime(config.lastUpdated) }}</span> </div> <div class="detail-item"> <span class="label">配置大小:</span> <span class="value">{{ formatFileSize(config.size) }}</span> </div> <div class="detail-item"> <span class="label">状态:</span> <span class="value" :class="config.valid ? 'valid' : 'invalid'" > {{ config.valid ? "有效" : "无效" }} </span> </div> </div> </div> <div v-if="filteredConfigs.length === 0" class="empty-state"> <el-empty description="没有找到配置信息"> <el-button type="primary" @click="refreshData" >刷新数据</el-button > </el-empty> </div> </div> </div> <!-- 操作历史 --> <div class="history-section"> <div class="section-header"> <h3>操作历史</h3> <el-button size="small" @click="clearHistory" :disabled="operationHistory.length === 0" > 清空历史 </el-button> </div> <div class="history-timeline"> <el-timeline> <el-timeline-item v-for="item in operationHistory" :key="item.id" :timestamp="formatDateTime(item.timestamp)" :type="getHistoryType(item.type)" > <div class="history-content"> <div class="history-title">{{ item.title }}</div> <div class="history-description"> {{ item.description }} </div> <div class="history-details" v-if="item.details"> <el-button size="small" text @click="showHistoryDetails(item)" > 查看详情 </el-button> </div> </div> </el-timeline-item> </el-timeline> <div v-if="operationHistory.length === 0" class="empty-history"> <el-text type="info">暂无操作历史</el-text> </div> </div> </div> </div> </el-tab-pane> <!-- 导出配置对话框 --> <el-dialog v-model="exportDialogVisible" title="导出配置" width="600px" :close-on-click-modal="false" > <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 label="加密密码" v-if="exportForm.sensitiveData === 'encrypt'" > <el-input v-model="exportForm.password" type="password" placeholder="请输入加密密码" show-password /> </el-form-item> <el-form-item label="备注信息"> <el-input v-model="exportForm.notes" type="textarea" placeholder="可选的备注信息" :rows="3" /> </el-form-item> </el-form> <template #footer> <el-button @click="exportDialogVisible = false">取消</el-button> <el-button type="primary" @click="executeExport" :loading="configStore.loading" :disabled="exportForm.types.length === 0" > 导出配置 </el-button> </template> </el-dialog> <!-- 导入配置对话框 --> <el-dialog v-model="importDialogVisible" title="导入配置" width="700px" :close-on-click-modal="false" > <div class="import-steps"> <el-steps :active="importStep" align-center> <el-step title="选择文件" description="上传配置文件" /> <el-step title="验证配置" description="检查配置有效性" /> <el-step title="解决冲突" description="处理配置冲突" /> <el-step title="应用配置" description="完成导入" /> </el-steps> </div> <!-- 步骤1: 文件上传 --> <div v-if="importStep === 0" class="import-step"> <el-upload ref="uploadRef" drag :auto-upload="false" :on-change="handleFileChange" :before-remove="handleFileRemove" accept=".json,.yaml,.yml,.zip" :limit="1" > <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 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">{{ getConfigLabel(type) }}:</span> <span class="count">{{ 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"> <div class="conflict-info"> <div class="conflict-title">{{ conflict.title }}</div> <el-tag size="small" :type="getConflictType(conflict.type)"> {{ getConflictLabel(conflict.type) }} </el-tag> </div> <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 class="conflict-details"> <div class="conflict-comparison"> <div class="current-config"> <h5>当前配置</h5> <pre>{{ JSON.stringify(conflict.current, null, 2) }}</pre> </div> <div class="new-config"> <h5>导入配置</h5> <pre>{{ JSON.stringify(conflict.incoming, null, 2) }}</pre> </div> </div> </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 class="import-progress" v-loading="importing"> <div v-if="!importing && 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="applied-item" > <span>{{ getConfigLabel(type) }}: </span> <span>{{ count }} 项已应用</span> </div> </div> <div v-else class="error-details"> <h4>错误详情</h4> <div class="error-message">{{ importResult.error }}</div> </div> </div> </template> </el-result> </div> </div> </div> <template #footer> <div class="dialog-footer"> <el-button @click="cancelImport">取消</el-button> <el-button v-if="importStep > 0" @click="prevStep" :disabled="importing" > 上一步 </el-button> <el-button type="primary" @click="nextStep" :loading="configStore.loading || importing" :disabled="!canProceed" > {{ getNextStepText() }} </el-button> </div> </template> </el-dialog> <!-- 配置预览对话框 --> <el-dialog v-model="previewDialogVisible" :title="`${getConfigLabel(previewConfigType)} 配置预览`" width="800px" > <div class="config-preview"> <div class="preview-toolbar"> <el-radio-group v-model="previewFormat"> <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" @click="copyPreviewContent" :icon="CopyDocument" > 复制 </el-button> </div> <div class="preview-content"> <pre v-if="previewFormat === 'json'">{{ previewContent.json }}</pre> <pre v-else>{{ previewContent.yaml }}</pre> </div> </div> </el-dialog> <!-- 历史详情对话框 --> <el-dialog v-model="historyDetailsVisible" title="操作详情" width="600px"> <div v-if="selectedHistoryItem" class="history-details"> <div class="detail-section"> <h4>基本信息</h4> <div class="info-grid"> <div class="info-item"> <span class="label">操作类型:</span> <span class="value">{{ selectedHistoryItem.title }}</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.success ? 'success' : 'error'" > {{ selectedHistoryItem.success ? "成功" : "失败" }} </span> </div> </div> </div> <div class="detail-section" v-if="selectedHistoryItem.details"> <h4>详细信息</h4> <pre>{{ JSON.stringify(selectedHistoryItem.details, null, 2) }}</pre> </div> </div> </el-dialog> <!-- 配置迁移向导对话框 --> <el-dialog v-model="migrationWizardVisible" title="配置迁移向导" width="90%" :before-close="handleMigrationWizardClose" :close-on-click-modal="false" > <ConfigMigrationWizard @completed="handleMigrationCompleted" @cancelled="handleMigrationCancelled" /> </el-dialog> <!-- 冲突解决对话框 --> <el-dialog v-model="conflictResolverVisible" title="解决配置冲突" width="85%" :before-close="handleConflictResolverClose" :close-on-click-modal="false" > <ConflictResolver :conflicts="detectedConflicts" :model-value="conflictResolverVisible" @update:model-value="conflictResolverVisible = $event" @resolved="handleConflictsResolved" @cancelled="handleConflictResolutionCancelled" /> </el-dialog> <el-tab-pane label="备份管理" name="backup"> <BackupManager /> </el-tab-pane> </el-tabs> </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, Guide, } from "@element-plus/icons-vue"; // 引入stores import { useAppStore } from "@/stores/app"; import { useConfigStore, type ConfigExportOptions, type ConfigValidationResult, type ConfigConflict, type ConfigConflictResolution, } from "@/stores/config"; // 引入组件 import BackupManager from "./components/BackupManager.vue"; import ConfigMigrationWizard from "./components/ConfigMigrationWizard.vue"; import ConflictResolver from "./components/ConflictResolver.vue"; // Types interface ConfigItem { type: string; count: number; lastUpdated: Date; size: number; valid: boolean; } interface OperationHistory { id: string; type: "export" | "import" | "migration" | "conflict-resolution"; title: string; description: string; timestamp: Date; success: boolean; details?: any; } interface ExportForm { types: string[]; format: "json" | "yaml" | "zip"; sensitiveData: "exclude" | "encrypt" | "include"; password: string; notes: string; } interface ImportForm { file: File | null; } interface ValidationResult { valid: boolean; configCounts: Record<string, number>; errors: Array<{ id: string; message: string; details: string }>; warnings: Array<{ id: string; message: string; details: string }>; } interface ConflictItem { id: string; type: string; title: string; current: any; incoming: any; resolution: "keep" | "replace" | "merge"; } interface ImportResult { success: boolean; message: string; appliedCounts?: Record<string, number>; error?: string; } // Store 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 migrationWizardVisible = ref(false); const conflictResolverVisible = 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 detectedConflicts = 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 showMigrationWizard = () => { migrationWizardVisible.value = true; }; const handleMigrationWizardClose = () => { migrationWizardVisible.value = false; }; const handleMigrationCompleted = (result: any) => { migrationWizardVisible.value = false; ElMessage.success( `配置迁移完成: ${result.fromVersion} → ${result.toVersion}`, ); // 添加到操作历史 operationHistory.value.unshift({ id: Date.now().toString(), type: "migration", title: "配置迁移", description: `从版本 ${result.fromVersion} 迁移到 ${result.toVersion}`, timestamp: new Date(), success: true, details: result.stats, }); // 刷新配置数据 refreshData(); }; const handleMigrationCancelled = () => { migrationWizardVisible.value = false; }; // 冲突解决相关方法 const handleConflictResolverClose = () => { conflictResolverVisible.value = false; }; const handleConflictsResolved = (resolutions: ConfigConflictResolution[]) => { conflictResolverVisible.value = false; ElMessage.success(`已解决 ${resolutions.length} 个配置冲突`); // 清空冲突数据 detectedConflicts.value = []; // 添加到操作历史 operationHistory.value.unshift({ id: Date.now().toString(), type: "conflict-resolution", title: "冲突解决", description: `解决了 ${resolutions.length} 个配置冲突`, timestamp: new Date(), success: true, details: { resolutions }, }); // 刷新配置数据 refreshData(); }; const handleConflictResolutionCancelled = () => { conflictResolverVisible.value = false; }; 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 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 handleFileChange = (file: UploadFile) => { importForm.value.file = file.raw || null; }; const handleFileRemove = () => { importForm.value.file = null; return true; }; 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 "应用配置"; case 3: return "完成"; default: return "下一步"; } }; 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 getConflictType = (type: string): string => { const types: Record<string, string> = { server: "warning", auth: "danger", setting: "info", }; return types[type] || "info"; }; const getConflictLabel = (type: string): string => { const labels: Record<string, string> = { server: "服务器冲突", auth: "认证冲突", setting: "设置冲突", }; return labels[type] || type; }; const getHistoryType = (type: string): string => { const types: Record<string, string> = { export: "primary", import: "success", }; return types[type] || "info"; }; const copyPreviewContent = async () => { try { const content = previewFormat.value === "json" ? previewContent.value.json : previewContent.value.yaml; await navigator.clipboard.writeText(content); ElMessage.success("内容已复制到剪贴板"); } catch (error) { ElMessage.error("复制失败"); } }; const clearHistory = async () => { try { await ElMessageBox.confirm("确定要清空所有操作历史吗?", "确认操作", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }); operationHistory.value = []; ElMessage.success("操作历史已清空"); } catch (error) { // 用户取消操作 } }; const showHistoryDetails = (item: OperationHistory) => { selectedHistoryItem.value = item; historyDetailsVisible.value = true; }; // Helper functions const generateMockConfig = (type: string): any => { const configs: Record<string, any> = { servers: [ { id: "1", name: "API Server", port: 3000, status: "running" }, { id: "2", name: "Test Server", port: 3001, status: "stopped" }, ], auth: [ { id: "1", type: "bearer", name: "API Token" }, { id: "2", type: "basic", name: "Basic Auth" }, ], openapi: [ { id: "1", name: "User API", version: "1.0.0" }, { id: "2", name: "Product API", version: "2.0.0" }, ], testcases: [ { id: "1", name: "Login Test", type: "auth" }, { id: "2", name: "CRUD Test", type: "api" }, ], settings: { theme: "light", language: "zh-CN", notifications: true, }, }; return configs[type] || {}; }; const convertToYaml = (obj: any): string => { // 简单的JSON到YAML转换(实际项目中应使用专门的库) return JSON.stringify(obj, null, 2) .replace(/"/g, "") .replace(/,$/gm, "") .replace(/^\s*{\s*$/gm, "") .replace(/^\s*}\s*$/gm, "") .replace(/^\s*\[\s*$/gm, "") .replace(/^\s*\]\s*$/gm, ""); }; const downloadFile = (content: string, filename: string, mimeType: string) => { 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 = (e) => 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("不支持的文件格式"); } }; const removeSensitiveData = (data: any) => { // 递归移除敏感数据 const sensitiveKeys = ["password", "token", "secret", "key"]; const removeSensitive = (obj: any): void => { if (typeof obj === "object" && obj !== null) { for (const key in obj) { if ( sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive), ) ) { obj[key] = "[REMOVED]"; } else if (typeof obj[key] === "object") { removeSensitive(obj[key]); } } } }; removeSensitive(data); }; const encryptSensitiveData = (data: any, password: string) => { // 简单的加密实现(实际项目中应使用更安全的加密方法) const encrypt = (text: string): string => { return btoa(text + password); // 简单的base64编码 }; const sensitiveKeys = ["password", "token", "secret", "key"]; const encryptSensitive = (obj: any): void => { if (typeof obj === "object" && obj !== null) { for (const key in obj) { if ( sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive), ) ) { if (typeof obj[key] === "string") { obj[key] = `[ENCRYPTED]${encrypt(obj[key])}`; } } else if (typeof obj[key] === "object") { encryptSensitive(obj[key]); } } } }; encryptSensitive(data); }; const createZipFile = async (data: any): Promise<string> => { // 模拟ZIP文件创建(实际项目中应使用JSZip等库) return JSON.stringify(data, null, 2); }; const addOperationHistory = ( type: "export" | "import", title: string, description: string, success: boolean, details?: any, ) => { const item: OperationHistory = { id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type, title, description, timestamp: new Date(), success, details, }; operationHistory.value.unshift(item); // 限制历史记录数量 if (operationHistory.value.length > 50) { operationHistory.value = operationHistory.value.slice(0, 50); } }; // 生命周期 onMounted(async () => { await refreshData(); }); </script> <style scoped> .config-manager { height: 100%; display: flex; flex-direction: column; padding: 20px; background-color: var(--el-bg-color-page); } .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; } .main-content { flex: 1; overflow-y: auto; display: grid; grid-template-columns: 2fr 1fr; gap: 20px; } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .section-header h3 { margin: 0; color: var(--el-text-color-primary); font-size: 18px; font-weight: 600; } .config-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; margin-bottom: 20px; } .config-card { background: var(--el-bg-color); border: 1px solid var(--el-border-color-light); border-radius: 8px; padding: 16px; transition: all 0.3s ease; } .config-card:hover { border-color: var(--el-color-primary); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; } .config-info { flex: 1; } .config-name { font-size: 16px; font-weight: 600; color: var(--el-text-color-primary); margin-bottom: 4px; display: flex; align-items: center; gap: 8px; } .card-actions { display: flex; gap: 8px; } .config-details { display: flex; flex-direction: column; gap: 4px; } .detail-item { display: flex; justify-content: space-between; align-items: center; font-size: 12px; } .detail-item .label { color: var(--el-text-color-secondary); } .detail-item .value { color: var(--el-text-color-primary); font-weight: 500; } .detail-item .value.valid { color: var(--el-color-success); } .detail-item .value.invalid { color: var(--el-color-danger); } .empty-state { grid-column: 1 / -1; text-align: center; padding: 40px; } .history-section { background: var(--el-bg-color); border: 1px solid var(--el-border-color-light); border-radius: 8px; padding: 16px; height: fit-content; } .history-timeline { max-height: 400px; overflow-y: auto; } .history-content { margin-left: 12px; } .history-title { font-weight: 600; color: var(--el-text-color-primary); margin-bottom: 4px; } .history-description { color: var(--el-text-color-regular); font-size: 12px; margin-bottom: 4px; } .history-details { margin-top: 8px; } .empty-history { text-align: center; padding: 20px; } .import-steps { margin-bottom: 20px; } .import-step { min-height: 200px; } .file-info { margin-top: 20px; padding: 16px; background: var(--el-fill-color-lighter); border-radius: 4px; } .file-info h4 { margin: 0 0 12px 0; color: var(--el-text-color-primary); } .file-details { display: flex; flex-direction: column; gap: 8px; } .validation-result { min-height: 150px; } .validation-summary { margin-bottom: 16px; } .summary-item { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 600; } .summary-item.success { color: var(--el-color-success); } .summary-item.error { color: var(--el-color-danger); } .config-overview { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; } .config-count-item { display: flex; justify-content: space-between; padding: 8px; background: var(--el-fill-color-lighter); border-radius: 4px; } .validation-errors, .validation-warnings { display: flex; flex-direction: column; gap: 8px; } .error-item, .warning-item { padding: 8px; border-radius: 4px; border-left: 3px solid; } .error-item { background: var(--el-color-danger-light-9); border-color: var(--el-color-danger); } .warning-item { background: var(--el-color-warning-light-9); border-color: var(--el-color-warning); } .error-details, .warning-details { font-size: 12px; margin-top: 4px; opacity: 0.8; } .conflicts-section { padding: 16px; } .conflicts-description { margin-bottom: 16px; color: var(--el-text-color-regular); } .conflicts-list { display: flex; flex-direction: column; gap: 16px; } .conflict-item { border: 1px solid var(--el-border-color); border-radius: 8px; padding: 16px; } .conflict-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .conflict-title { font-weight: 600; color: var(--el-text-color-primary); } .conflict-comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } .current-config, .new-config { padding: 12px; background: var(--el-fill-color-lighter); border-radius: 4px; } .current-config h5, .new-config h5 { margin: 0 0 8px 0; color: var(--el-text-color-primary); } .current-config pre, .new-config pre { margin: 0; font-size: 12px; background: transparent; white-space: pre-wrap; } .no-conflicts { text-align: center; padding: 40px; } .import-progress { min-height: 200px; } .import-summary { text-align: left; margin-top: 16px; } .success-details h4, .error-details h4 { margin: 0 0 12px 0; color: var(--el-text-color-primary); } .applied-item { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid var(--el-border-color-lighter); } .error-message { padding: 12px; background: var(--el-color-danger-light-9); border-radius: 4px; color: var(--el-color-danger); } .dialog-footer { display: flex; justify-content: flex-end; gap: 12px; } .config-preview { height: 500px; display: flex; flex-direction: column; } .preview-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--el-border-color-lighter); } .preview-content { flex: 1; overflow: auto; background: var(--el-fill-color-lighter); border-radius: 4px; padding: 12px; } .preview-content pre { margin: 0; font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; font-size: 12px; line-height: 1.5; white-space: pre-wrap; } .history-details { display: flex; flex-direction: column; gap: 16px; } .detail-section h4 { margin: 0 0 12px 0; color: var(--el-text-color-primary); font-size: 16px; } .info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; } .info-item { display: flex; flex-direction: column; gap: 4px; } .info-item .label { font-size: 12px; color: var(--el-text-color-secondary); } .info-item .value { font-weight: 600; color: var(--el-text-color-primary); } .info-item .value.success { color: var(--el-color-success); } .info-item .value.error { color: var(--el-color-danger); } .detail-section pre { background: var(--el-fill-color-lighter); padding: 12px; border-radius: 4px; font-size: 12px; overflow: auto; max-height: 300px; } </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