Skip to main content
Glama
ConfigMigrationWizard.vue48.6 kB
<template> <div class="migration-wizard"> <!-- 向导步骤指示器 --> <div class="wizard-steps"> <el-steps :active="currentStep" finish-status="success" align-center> <el-step title="检测版本" icon="Search" /> <el-step title="选择迁移路径" icon="Guide" /> <el-step title="预览更改" icon="View" /> <el-step title="执行迁移" icon="Upload" /> <el-step title="完成" icon="Check" /> </el-steps> </div> <div class="wizard-content"> <!-- 步骤 1: 版本检测 --> <div v-if="currentStep === 0" class="step-content"> <div class="step-header"> <h3>配置版本检测</h3> <p>分析当前配置版本并检查可用的迁移路径</p> </div> <div class="detection-section"> <div class="current-version-card"> <el-card shadow="never"> <template #header> <div class="card-header"> <span>当前版本信息</span> <el-button size="small" @click="detectVersion" :loading="detecting" > 重新检测 </el-button> </div> </template> <div v-if="currentVersionInfo" class="version-info"> <div class="info-row"> <span class="label">版本号:</span> <el-tag :type="getVersionTypeColor(currentVersionInfo.version)" size="large" > {{ currentVersionInfo.version }} </el-tag> </div> <div class="info-row"> <span class="label">格式:</span> <span class="value">{{ currentVersionInfo.format }}</span> </div> <div class="info-row"> <span class="label">特性:</span> <div class="features"> <el-tag v-for="feature in currentVersionInfo.features" :key="feature" size="small" class="feature-tag" > {{ feature }} </el-tag> </div> </div> <div class="info-row"> <span class="label">兼容性:</span> <div class="compatibility"> <el-tag :type=" currentVersionInfo.deprecated ? 'danger' : 'success' " size="small" > {{ currentVersionInfo.deprecated ? "已弃用" : "支持中" }} </el-tag> </div> </div> </div> <div v-else class="no-version"> <el-empty description="未检测到版本信息" /> </div> </el-card> </div> <div class="available-versions"> <h4>可用迁移版本</h4> <div class="versions-grid"> <div v-for="version in availableVersions" :key="version.version" class="version-card" :class="{ recommended: version.recommended, selected: selectedTargetVersion === version.version, }" @click="selectTargetVersion(version.version)" > <div class="version-header"> <div class="version-title"> <span class="version-number">{{ version.version }}</span> <el-tag v-if="version.recommended" type="success" size="small" > 推荐 </el-tag> </div> <div class="version-status"> <el-icon v-if="selectedTargetVersion === version.version" color="var(--el-color-primary)" > <Check /> </el-icon> </div> </div> <div class="version-description"> {{ version.description }} </div> <div class="version-features"> <div class="new-features"> <span class="features-label">新特性:</span> <ul class="features-list"> <li v-for="feature in version.newFeatures" :key="feature"> {{ feature }} </li> </ul> </div> <div v-if=" version.breakingChanges && version.breakingChanges.length > 0 " class="breaking-changes" > <span class="features-label">重大变更:</span> <ul class="features-list warning"> <li v-for="change in version.breakingChanges" :key="change" > {{ change }} </li> </ul> </div> </div> </div> </div> </div> </div> </div> <!-- 步骤 2: 迁移路径选择 --> <div v-if="currentStep === 1" class="step-content"> <div class="step-header"> <h3>选择迁移路径</h3> <p> 规划从 {{ currentVersionInfo?.version }} 到 {{ selectedTargetVersion }} 的迁移步骤 </p> </div> <div class="migration-path-section"> <div class="path-overview"> <el-card shadow="never"> <template #header> <span>迁移路径概览</span> </template> <div class="path-flow"> <div class="path-node current"> <div class="node-content"> <div class="node-version"> {{ currentVersionInfo?.version }} </div> <div class="node-label">当前版本</div> </div> </div> <div v-for="(step, index) in migrationSteps" :key="step.id" class="path-arrow-and-node" > <div class="path-arrow"> <el-icon><ArrowRight /></el-icon> </div> <div class="path-node step"> <div class="node-content"> <div class="node-version">{{ step.targetVersion }}</div> <div class="node-label">步骤 {{ index + 1 }}</div> </div> </div> </div> </div> </el-card> </div> <div class="steps-details"> <h4>迁移步骤详情</h4> <div class="steps-list"> <div v-for="(step, index) in migrationSteps" :key="step.id" class="step-item" > <div class="step-header"> <div class="step-title"> <span class="step-number">{{ index + 1 }}</span> <span class="step-name">{{ step.title }}</span> <el-tag :type="getStepTypeColor(step.type || 'config')" size="small" > {{ getStepTypeLabel(step.type || "config") }} </el-tag> </div> <div class="step-duration"> <el-icon><Clock /></el-icon> <span>预计 {{ step.estimatedDuration }}</span> </div> </div> <div class="step-description"> {{ step.description }} </div> <div v-if="step.actions && step.actions.length > 0" class="step-actions" > <div class="actions-label">将执行的操作:</div> <ul class="actions-list"> <li v-for="action in step.actions" :key="action.id" class="action-item" > <el-icon :color="getActionIconColor(action.type)"> <component :is="getActionIcon(action.type)" /> </el-icon> <span>{{ action.description }}</span> </li> </ul> </div> <div v-if="step.risks && step.risks.length > 0" class="step-risks" > <el-alert title="注意事项" type="warning" :closable="false" show-icon > <ul class="risks-list"> <li v-for="risk in step.risks" :key="risk"> {{ risk }} </li> </ul> </el-alert> </div> </div> </div> </div> <div class="migration-options"> <el-card shadow="never"> <template #header> <span>迁移选项</span> </template> <div class="options-form"> <el-form :model="migrationOptions" label-width="120px"> <el-form-item label="备份策略"> <el-radio-group v-model="migrationOptions.backupStrategy"> <el-radio label="full">完整备份</el-radio> <el-radio label="incremental">增量备份</el-radio> <el-radio label="none">不备份</el-radio> </el-radio-group> </el-form-item> <el-form-item label="失败处理"> <el-radio-group v-model="migrationOptions.failureHandling"> <el-radio label="rollback">自动回滚</el-radio> <el-radio label="stop">停止执行</el-radio> <el-radio label="continue">继续执行</el-radio> </el-radio-group> </el-form-item> <el-form-item label="验证级别"> <el-select v-model="migrationOptions.validationLevel"> <el-option label="严格验证" value="strict" /> <el-option label="标准验证" value="standard" /> <el-option label="宽松验证" value="loose" /> </el-select> </el-form-item> <el-form-item label="并行执行"> <el-switch v-model="migrationOptions.parallelExecution" active-text="启用" inactive-text="禁用" /> </el-form-item> </el-form> </div> </el-card> </div> </div> </div> <!-- 步骤 3: 预览更改 --> <div v-if="currentStep === 2" class="step-content"> <div class="step-header"> <h3>预览迁移更改</h3> <p>查看迁移过程中将进行的所有更改</p> </div> <div class="preview-section"> <el-tabs v-model="previewTab" type="border-card"> <el-tab-pane label="配置对比" name="diff"> <div class="config-diff"> <div class="diff-header"> <div class="diff-section"> <h5>迁移前配置</h5> <el-tag type="info" size="small">{{ currentVersionInfo?.version }}</el-tag> </div> <div class="diff-section"> <h5>迁移后配置</h5> <el-tag type="success" size="small">{{ selectedTargetVersion }}</el-tag> </div> </div> <div class="diff-content"> <div class="config-before"> <JsonViewer :data="previewData.before" :expanded="2" :show-copy="false" /> </div> <div class="config-after"> <JsonViewer :data="previewData.after" :expanded="2" :show-copy="false" /> </div> </div> </div> </el-tab-pane> <el-tab-pane label="变更清单" name="changes"> <div class="changes-list"> <div v-for="change in previewData.changes" :key="change.id" class="change-item" > <div class="change-header"> <div class="change-type"> <el-icon :color="getChangeIconColor(change.type)"> <component :is="getChangeIcon(change.type)" /> </el-icon> <span class="change-operation">{{ getChangeTypeText(change.type) }}</span> </div> <div class="change-path">{{ change.path }}</div> </div> <div class="change-description"> {{ change.description }} </div> <div v-if="change.oldValue || change.newValue" class="change-values" > <div v-if="change.oldValue" class="old-value"> <span class="value-label">原值:</span> <code>{{ JSON.stringify(change.oldValue) }}</code> </div> <div v-if="change.newValue" class="new-value"> <span class="value-label">新值:</span> <code>{{ JSON.stringify(change.newValue) }}</code> </div> </div> </div> </div> </el-tab-pane> <el-tab-pane label="风险评估" name="risks"> <div class="risks-assessment"> <div class="risk-summary"> <el-card shadow="never"> <div class="summary-stats"> <div class="stat-item"> <div class="stat-value high-risk"> {{ riskSummary.high }} </div> <div class="stat-label">高风险</div> </div> <div class="stat-item"> <div class="stat-value medium-risk"> {{ riskSummary.medium }} </div> <div class="stat-label">中风险</div> </div> <div class="stat-item"> <div class="stat-value low-risk"> {{ riskSummary.low }} </div> <div class="stat-label">低风险</div> </div> </div> </el-card> </div> <div class="risks-list"> <div v-for="risk in previewData.risks" :key="risk.id" class="risk-item" > <el-alert :title="risk.title" :type="getRiskAlertType(risk.level)" :description="risk.description" show-icon :closable="false" > <template v-if="risk.mitigation" #default> <div class="risk-mitigation"> <strong>缓解措施:</strong> {{ risk.mitigation }} </div> </template> </el-alert> </div> </div> </div> </el-tab-pane> </el-tabs> </div> </div> <!-- 步骤 4: 执行迁移 --> <div v-if="currentStep === 3" class="step-content"> <div class="step-header"> <h3>执行迁移</h3> <p>正在执行配置迁移,请耐心等待...</p> </div> <div class="execution-section"> <div class="execution-progress"> <el-card shadow="never"> <template #header> <div class="progress-header"> <span>迁移进度</span> <span class="progress-text"> {{ executionProgress.current }} / {{ executionProgress.total }} </span> </div> </template> <div class="progress-content"> <el-progress :percentage="executionProgress.percentage" :status="executionProgress.status" :stroke-width="8" /> <div class="current-step"> <div class="step-info"> <strong>当前步骤:</strong> {{ executionProgress.currentStepTitle }} </div> <div class="step-description"> {{ executionProgress.currentStepDescription }} </div> </div> </div> </el-card> </div> <div class="execution-log"> <el-card shadow="never"> <template #header> <div class="log-header"> <span>执行日志</span> <el-button size="small" @click="clearLog">清空</el-button> </div> </template> <div class="log-content" ref="logContainer"> <div v-for="(log, index) in executionLogs" :key="index" class="log-entry" :class="log.level" > <span class="log-time">{{ formatTime(log.timestamp) }}</span> <span class="log-level">{{ log.level.toUpperCase() }}</span> <span class="log-message">{{ log.message }}</span> </div> </div> </el-card> </div> </div> </div> <!-- 步骤 5: 完成 --> <div v-if="currentStep === 4" class="step-content"> <div class="step-header"> <h3>迁移完成</h3> <p>配置迁移已成功完成</p> </div> <div class="completion-section"> <div class="completion-status"> <el-result icon="success" title="迁移成功" :sub-title="`配置已从 ${currentVersionInfo?.version} 成功迁移到 ${selectedTargetVersion}`" > <template #extra> <div class="completion-stats"> <div class="stat-item"> <div class="stat-value"> {{ completionStats.processedFiles }} </div> <div class="stat-label">处理文件</div> </div> <div class="stat-item"> <div class="stat-value"> {{ completionStats.appliedChanges }} </div> <div class="stat-label">应用更改</div> </div> <div class="stat-item"> <div class="stat-value">{{ completionStats.duration }}</div> <div class="stat-label">耗时</div> </div> </div> </template> </el-result> </div> <div class="post-migration-actions"> <el-card shadow="never"> <template #header> <span>后续操作</span> </template> <div class="actions-list"> <div class="action-item"> <el-button type="primary" @click="validateMigration"> 验证迁移结果 </el-button> <span class="action-description" >检查迁移后的配置是否正确</span > </div> <div class="action-item"> <el-button @click="exportMigrationReport"> 导出迁移报告 </el-button> <span class="action-description">生成详细的迁移报告</span> </div> <div class="action-item"> <el-button @click="restartServices"> 重启相关服务 </el-button> <span class="action-description">应用新配置并重启服务</span> </div> </div> </el-card> </div> </div> </div> </div> <!-- 向导控制按钮 --> <div class="wizard-controls"> <div class="controls-left"> <el-button v-if="currentStep > 0" @click="previousStep"> 上一步 </el-button> </div> <div class="controls-right"> <el-button @click="cancelMigration">取消</el-button> <el-button v-if="currentStep < 4" type="primary" @click="nextStep" :disabled="!canProceedToNext" :loading="processing" > {{ getNextButtonText() }} </el-button> <el-button v-else type="success" @click="finishMigration"> 完成 </el-button> </div> </div> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted, nextTick } from "vue"; import { ElMessage, ElMessageBox } from "element-plus"; import { Search, Guide, View, Upload, Check, ArrowRight, Clock, Setting, Plus, Edit, Delete, Warning, InfoFilled, } from "@element-plus/icons-vue"; import { useConfigStore } from "@/stores/config"; import type { ConfigVersionInfo, ConfigMigrationStep, ConfigMigrationOptions, } from "@/stores/config"; // 引入组件 import JsonViewer from "@/shared/components/ui/JsonViewer.vue"; interface MigrationChange { id: string; type: "add" | "modify" | "remove" | "rename"; path: string; description: string; oldValue?: any; newValue?: any; } interface MigrationRisk { id: string; level: "low" | "medium" | "high"; title: string; description: string; mitigation?: string; } interface PreviewData { before: any; after: any; changes: MigrationChange[]; risks: MigrationRisk[]; } interface ExecutionLog { timestamp: Date; level: "info" | "warn" | "error" | "success"; message: string; } interface ExecutionProgress { current: number; total: number; percentage: number; status: "normal" | "exception" | "success"; currentStepTitle: string; currentStepDescription: string; } interface CompletionStats { processedFiles: number; appliedChanges: number; duration: string; } interface Emits { (e: "completed", result: any): void; (e: "cancelled"): void; } const emit = defineEmits<Emits>(); // Store const configStore = useConfigStore(); // 状态 const currentStep = ref(0); const detecting = ref(false); const processing = ref(false); const previewTab = ref("diff"); // 版本信息 const currentVersionInfo = ref<ConfigVersionInfo | null>(null); const availableVersions = ref<ConfigVersionInfo[]>([]); const selectedTargetVersion = ref(""); // 迁移信息 const migrationSteps = ref<ConfigMigrationStep[]>([]); const migrationOptions = ref<ConfigMigrationOptions>({ backupStrategy: "full", failureHandling: "rollback", validationLevel: "standard", parallelExecution: false, }); // 预览数据 const previewData = ref<PreviewData>({ before: {}, after: {}, changes: [], risks: [], }); // 执行状态 const executionProgress = ref<ExecutionProgress>({ current: 0, total: 0, percentage: 0, status: "normal", currentStepTitle: "", currentStepDescription: "", }); const executionLogs = ref<ExecutionLog[]>([]); const logContainer = ref<HTMLElement>(); // 完成统计 const completionStats = ref<CompletionStats>({ processedFiles: 0, appliedChanges: 0, duration: "0s", }); // 计算属性 const canProceedToNext = computed(() => { switch (currentStep.value) { case 0: return currentVersionInfo.value && selectedTargetVersion.value; case 1: return migrationSteps.value.length > 0; case 2: return true; case 3: return executionProgress.value.status === "success"; default: return false; } }); const riskSummary = computed(() => { const risks = previewData.value.risks; return { high: risks.filter((r) => r.level === "high").length, medium: risks.filter((r) => r.level === "medium").length, low: risks.filter((r) => r.level === "low").length, }; }); // 方法 const detectVersion = async () => { detecting.value = true; try { // 模拟检测当前版本 currentVersionInfo.value = { version: "1.0.0", format: "JSON", compatibleVersions: ["1.0.0", "1.1.0"], description: "当前版本", features: ["基础功能", "配置管理"], deprecated: false, }; // 模拟获取可用版本 availableVersions.value = [ { version: "1.1.0", format: "JSON", compatibleVersions: ["1.0.0", "1.1.0", "1.2.0"], description: "增强版本", features: ["批量操作", "配置模板"], newFeatures: ["批量操作", "配置模板"], deprecated: false, recommended: true, breakingChanges: [], }, { version: "2.0.0", format: "JSON", compatibleVersions: ["1.2.0", "2.0.0", "2.1.0"], description: "重大更新版本", features: ["新架构", "WebSocket支持"], newFeatures: ["新架构", "WebSocket支持"], deprecated: false, recommended: false, breakingChanges: ["API架构变更", "配置格式更新"], }, ]; // 自动选择推荐版本 const recommended = availableVersions.value.find((v) => v.recommended); if (recommended) { selectedTargetVersion.value = recommended.version; } ElMessage.success("版本检测完成"); } catch (error) { ElMessage.error( "版本检测失败: " + (error instanceof Error ? error.message : "未知错误"), ); } finally { detecting.value = false; } }; const selectTargetVersion = (version: string) => { selectedTargetVersion.value = version; }; const nextStep = async () => { processing.value = true; try { switch (currentStep.value) { case 0: await planMigration(); break; case 1: await generatePreview(); break; case 2: await executeMigration(); break; case 3: // 执行完成,等待用户操作 break; } currentStep.value++; } catch (error) { ElMessage.error( "操作失败: " + (error instanceof Error ? error.message : "未知错误"), ); } finally { processing.value = false; } }; const previousStep = () => { if (currentStep.value > 0) { currentStep.value--; } }; const planMigration = async () => { if (!currentVersionInfo.value || !selectedTargetVersion.value) { throw new Error("缺少版本信息"); } // 模拟生成迁移步骤 migrationSteps.value = [ { id: "1", name: "更新配置架构", title: "更新配置架构", description: "将配置文件架构从旧版本升级到新版本", fromVersion: currentVersionInfo.value.version, toVersion: selectedTargetVersion.value, type: "schema", estimatedDuration: "2分钟", required: true, executed: false, }, { id: "2", name: "迁移数据结构", title: "迁移数据结构", description: "迁移现有数据到新的数据结构", fromVersion: currentVersionInfo.value.version, toVersion: selectedTargetVersion.value, type: "data", estimatedDuration: "3分钟", required: true, executed: false, }, ]; }; const generatePreview = async () => { if (!currentVersionInfo.value || !selectedTargetVersion.value) { throw new Error("缺少版本信息"); } // 模拟预览数据 const mockPreview = { currentConfig: { version: currentVersionInfo.value.version, settings: { theme: "light", language: "zh-CN", }, }, targetConfig: { version: selectedTargetVersion.value, settings: { theme: "light", language: "zh-CN", newFeature: true, }, }, changes: [ { id: "change1", type: "add", path: "/settings/newFeature", description: "添加新功能配置项", oldValue: undefined, newValue: true, }, { id: "change2", type: "modify", path: "/version", description: "更新版本号", oldValue: currentVersionInfo.value.version, newValue: selectedTargetVersion.value, }, ], risks: [ { id: "risk1", level: "medium", title: "配置兼容性风险", description: "新版本配置可能与旧版本不完全兼容", mitigation: "建议在迁移前备份当前配置", }, { id: "risk2", level: "low", title: "功能变更风险", description: "部分功能可能发生变化", mitigation: "请查阅版本更新说明", }, ], }; previewData.value = { before: mockPreview.currentConfig, after: mockPreview.targetConfig, changes: mockPreview.changes.map((change) => ({ id: change.id, type: change.type as any, path: change.path, description: change.description, oldValue: change.oldValue, newValue: change.newValue, })), risks: mockPreview.risks.map((risk: any) => ({ id: risk.id, level: risk.level as any, title: risk.title, description: risk.description, mitigation: risk.mitigation, })), }; }; const executeMigration = async () => { if (!currentVersionInfo.value || !selectedTargetVersion.value) { throw new Error("缺少版本信息"); } const startTime = Date.now(); // 重置执行状态 executionProgress.value = { current: 0, total: migrationSteps.value.length, percentage: 0, status: "normal", currentStepTitle: "", currentStepDescription: "", }; executionLogs.value = []; addLog("info", "开始配置迁移..."); try { for (let i = 0; i < migrationSteps.value.length; i++) { const step = migrationSteps.value[i]; executionProgress.value.current = i + 1; executionProgress.value.currentStepTitle = step.title || ""; executionProgress.value.currentStepDescription = step.description || ""; executionProgress.value.percentage = Math.round( ((i + 1) / migrationSteps.value.length) * 100, ); addLog("info", `执行步骤 ${i + 1}: ${step.title}`); // 模拟执行迁移步骤 await new Promise((resolve) => setTimeout(resolve, 1000)); addLog("success", `步骤 ${i + 1} 完成`); // 模拟进度延迟 await new Promise((resolve) => setTimeout(resolve, 500)); } executionProgress.value.status = "success"; addLog("success", "配置迁移完成"); // 更新完成统计 const endTime = Date.now(); completionStats.value = { processedFiles: migrationSteps.value.length, appliedChanges: previewData.value.changes.length, duration: formatDuration(endTime - startTime), }; } catch (error) { executionProgress.value.status = "exception"; addLog( "error", "迁移失败: " + (error instanceof Error ? error.message : "未知错误"), ); throw error; } }; const addLog = (level: ExecutionLog["level"], message: string) => { executionLogs.value.push({ timestamp: new Date(), level, message, }); // 自动滚动到底部 nextTick(() => { if (logContainer.value) { logContainer.value.scrollTop = logContainer.value.scrollHeight; } }); }; const clearLog = () => { executionLogs.value = []; }; const validateMigration = async () => { try { // 简单的配置验证逻辑 if (migrationSteps.value.length > 0 && selectedTargetVersion.value) { ElMessage.success("配置验证通过"); } else { ElMessage.warning("配置验证失败: 缺少必要的迁移信息"); } } catch (error) { ElMessage.error( "验证失败: " + (error instanceof Error ? error.message : "未知错误"), ); } }; const exportMigrationReport = () => { const report = { migration: { fromVersion: currentVersionInfo.value?.version, toVersion: selectedTargetVersion.value, timestamp: new Date().toISOString(), options: migrationOptions.value, }, steps: migrationSteps.value, changes: previewData.value.changes, risks: previewData.value.risks, execution: { logs: executionLogs.value, stats: completionStats.value, }, }; const blob = new Blob([JSON.stringify(report, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `migration-report-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); ElMessage.success("迁移报告已导出"); }; const restartServices = async () => { try { await ElMessageBox.confirm( "重启服务将短暂中断服务,确定要继续吗?", "确认重启", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }, ); // 模拟服务重启 await new Promise((resolve) => setTimeout(resolve, 2000)); ElMessage.success("服务重启成功"); } catch (error) { if (error !== "cancel") { ElMessage.error( "服务重启失败: " + (error instanceof Error ? error.message : "未知错误"), ); } } }; const cancelMigration = async () => { try { await ElMessageBox.confirm( "确定要取消迁移吗?已进行的操作将保留。", "确认取消", { confirmButtonText: "确定", cancelButtonText: "继续迁移", type: "warning", }, ); emit("cancelled"); } catch { // 用户选择继续迁移 } }; const finishMigration = () => { emit("completed", { fromVersion: currentVersionInfo.value?.version, toVersion: selectedTargetVersion.value, stats: completionStats.value, }); }; // 辅助方法 const getNextButtonText = (): string => { const texts = ["开始规划", "生成预览", "执行迁移", "完成"]; return texts[currentStep.value] || "下一步"; }; const getVersionTypeColor = (version: string): string => { // 根据版本号返回不同颜色 if (version.includes("beta")) return "warning"; if (version.includes("alpha")) return "danger"; return "primary"; }; const getStepTypeColor = (type: string): string => { const colors: Record<string, string> = { schema: "primary", data: "success", config: "warning", validation: "info", }; return colors[type] || "default"; }; const getStepTypeLabel = (type: string): string => { const labels: Record<string, string> = { schema: "架构升级", data: "数据迁移", config: "配置更新", validation: "验证检查", }; return labels[type] || type; }; const getActionIcon = (type: string): any => { const icons: Record<string, any> = { add: Plus, modify: Edit, remove: Delete, rename: Edit, }; return icons[type] || Setting; }; const getActionIconColor = (type: string): string => { const colors: Record<string, string> = { add: "var(--el-color-success)", modify: "var(--el-color-warning)", remove: "var(--el-color-danger)", rename: "var(--el-color-primary)", }; return colors[type] || "var(--el-text-color-regular)"; }; const getChangeIcon = (type: string): any => { return getActionIcon(type); }; const getChangeIconColor = (type: string): string => { return getActionIconColor(type); }; const getChangeTypeText = (type: string): string => { const texts: Record<string, string> = { add: "新增", modify: "修改", remove: "删除", rename: "重命名", }; return texts[type] || type; }; const getRiskAlertType = (level: string): any => { const types: Record<string, any> = { low: "info", medium: "warning", high: "error", }; return types[level] || "info"; }; const formatTime = (date: Date): string => { return date.toLocaleTimeString(); }; const formatDuration = (ms: number): string => { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } }; // 初始化 onMounted(() => { detectVersion(); }); </script> <style scoped> .migration-wizard { padding: 20px; max-height: 85vh; overflow-y: auto; } .wizard-steps { margin-bottom: 32px; } .wizard-content { min-height: 500px; margin-bottom: 24px; } .step-content { animation: fadeInUp 0.3s ease-out; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .step-header { text-align: center; margin-bottom: 32px; } .step-header h3 { margin: 0 0 8px 0; color: var(--el-text-color-primary); font-size: 24px; } .step-header p { margin: 0; color: var(--el-text-color-secondary); font-size: 14px; } /* 版本检测样式 */ .detection-section { max-width: 1200px; margin: 0 auto; } .current-version-card { margin-bottom: 24px; } .card-header { display: flex; justify-content: space-between; align-items: center; } .version-info { display: flex; flex-direction: column; gap: 12px; } .info-row { display: flex; align-items: center; gap: 12px; } .label { font-weight: 600; color: var(--el-text-color-primary); min-width: 80px; } .value { color: var(--el-text-color-regular); } .features, .compatibility { display: flex; gap: 8px; flex-wrap: wrap; } .feature-tag { margin: 0; } .available-versions h4 { margin: 0 0 16px 0; color: var(--el-text-color-primary); } .versions-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; } .version-card { border: 1px solid var(--el-border-color); border-radius: 8px; padding: 16px; cursor: pointer; transition: all 0.3s ease; } .version-card:hover { border-color: var(--el-color-primary); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .version-card.recommended { border-color: var(--el-color-success); background: var(--el-color-success-light-9); } .version-card.selected { border-color: var(--el-color-primary); background: var(--el-color-primary-light-9); } .version-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .version-title { display: flex; align-items: center; gap: 8px; } .version-number { font-weight: 600; color: var(--el-text-color-primary); font-size: 16px; } .version-description { color: var(--el-text-color-secondary); font-size: 13px; margin-bottom: 12px; } .version-features { font-size: 12px; } .new-features, .breaking-changes { margin-bottom: 8px; } .features-label { font-weight: 600; color: var(--el-text-color-primary); } .features-list { margin: 4px 0 0 16px; padding: 0; } .features-list.warning { color: var(--el-color-warning); } /* 迁移路径样式 */ .migration-path-section { max-width: 1200px; margin: 0 auto; } .path-overview { margin-bottom: 24px; } .path-flow { display: flex; align-items: center; gap: 16px; padding: 20px; overflow-x: auto; } .path-node { display: flex; flex-direction: column; align-items: center; min-width: 120px; } .path-node.current .node-content { background: var(--el-color-primary); color: white; } .path-node.step .node-content { background: var(--el-color-info-light-3); color: var(--el-text-color-primary); } .node-content { display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 8px; border: 2px solid var(--el-border-color); } .node-version { font-weight: 600; font-size: 14px; margin-bottom: 4px; } .node-label { font-size: 12px; opacity: 0.8; } .path-arrow-and-node { display: flex; align-items: center; gap: 16px; } .path-arrow { color: var(--el-color-primary); font-size: 18px; } .steps-details h4 { margin: 0 0 16px 0; color: var(--el-text-color-primary); } .steps-list { display: flex; flex-direction: column; gap: 16px; } .step-item { border: 1px solid var(--el-border-color); border-radius: 8px; padding: 16px; } .step-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .step-title { display: flex; align-items: center; gap: 12px; } .step-number { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; background: var(--el-color-primary); color: white; border-radius: 50%; font-size: 12px; font-weight: 600; } .step-name { font-weight: 600; color: var(--el-text-color-primary); } .step-duration { display: flex; align-items: center; gap: 4px; color: var(--el-text-color-secondary); font-size: 13px; } .step-description { color: var(--el-text-color-secondary); margin-bottom: 12px; } .step-actions { margin-bottom: 12px; } .actions-label { font-weight: 600; color: var(--el-text-color-primary); margin-bottom: 8px; } .actions-list { list-style: none; padding: 0; margin: 0; } .action-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 13px; color: var(--el-text-color-regular); } .step-risks { margin-top: 12px; } .risks-list { list-style: none; padding: 0; margin: 8px 0 0 0; } .migration-options { margin-top: 24px; } .options-form { max-width: 600px; } /* 预览样式 */ .preview-section { max-width: 1200px; margin: 0 auto; } .config-diff { padding: 16px 0; } .diff-header { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 16px; } .diff-section { display: flex; align-items: center; gap: 8px; } .diff-section h5 { margin: 0; color: var(--el-text-color-primary); } .diff-content { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } .config-before, .config-after { border: 1px solid var(--el-border-color); border-radius: 6px; padding: 12px; background: var(--el-bg-color-page); max-height: 400px; overflow-y: auto; } .changes-list { padding: 16px 0; } .change-item { border: 1px solid var(--el-border-color); border-radius: 6px; padding: 16px; margin-bottom: 12px; } .change-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .change-type { display: flex; align-items: center; gap: 8px; } .change-operation { font-weight: 600; color: var(--el-text-color-primary); } .change-path { font-family: monospace; color: var(--el-text-color-secondary); font-size: 13px; } .change-description { color: var(--el-text-color-regular); margin-bottom: 8px; } .change-values { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; } .old-value, .new-value { display: flex; align-items: center; gap: 8px; } .value-label { font-weight: 600; color: var(--el-text-color-primary); min-width: 40px; } .old-value code { background: var(--el-color-danger-light-9); color: var(--el-color-danger); } .new-value code { background: var(--el-color-success-light-9); color: var(--el-color-success); } code { padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 12px; } .risks-assessment { padding: 16px 0; } .risk-summary { margin-bottom: 24px; } .summary-stats { display: flex; justify-content: center; gap: 32px; } .stat-item { text-align: center; } .stat-value { font-size: 32px; font-weight: 600; margin-bottom: 4px; } .stat-value.high-risk { color: var(--el-color-danger); } .stat-value.medium-risk { color: var(--el-color-warning); } .stat-value.low-risk { color: var(--el-color-success); } .stat-label { color: var(--el-text-color-secondary); font-size: 14px; } .risks-list { display: flex; flex-direction: column; gap: 12px; } .risk-item { margin-bottom: 8px; } .risk-mitigation { margin-top: 8px; padding: 8px; background: var(--el-bg-color-page); border-radius: 4px; font-size: 13px; } /* 执行样式 */ .execution-section { max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; } .execution-progress { width: 100%; } .progress-header { display: flex; justify-content: space-between; align-items: center; } .progress-text { color: var(--el-text-color-secondary); font-size: 14px; } .progress-content { display: flex; flex-direction: column; gap: 16px; } .current-step { padding: 16px; background: var(--el-bg-color-page); border-radius: 6px; } .step-info { margin-bottom: 8px; color: var(--el-text-color-primary); } .step-description { color: var(--el-text-color-secondary); font-size: 14px; } .execution-log { width: 100%; } .log-header { display: flex; justify-content: space-between; align-items: center; } .log-content { max-height: 300px; overflow-y: auto; font-family: monospace; font-size: 13px; line-height: 1.5; } .log-entry { display: flex; gap: 12px; padding: 4px 8px; border-bottom: 1px solid var(--el-border-color-lighter); } .log-entry:last-child { border-bottom: none; } .log-entry.info { color: var(--el-text-color-regular); } .log-entry.warn { color: var(--el-color-warning); } .log-entry.error { color: var(--el-color-danger); } .log-entry.success { color: var(--el-color-success); } .log-time { color: var(--el-text-color-secondary); min-width: 80px; } .log-level { font-weight: 600; min-width: 60px; } .log-message { flex: 1; } /* 完成样式 */ .completion-section { max-width: 800px; margin: 0 auto; } .completion-status { margin-bottom: 32px; } .completion-stats { display: flex; justify-content: center; gap: 32px; margin-top: 24px; } .post-migration-actions .actions-list { display: flex; flex-direction: column; gap: 16px; } .post-migration-actions .action-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid var(--el-border-color); border-radius: 6px; } .action-description { color: var(--el-text-color-secondary); font-size: 14px; } /* 控制按钮 */ .wizard-controls { display: flex; justify-content: space-between; align-items: center; padding-top: 24px; border-top: 1px solid var(--el-border-color); } .controls-left, .controls-right { display: flex; gap: 12px; } </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