第277集:Vue 3低代码平台版本管理与回滚

一、版本管理概述

在低代码平台中,版本管理是一项至关重要的功能,它允许用户保存、管理和回滚应用的不同版本。通过版本管理,用户可以:

  • 跟踪应用的演进历史
  • 备份重要的应用状态
  • 在出现问题时快速回滚到之前的稳定版本
  • 支持团队协作开发
  • 实现A/B测试和灰度发布

1.1 核心功能

  • 版本创建:保存应用的当前状态为一个版本
  • 版本列表:查看所有保存的版本
  • 版本详情:查看版本的详细信息和变更内容
  • 版本回滚:将应用恢复到指定版本
  • 版本比较:比较不同版本之间的差异
  • 版本删除:删除不需要的版本
  • 版本发布:将版本发布到不同环境

1.2 架构设计

版本管理系统通常采用以下架构设计:

  • 表现层:提供可视化的版本管理界面
  • 核心服务层:处理版本的创建、查询、回滚等核心逻辑
  • 存储层:保存版本数据和应用状态
  • 集成层:与其他系统(如Git、CI/CD)集成

二、核心实现

2.1 数据模型设计

首先,我们需要设计版本管理的数据模型:

// 版本状态枚举
export enum VersionStatus {
  DRAFT = 'draft',      // 草稿
  PUBLISHED = 'published', // 已发布
  ARCHIVED = 'archived'   // 已归档
}

// 版本信息接口
export interface Version {
  id: string;
  appId: string;        // 应用ID
  name: string;         // 版本名称
  description?: string; // 版本描述
  versionNumber: string; // 版本号(如:1.0.0)
  status: VersionStatus; // 版本状态
  createdAt: Date;      // 创建时间
  createdBy: string;    // 创建人
  updatedAt: Date;      // 更新时间
  snapshot: AppSnapshot; // 应用快照
  changeLog?: string;   // 变更日志
  tags?: string[];      // 标签
}

// 应用快照接口
export interface AppSnapshot {
  pages: PageSnapshot[]; // 页面快照列表
  components: ComponentSnapshot[]; // 组件快照列表
  dataSources: DataSourceSnapshot[]; // 数据源快照列表
  config: AppConfig;     // 应用配置
  dependencies?: Record<string, string>; // 依赖信息
}

// 页面快照接口
export interface PageSnapshot {
  id: string;
  name: string;
  path: string;
  structure: any;       // 页面结构
  config: any;          // 页面配置
}

// 组件快照接口
export interface ComponentSnapshot {
  id: string;
  name: string;
  type: string;
  config: any;          // 组件配置
}

// 数据源快照接口
export interface DataSourceSnapshot {
  id: string;
  name: string;
  type: string;
  config: any;          // 数据源配置
}

// 应用配置接口
export interface AppConfig {
  theme: string;
  layout: string;
  router: any;
  settings: any;
}

// 版本比较结果接口
export interface VersionDiff {
  added: any[];
  modified: any[];
  deleted: any[];
}

2.2 版本管理服务

实现版本管理的核心服务,包括版本创建、查询、回滚等功能:

import type { Version, VersionStatus, AppSnapshot, VersionDiff } from './types';

// 版本管理服务
export class VersionManager {
  private versions: Map<string, Version[]> = new Map();
  private storageKey = 'lowcode_versions';
  
  constructor() {
    this.loadFromStorage();
  }
  
  // 创建版本
  createVersion(appId: string, name: string, description: string, snapshot: AppSnapshot): Version {
    const appVersions = this.versions.get(appId) || [];
    const versionNumber = this.generateVersionNumber(appVersions);
    
    const newVersion: Version = {
      id: `version_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
      appId,
      name,
      description,
      versionNumber,
      status: VersionStatus.DRAFT,
      createdAt: new Date(),
      createdBy: 'admin',
      updatedAt: new Date(),
      snapshot
    };
    
    appVersions.push(newVersion);
    this.versions.set(appId, appVersions);
    this.saveToStorage();
    
    return newVersion;
  }
  
  // 获取应用的所有版本
  getVersions(appId: string): Version[] {
    return this.versions.get(appId) || [];
  }
  
  // 获取指定版本
  getVersion(versionId: string): Version | undefined {
    for (const appVersions of this.versions.values()) {
      const version = appVersions.find(v => v.id === versionId);
      if (version) return version;
    }
    return undefined;
  }
  
  // 删除版本
  deleteVersion(versionId: string): boolean {
    for (const [appId, appVersions] of this.versions.entries()) {
      const index = appVersions.findIndex(v => v.id === versionId);
      if (index >= 0) {
        appVersions.splice(index, 1);
        this.versions.set(appId, appVersions);
        this.saveToStorage();
        return true;
      }
    }
    return false;
  }
  
  // 更新版本状态
  updateVersionStatus(versionId: string, status: VersionStatus): boolean {
    const version = this.getVersion(versionId);
    if (version) {
      version.status = status;
      version.updatedAt = new Date();
      this.saveToStorage();
      return true;
    }
    return false;
  }
  
  // 比较两个版本
  compareVersions(version1: Version, version2: Version): VersionDiff {
    // 实现版本比较逻辑
    return {
      added: [],
      modified: [],
      deleted: []
    };
  }
  
  // 生成版本号
  private generateVersionNumber(existingVersions: Version[]): string {
    if (existingVersions.length === 0) {
      return '1.0.0';
    }
    
    // 按版本号排序,取最大的版本号
    const sortedVersions = [...existingVersions].sort((a, b) => {
      const aParts = a.versionNumber.split('.').map(Number);
      const bParts = b.versionNumber.split('.').map(Number);
      
      for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
        const aVal = aParts[i] || 0;
        const bVal = bParts[i] || 0;
        if (aVal !== bVal) {
          return bVal - aVal;
        }
      }
      return 0;
    });
    
    // 递增版本号(简单实现:只递增修订号)
    const latestParts = sortedVersions[0].versionNumber.split('.').map(Number);
    latestParts[2] = (latestParts[2] || 0) + 1;
    return latestParts.join('.');
  }
  
  // 从本地存储加载版本数据
  private loadFromStorage(): void {
    const savedData = localStorage.getItem(this.storageKey);
    if (savedData) {
      try {
        const data = JSON.parse(savedData);
        // 转换日期字符串为Date对象
        for (const [appId, appVersions] of Object.entries(data)) {
          const versions = (appVersions as any[]).map(v => ({
            ...v,
            createdAt: new Date(v.createdAt),
            updatedAt: new Date(v.updatedAt)
          }));
          this.versions.set(appId, versions as Version[]);
        }
      } catch (e) {
        console.error('Failed to load versions from storage:', e);
      }
    }
  }
  
  // 保存版本数据到本地存储
  private saveToStorage(): void {
    localStorage.setItem(this.storageKey, JSON.stringify(Object.fromEntries(this.versions)));
  }
}

2.3 版本管理组件

实现版本管理的可视化组件,包括版本列表、版本创建、版本详情等:

<template>
  <div class="version-manager">
    <div class="manager-header">
      <h2>版本管理</h2>
      <button class="create-btn" @click="showCreateModal = true">创建新版本</button>
    </div>
    
    <!-- 版本列表 -->
    <div class="version-list">
      <div 
        v-for="version in versions" 
        :key="version.id"
        class="version-item"
        :class="{ 'active': selectedVersion?.id === version.id }"
        @click="selectVersion(version)"
      >
        <div class="version-info">
          <div class="version-header">
            <div class="version-name">{{ version.name }}</div>
            <div class="version-number">{{ version.versionNumber }}</div>
          </div>
          <div class="version-meta">
            <span class="status-badge" :class="version.status">{{ getStatusText(version.status) }}</span>
            <span class="created-at">{{ formatDate(version.createdAt) }}</span>
            <span class="created-by">创建人:{{ version.createdBy }}</span>
          </div>
          <div class="version-description">{{ version.description || '无描述' }}</div>
        </div>
        <div class="version-actions">
          <button class="action-btn view-btn" @click.stop="viewVersion(version)">查看</button>
          <button class="action-btn rollback-btn" @click.stop="rollbackVersion(version)">回滚</button>
          <button class="action-btn delete-btn" @click.stop="deleteVersion(version)">删除</button>
        </div>
      </div>
      
      <!-- 空状态 -->
      <div class="empty-state" v-if="versions.length === 0">
        <p>暂无版本,请点击"创建新版本"按钮创建第一个版本</p>
      </div>
    </div>
    
    <!-- 版本详情 -->
    <div class="version-detail" v-if="selectedVersion">
      <div class="detail-header">
        <h3>{{ selectedVersion.name }} ({{ selectedVersion.versionNumber }})</h3>
        <button class="close-btn" @click="selectedVersion = null">&times;</button>
      </div>
      <div class="detail-content">
        <div class="detail-section">
          <h4>基本信息</h4>
          <div class="info-item">
            <label>版本号:</label>
            <span>{{ selectedVersion.versionNumber }}</span>
          </div>
          <div class="info-item">
            <label>状态:</label>
            <span class="status-badge" :class="selectedVersion.status">{{ getStatusText(selectedVersion.status) }}</span>
          </div>
          <div class="info-item">
            <label>创建时间:</label>
            <span>{{ formatDate(selectedVersion.createdAt) }}</span>
          </div>
          <div class="info-item">
            <label>创建人:</label>
            <span>{{ selectedVersion.createdBy }}</span>
          </div>
          <div class="info-item">
            <label>更新时间:</label>
            <span>{{ formatDate(selectedVersion.updatedAt) }}</span>
          </div>
        </div>
        
        <div class="detail-section">
          <h4>版本描述</h4>
          <div class="version-description-full">{{ selectedVersion.description || '无描述' }}</div>
        </div>
        
        <div class="detail-section">
          <h4>变更日志</h4>
          <div class="change-log">{{ selectedVersion.changeLog || '无变更日志' }}</div>
        </div>
        
        <div class="detail-section">
          <h4>版本内容</h4>
          <div class="version-content">
            <div class="content-item">
              <strong>页面数量:</strong>{{ selectedVersion.snapshot.pages.length }}
            </div>
            <div class="content-item">
              <strong>组件数量:</strong>{{ selectedVersion.snapshot.components.length }}
            </div>
            <div class="content-item">
              <strong>数据源数量:</strong>{{ selectedVersion.snapshot.dataSources.length }}
            </div>
          </div>
        </div>
        
        <div class="detail-actions">
          <button class="primary-btn" @click="rollbackVersion(selectedVersion)">回滚到此版本</button>
          <button class="secondary-btn" @click="publishVersion(selectedVersion)">发布此版本</button>
          <button class="secondary-btn" @click="archiveVersion(selectedVersion)">归档此版本</button>
        </div>
      </div>
    </div>
    
    <!-- 创建版本模态框 -->
    <div class="modal" v-if="showCreateModal">
      <div class="modal-overlay" @click="showCreateModal = false"></div>
      <div class="modal-content">
        <div class="modal-header">
          <h3>创建新版本</h3>
          <button class="close-btn" @click="showCreateModal = false">&times;</button>
        </div>
        <div class="modal-body">
          <form @submit.prevent="createVersion">
            <div class="form-group">
              <label>版本名称 *</label>
              <input 
                type="text" 
                v-model="newVersion.name" 
                required 
                placeholder="请输入版本名称"
              />
            </div>
            
            <div class="form-group">
              <label>版本描述</label>
              <textarea 
                v-model="newVersion.description" 
                placeholder="请输入版本描述"
                rows="3"
              ></textarea>
            </div>
            
            <div class="form-group">
              <label>变更日志</label>
              <textarea 
                v-model="newVersion.changeLog" 
                placeholder="请输入变更内容"
                rows="4"
              ></textarea>
            </div>
            
            <div class="form-group">
              <label>标签</label>
              <input 
                type="text" 
                v-model="newVersion.tagsInput" 
                placeholder="请输入标签,用逗号分隔"
              />
            </div>
            
            <div class="form-actions">
              <button type="button" class="cancel-btn" @click="showCreateModal = false">取消</button>
              <button type="submit" class="save-btn" :disabled="isCreating">
                {{ isCreating ? '创建中...' : '创建版本' }}
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import type { Version, VersionStatus } from './types';
import { VersionManager } from './services/VersionManager';
import { VersionStatus as VersionStatusEnum } from './types';

// 版本管理器实例
const versionManager = new VersionManager();

// 当前应用ID(实际项目中应该从路由或状态管理获取)
const appId = ref('app_123');

// 版本列表
const versions = ref<Version[]>([]);
// 选中的版本
const selectedVersion = ref<Version | null>(null);
// 显示创建版本模态框
const showCreateModal = ref(false);
// 是否正在创建版本
const isCreating = ref(false);

// 新版本表单数据
const newVersion = reactive({
  name: '',
description: '',
  changeLog: '',
  tagsInput: ''
});

// 初始化
function init() {
  versions.value = versionManager.getVersions(appId.value);
}

// 格式化日期
function formatDate(date: Date): string {
  return new Date(date).toLocaleString('zh-CN');
}

// 获取状态文本
function getStatusText(status: VersionStatus): string {
  const statusMap: Record<VersionStatus, string> = {
    [VersionStatusEnum.DRAFT]: '草稿',
    [VersionStatusEnum.PUBLISHED]: '已发布',
    [VersionStatusEnum.ARCHIVED]: '已归档'
  };
  return statusMap[status] || status;
}

// 选择版本
function selectVersion(version: Version) {
  selectedVersion.value = version;
}

// 查看版本
function viewVersion(version: Version) {
  selectedVersion.value = version;
}

// 创建版本
async function createVersion() {
  try {
    isCreating.value = true;
    
    // 生成标签数组
    const tags = newVersion.tagsInput
      ? newVersion.tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag)
      : [];
    
    // 模拟应用快照数据
    const mockSnapshot = {
      pages: [
        {
          id: 'page_1',
          name: '首页',
          path: '/',
          structure: {},
          config: {}
        }
      ],
      components: [
        {
          id: 'comp_1',
          name: 'Header',
          type: 'header',
          config: {}
        }
      ],
      dataSources: [],
      config: {
        theme: 'default',
        layout: 'vertical',
        router: {},
        settings: {}
      }
    };
    
    // 创建版本
    const createdVersion = versionManager.createVersion(
      appId.value,
      newVersion.name,
      newVersion.description,
      mockSnapshot
    );
    
    // 更新标签
    createdVersion.tags = tags;
    createdVersion.changeLog = newVersion.changeLog;
    
    // 刷新版本列表
    init();
    
    // 关闭模态框
    showCreateModal.value = false;
    
    // 重置表单
    Object.assign(newVersion, {
      name: '',
description: '',
      changeLog: '',
      tagsInput: ''
    });
    
    alert('版本创建成功!');
  } catch (error) {
    console.error('Failed to create version:', error);
    alert('版本创建失败!');
  } finally {
    isCreating.value = false;
  }
}

// 回滚版本
function rollbackVersion(version: Version) {
  if (confirm(`确定要回滚到版本"${version.name}"吗?当前的修改将会丢失。`)) {
    // 模拟回滚操作
    alert(`已回滚到版本:${version.name} (${version.versionNumber})`);
  }
}

// 发布版本
function publishVersion(version: Version) {
  versionManager.updateVersionStatus(version.id, VersionStatusEnum.PUBLISHED);
  init();
  alert(`版本"${version.name}"已发布!`);
}

// 归档版本
function archiveVersion(version: Version) {
  versionManager.updateVersionStatus(version.id, VersionStatusEnum.ARCHIVED);
  init();
  alert(`版本"${version.name}"已归档!`);
}

// 删除版本
function deleteVersion(version: Version) {
  if (confirm(`确定要删除版本"${version.name}"吗?此操作不可恢复。`)) {
    versionManager.deleteVersion(version.id);
    init();
    if (selectedVersion.value?.id === version.id) {
      selectedVersion.value = null;
    }
    alert(`版本"${version.name}"已删除!`);
  }
}

onMounted(() => {
  init();
});
</script>

<style scoped>
.version-manager {
  display: flex;
  flex-direction: column;
  height: 100%;
  font-family: Arial, sans-serif;
  background-color: #f5f5f5;
}

/* 头部样式 */
.manager-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  background-color: white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.manager-header h2 {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  color: #333;
}

.create-btn {
  padding: 8px 16px;
  background-color: #1976d2;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s ease;
}

.create-btn:hover {
  background-color: #1565c0;
}

/* 版本列表样式 */
.version-list {
  padding: 20px;
  overflow-y: auto;
  flex: 1;
}

.version-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background-color: white;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 12px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.version-item:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transform: translateY(-1px);
}

.version-item.active {
  border-color: #1976d2;
  box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}

.version-info {
  flex: 1;
  min-width: 0;
}

.version-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.version-name {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

.version-number {
  font-size: 14px;
  color: #1976d2;
  background-color: rgba(25, 118, 210, 0.1);
  padding: 2px 8px;
  border-radius: 12px;
}

.version-meta {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
  font-size: 12px;
  color: #666;
}

.status-badge {
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
}

.status-badge.draft {
  background-color: #fff3cd;
  color: #856404;
}

.status-badge.published {
  background-color: #d4edda;
  color: #155724;
}

.status-badge.archived {
  background-color: #e2e3e5;
  color: #383d41;
}

.version-description {
  font-size: 14px;
  color: #666;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.version-actions {
  display: flex;
  gap: 8px;
}

.action-btn {
  padding: 6px 12px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s ease;
}

.view-btn {
  background-color: #2196f3;
  color: white;
}

.view-btn:hover {
  background-color: #1976d2;
}

.rollback-btn {
  background-color: #ff9800;
  color: white;
}

.rollback-btn:hover {
  background-color: #f57c00;
}

.delete-btn {
  background-color: #f44336;
  color: white;
}

.delete-btn:hover {
  background-color: #d32f2f;
}

/* 空状态样式 */
.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: #999;
}

/* 版本详情样式 */
.version-detail {
  position: fixed;
  top: 0;
  right: 0;
  width: 400px;
  height: 100vh;
  background-color: white;
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
  overflow-y: auto;
  z-index: 100;
}

.detail-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  border-bottom: 1px solid #e0e0e0;
}

.detail-header h3 {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}

.close-btn {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #666;
  padding: 0;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  transition: all 0.3s ease;
}

.close-btn:hover {
  background-color: #f5f5f5;
  color: #333;
}

.detail-content {
  padding: 20px;
}

.detail-section {
  margin-bottom: 20px;
}

.detail-section h4 {
  margin: 0 0 12px 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

.info-item {
  display: flex;
  margin-bottom: 8px;
  font-size: 14px;
}

.info-item label {
  width: 80px;
  font-weight: 500;
  color: #666;
}

.info-item span {
  flex: 1;
  color: #333;
}

.version-description-full, .change-log {
  background-color: #fafafa;
  padding: 12px;
  border-radius: 4px;
  font-size: 14px;
  line-height: 1.5;
  color: #333;
}

.version-content {
  background-color: #fafafa;
  padding: 12px;
  border-radius: 4px;
}

.content-item {
  margin-bottom: 8px;
  font-size: 14px;
  color: #333;
}

.detail-actions {
  display: flex;
  gap: 12px;
  margin-top: 24px;
}

.primary-btn, .secondary-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.3s ease;
}

.primary-btn {
  background-color: #1976d2;
  color: white;
}

.primary-btn:hover {
  background-color: #1565c0;
}

.secondary-btn {
  background-color: #f5f5f5;
  color: #333;
  border: 1px solid #e0e0e0;
}

.secondary-btn:hover {
  background-color: #e0e0e0;
}

/* 模态框样式 */
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
}

.modal-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
}

.modal-content {
  position: relative;
  width: 500px;
  max-width: 90%;
  max-height: 90vh;
  overflow-y: auto;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}

.modal-body {
  padding: 20px;
}

/* 表单样式 */
.form-group {
  margin-bottom: 16px;
}

.form-group label {
  display: block;
  margin-bottom: 6px;
  font-weight: 500;
  color: #333;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  font-size: 14px;
  transition: all 0.3s ease;
}

.form-group input:focus,
.form-group textarea:focus {
  outline: none;
  border-color: #1976d2;
  box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 24px;
}

.cancel-btn, .save-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s ease;
}

.cancel-btn {
  background-color: #f5f5f5;
  color: #333;
}

.cancel-btn:hover {
  background-color: #e0e0e0;
}

.save-btn {
  background-color: #1976d2;
  color: white;
}

.save-btn:hover:not(:disabled) {
  background-color: #1565c0;
}

.save-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>

2.4 版本回滚组件

实现版本回滚功能,允许用户将应用恢复到指定版本:

<template>
  <div class="rollback-manager">
    <h3>版本回滚</h3>
    <p class="warning-text">警告:回滚操作会覆盖当前应用的所有修改,请谨慎操作!</p>
    
    <!-- 版本选择 -->
    <div class="version-selector">
      <label>选择要回滚的版本:</label>
      <select v-model="selectedVersionId" class="version-select">
        <option value="">请选择版本</option>
        <option 
          v-for="version in availableVersions" 
          :key="version.id" 
          :value="version.id"
        >
          {{ version.name }} ({{ version.versionNumber }}) - {{ formatDate(version.createdAt) }}
        </option>
      </select>
    </div>
    
    <!-- 版本预览 -->
    <div class="version-preview" v-if="selectedVersion">
      <h4>版本详情</h4>
      <div class="preview-content">
        <div class="preview-item">
          <strong>版本名称:</strong>{{ selectedVersion.name }}
        </div>
        <div class="preview-item">
          <strong>版本号:</strong>{{ selectedVersion.versionNumber }}
        </div>
        <div class="preview-item">
          <strong>创建时间:</strong>{{ formatDate(selectedVersion.createdAt) }}
        </div>
        <div class="preview-item">
          <strong>创建人:</strong>{{ selectedVersion.createdBy }}
        </div>
        <div class="preview-item description">
          <strong>版本描述:</strong>{{ selectedVersion.description || '无描述' }}
        </div>
      </div>
    </div>
    
    <!-- 回滚确认 -->
    <div class="rollback-actions" v-if="selectedVersion">
      <div class="confirm-section">
        <input type="checkbox" id="confirm" v-model="confirmRollback" />
        <label for="confirm">我已了解风险,确认回滚</label>
      </div>
      <button 
        class="rollback-btn" 
        @click="performRollback" 
        :disabled="!confirmRollback || isRollingBack"
      >
        {{ isRollingBack ? '回滚中...' : '确认回滚' }}
      </button>
    </div>
    
    <!-- 回滚日志 -->
    <div class="rollback-log" v-if="rollbackLogs.length > 0">
      <h4>回滚日志</h4>
      <div class="log-list">
        <div 
          v-for="(log, index) in rollbackLogs" 
          :key="index"
          class="log-item"
          :class="log.status"
        >
          <div class="log-time">{{ log.time }}</div>
          <div class="log-content">{{ log.message }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import type { Version } from './types';
import { VersionManager } from './services/VersionManager';

// 版本管理器实例
const versionManager = new VersionManager();

// 当前应用ID
const appId = ref('app_123');

// 可用版本列表(只显示已发布和草稿版本)
const availableVersions = ref<Version[]>([]);
// 选中的版本ID
const selectedVersionId = ref('');
// 是否确认回滚
const confirmRollback = ref(false);
// 是否正在回滚
const isRollingBack = ref(false);
// 回滚日志
const rollbackLogs = ref<Array<{ time: string; message: string; status: 'success' | 'error' | 'info' }>>([]);

// 选中的版本
const selectedVersion = computed(() => {
  if (!selectedVersionId.value) return null;
  return availableVersions.value.find(v => v.id === selectedVersionId.value) || null;
});

// 初始化
function init() {
  const allVersions = versionManager.getVersions(appId.value);
  // 只显示草稿和已发布版本
  availableVersions.value = allVersions.filter(v => v.status !== 'archived');
}

// 格式化日期
function formatDate(date: Date): string {
  return new Date(date).toLocaleString('zh-CN');
}

// 监听选中版本变化
watch(selectedVersionId, () => {
  confirmRollback.value = false;
});

// 执行回滚
async function performRollback() {
  if (!selectedVersion.value) return;
  
  try {
    isRollingBack.value = true;
    confirmRollback.value = false;
    
    // 清空日志
    rollbackLogs.value = [];
    
    // 添加开始日志
    addLog('info', `开始回滚到版本:${selectedVersion.value.name} (${selectedVersion.value.versionNumber})`);
    
    // 模拟回滚操作
    await new Promise(resolve => setTimeout(resolve, 1500));
    
    // 添加成功日志
    addLog('success', `回滚成功!已恢复到版本:${selectedVersion.value.name} (${selectedVersion.value.versionNumber})`);
    
    // 重置选中版本
    selectedVersionId.value = '';
    
    // 触发回滚成功事件(实际项目中应该使用事件总线或状态管理)
    // emit('rollback-success', selectedVersion.value);
    
  } catch (error) {
    console.error('Rollback failed:', error);
    addLog('error', `回滚失败:${error instanceof Error ? error.message : '未知错误'}`);
  } finally {
    isRollingBack.value = false;
  }
}

// 添加日志
function addLog(status: 'success' | 'error' | 'info', message: string) {
  rollbackLogs.value.push({
    time: new Date().toLocaleTimeString('zh-CN'),
    message,
    status
  });
}

init();
</script>

<style scoped>
.rollback-manager {
  padding: 20px;
  font-family: Arial, sans-serif;
  background-color: white;
  border-radius: 8px;
  border: 1px solid #e0e0e0;
}

.rollback-manager h3 {
  margin: 0 0 16px 0;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}

.warning-text {
  color: #f57c00;
  background-color: #fff3cd;
  padding: 12px;
  border-radius: 4px;
  margin-bottom: 20px;
  font-size: 14px;
  line-height: 1.5;
}

.version-selector {
  margin-bottom: 20px;
}

.version-selector label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
  color: #333;
  font-size: 14px;
}

.version-select {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  font-size: 14px;
  transition: all 0.3s ease;
}

.version-select:focus {
  outline: none;
  border-color: #1976d2;
  box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}

.version-preview {
  background-color: #fafafa;
  padding: 16px;
  border-radius: 4px;
  margin-bottom: 20px;
}

.version-preview h4 {
  margin: 0 0 12px 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

.preview-content {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.preview-item {
  font-size: 14px;
  color: #333;
}

.preview-item.description {
  margin-top: 4px;
  line-height: 1.5;
}

.rollback-actions {
  margin-bottom: 20px;
}

.confirm-section {
  margin-bottom: 16px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: #333;
}

.rollback-btn {
  padding: 10px 20px;
  background-color: #ff9800;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.3s ease;
}

.rollback-btn:hover:not(:disabled) {
  background-color: #f57c00;
}

.rollback-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.rollback-log {
  margin-top: 20px;
}

.rollback-log h4 {
  margin: 0 0 12px 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

.log-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.log-item {
  padding: 10px 12px;
  border-radius: 4px;
  font-size: 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.log-item.info {
  background-color: #e3f2fd;
  color: #1565c0;
}

.log-item.success {
  background-color: #d4edda;
  color: #155724;
}

.log-item.error {
  background-color: #f8d7da;
  color: #721c24;
}

.log-time {
  font-size: 12px;
  opacity: 0.8;
}

.log-content {
  font-size: 14px;
  line-height: 1.4;
}
</style>

三、使用示例

3.1 基本使用

  1. 创建版本

    • 点击"创建新版本"按钮
    • 输入版本名称和描述
    • 填写变更日志
    • 点击"创建版本"按钮
  2. 查看版本

    • 在版本列表中点击任意版本
    • 在右侧面板查看版本详情
  3. 回滚版本

    • 选择要回滚的版本
    • 勾选确认框
    • 点击"确认回滚"按钮
    • 查看回滚日志
  4. 发布版本

    • 在版本详情中点击"发布此版本"按钮
    • 版本状态变为"已发布"
  5. 归档版本

    • 在版本详情中点击"归档此版本"按钮
    • 版本状态变为"已归档"

3.2 集成到低代码平台

将版本管理功能集成到低代码平台中:

<template>
  <div class="lowcode-platform">
    <!-- 平台头部 -->
    <header class="platform-header">
      <h1>Vue 3低代码平台</h1>
      <div class="header-actions">
        <button class="action-btn" @click="showVersionManager = true">版本管理</button>
        <!-- 其他操作按钮 -->
      </div>
    </header>
    
    <!-- 平台主体 -->
    <main class="platform-main">
      <!-- 左侧组件面板 -->
      <aside class="component-panel">
        <!-- 组件列表 -->
      </aside>
      
      <!-- 中间画布区域 -->
      <section class="canvas-area">
        <!-- 设计画布 -->
      </section>
      
      <!-- 右侧属性面板 -->
      <aside class="property-panel">
        <!-- 属性配置 -->
      </aside>
    </main>
    
    <!-- 版本管理侧边栏 -->
    <aside 
      class="version-sidebar" 
      :class="{ 'open': showVersionManager }"
    >
      <VersionManager @close="showVersionManager = false" />
    </aside>
    
    <!-- 版本回滚模态框 -->
    <div class="modal" v-if="showRollbackModal">
      <div class="modal-overlay" @click="showRollbackModal = false"></div>
      <div class="modal-content large">
        <div class="modal-header">
          <h3>版本回滚</h3>
          <button class="close-btn" @click="showRollbackModal = false">&times;</button>
        </div>
        <div class="modal-body">
          <RollbackManager @rollback-success="handleRollbackSuccess" />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import VersionManager from './components/VersionManager.vue';
import RollbackManager from './components/RollbackManager.vue';

// 显示版本管理器
const showVersionManager = ref(false);
// 显示回滚模态框
const showRollbackModal = ref(false);

// 处理回滚成功
function handleRollbackSuccess(version: any) {
  showRollbackModal.value = false;
  // 刷新应用状态
  console.log('Rollback successful to version:', version);
}
</script>

<style scoped>
/* 平台样式省略 */
</style>

四、性能优化

4.1 快照优化

  • 增量快照:只保存版本之间的差异,而不是完整快照
  • 压缩存储:对快照数据进行压缩,减少存储空间
  • 异步保存:版本创建过程异步执行,不阻塞用户操作
  • 快照验证:创建版本后验证快照的完整性

4.2 版本列表优化

  • 分页加载:大量版本时采用分页加载
  • 虚拟滚动:长列表使用虚拟滚动技术
  • 缓存机制:缓存已加载的版本数据
  • 按需加载:版本详情按需加载,不自动加载所有版本的详细信息

4.3 回滚优化

  • 预加载机制:回滚前预加载目标版本的快照
  • 并行处理:多任务并行处理,提高回滚速度
  • 进度反馈:提供详细的回滚进度反馈
  • 回滚测试:在沙箱环境中测试回滚效果,确保回滚安全

五、最佳实践

5.1 版本命名规范

  • 语义化版本号:使用语义化版本号(如:1.0.0)
  • 清晰的版本名称:版本名称应能反映版本的主要功能或变更
  • 详细的描述:每个版本应有详细的描述和变更日志
  • 标签管理:使用标签对版本进行分类(如:beta、stable、feature-x)

5.2 版本管理策略

  • 定期创建版本:建议在每次重要变更后创建版本
  • 保留关键版本:保留所有已发布的版本,定期清理草稿版本
  • 版本审核:重要版本发布前进行审核
  • 回滚测试:定期测试回滚功能,确保其可用性

5.3 团队协作

  • 分支管理:支持多分支开发,每个分支有独立的版本历史
  • 权限控制:不同角色有不同的版本管理权限
  • 协作日志:记录版本变更的协作信息
  • 通知机制:版本变更时通知相关人员

六、总结

版本管理是低代码平台的重要组成部分,它为用户提供了应用状态的历史记录和回滚能力。在本集中,我们学习了:

  1. 版本管理的核心功能和架构设计
  2. 版本管理的数据模型设计
  3. 版本管理服务的实现
  4. 版本管理可视化组件的实现
  5. 版本回滚功能的实现
  6. 版本管理的集成使用
  7. 性能优化策略
  8. 最佳实践

通过合理设计和实现版本管理功能,我们可以构建出更可靠、更易用的低代码平台,使用户能够安全地管理和演进他们的应用。

在下一集中,我们将学习如何实现低代码平台的多租户架构设计,支持多个团队或组织共享同一个平台。

« 上一篇 Vue 3低代码平台数据源管理实现 下一篇 » Vue 3低代码平台多租户架构设计