feat: 完善版本管理功能
后端改进: - 新增版本发布状态管理 (draft/published/deprecated/superseded) - 新增发布/废弃/回滚 API 接口 - 新增增量包生成功能 (仅包含差异文件) - 新增删除版本时的依赖检查 - 数据库字段: publish_status, superseded_by, delta_package_* 前端改进: - 版本列表新增发布状态列和增量包大小列 - 新增发布/废弃/回滚操作按钮 - 新增增量包下载功能 - 更新国际化翻译 修复: - 修复 announcements 页面 typeOptions 未定义问题 - 修复 echarts 模块导入类型问题 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -234,17 +234,24 @@ type Version struct {
|
||||
FullPackagePath string `gorm:"size:255" json:"full_package_path"`
|
||||
FullPackageSize int64 `json:"full_package_size"`
|
||||
FullPackageHash string `gorm:"size:64" json:"full_package_hash"`
|
||||
DeltaPackagePath string `gorm:"size:255" json:"delta_package_path"`
|
||||
DeltaPackageSize int64 `json:"delta_package_size"`
|
||||
DeltaPackageHash string `gorm:"size:64" json:"delta_package_hash"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Changelog string `gorm:"type:text" json:"changelog"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
PublishStatus string `gorm:"size:20;default:draft;index" json:"publish_status"` // draft, published, deprecated, superseded
|
||||
SupersededBy *uint `json:"superseded_by"`
|
||||
PublishedAt *time.Time `json:"published_at"`
|
||||
DeprecatedAt *time.Time `json:"deprecated_at"`
|
||||
BaseVersionID *uint `json:"base_version_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"`
|
||||
BaseVersion *Version `gorm:"foreignKey:BaseVersionID" json:"base_version,omitempty"`
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"`
|
||||
BaseVersion *Version `gorm:"foreignKey:BaseVersionID" json:"base_version,omitempty"`
|
||||
Superseder *Version `gorm:"foreignKey:SupersededBy" json:"superseder,omitempty"`
|
||||
}
|
||||
|
||||
// VersionFile 版本文件模型
|
||||
|
||||
@@ -903,7 +903,7 @@ func handleCreateVersion(c *gin.Context) {
|
||||
Version: req.Version,
|
||||
Description: req.Description,
|
||||
FilePath: filePath,
|
||||
Status: "active",
|
||||
PublishStatus: "draft",
|
||||
FileSize: fileSize,
|
||||
}
|
||||
|
||||
@@ -952,7 +952,7 @@ func handleUpdateVersion(c *gin.Context) {
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
FilePath string `json:"file_path"`
|
||||
Status string `json:"status"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
@@ -968,7 +968,7 @@ func handleUpdateVersion(c *gin.Context) {
|
||||
version.Version = req.Version
|
||||
version.Description = req.Description
|
||||
version.FilePath = req.FilePath
|
||||
version.Status = req.Status
|
||||
version.PublishStatus = req.PublishStatus
|
||||
|
||||
if err := database.DB.Save(&version).Error; err != nil {
|
||||
response.Error(c, 500, "更新版本失败")
|
||||
@@ -997,31 +997,6 @@ func handleDeleteVersion(c *gin.Context) {
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func handlePublishVersion(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
appID := c.Param("id")
|
||||
versionID := c.Param("versionId")
|
||||
|
||||
var app model.Application
|
||||
if err := database.DB.Where("id = ? AND user_id = ?", appID, userID).First(&app).Error; err != nil {
|
||||
response.Error(c, 404, "应用不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var version model.Version
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", versionID, appID).First(&version).Error; err != nil {
|
||||
response.Error(c, 404, "版本不存在")
|
||||
return
|
||||
}
|
||||
|
||||
version.Status = "published"
|
||||
if err := database.DB.Save(&version).Error; err != nil {
|
||||
response.Error(c, 500, "发布版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, version)
|
||||
}
|
||||
|
||||
func handleManualDeduct(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
@@ -2,6 +2,8 @@ package admin
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -61,6 +63,147 @@ func MergePatchWithFullPackage(fullPackagePath, patchPath string, appID uint) (s
|
||||
return "/" + filepath.ToSlash(newFilePath), fileInfo.Size(), hash, nil
|
||||
}
|
||||
|
||||
// GenerateDeltaPackage generates a delta package containing only changed/new files
|
||||
func GenerateDeltaPackage(basePackagePath, newPackagePath string, appID uint) (string, int64, string, error) {
|
||||
basePackageFile := filepath.Join(".", basePackagePath)
|
||||
newPackageFile := filepath.Join(".", newPackagePath)
|
||||
|
||||
// 获取基础包的文件哈希映射
|
||||
baseFiles, err := getZipFileHashes(basePackageFile)
|
||||
if err != nil {
|
||||
return "", 0, "", fmt.Errorf("read base package failed: %v", err)
|
||||
}
|
||||
|
||||
// 获取新包的文件哈希映射
|
||||
newFiles, err := getZipFileHashes(newPackageFile)
|
||||
if err != nil {
|
||||
return "", 0, "", fmt.Errorf("read new package failed: %v", err)
|
||||
}
|
||||
|
||||
// 找出差异文件(新增或修改)
|
||||
var deltaFiles []string
|
||||
for filePath, hash := range newFiles {
|
||||
if baseHash, exists := baseFiles[filePath]; !exists || baseHash != hash {
|
||||
deltaFiles = append(deltaFiles, filePath)
|
||||
}
|
||||
}
|
||||
|
||||
if len(deltaFiles) == 0 {
|
||||
log.Printf("[INFO] No delta files found, packages are identical")
|
||||
return "", 0, "", nil
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Found %d delta files", len(deltaFiles))
|
||||
|
||||
// 创建增量包
|
||||
timestamp := time.Now().Unix()
|
||||
deltaFileName := fmt.Sprintf("delta_%d_%d.zip", appID, timestamp)
|
||||
deltaFilePath := filepath.Join("uploads", "versions", deltaFileName)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(deltaFilePath), 0755); err != nil {
|
||||
return "", 0, "", fmt.Errorf("create dir failed: %v", err)
|
||||
}
|
||||
|
||||
// 从新包中提取差异文件并打包
|
||||
if err := createDeltaZip(newPackageFile, deltaFilePath, deltaFiles); err != nil {
|
||||
return "", 0, "", fmt.Errorf("create delta package failed: %v", err)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(deltaFilePath)
|
||||
if err != nil {
|
||||
return "", 0, "", fmt.Errorf("stat delta file failed: %v", err)
|
||||
}
|
||||
|
||||
hash, err := calculateFileHash(deltaFilePath)
|
||||
if err != nil {
|
||||
return "", 0, "", fmt.Errorf("calculate hash failed: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Created delta package: %s, size: %d", deltaFilePath, fileInfo.Size())
|
||||
|
||||
return "/" + filepath.ToSlash(deltaFilePath), fileInfo.Size(), hash, nil
|
||||
}
|
||||
|
||||
// getZipFileHashes returns a map of file paths to their SHA256 hashes
|
||||
func getZipFileHashes(zipPath string) (map[string]string, error) {
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
fileHashes := make(map[string]string)
|
||||
for _, f := range r.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
io.Copy(hash, rc)
|
||||
rc.Close()
|
||||
|
||||
fileHashes[f.Name] = hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
return fileHashes, nil
|
||||
}
|
||||
|
||||
// createDeltaZip creates a zip file containing only the specified files from the source zip
|
||||
func createDeltaZip(sourceZipPath, deltaZipPath string, deltaFiles []string) error {
|
||||
r, err := zip.OpenReader(sourceZipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
deltaFileSet := make(map[string]bool)
|
||||
for _, f := range deltaFiles {
|
||||
deltaFileSet[f] = true
|
||||
}
|
||||
|
||||
zipFile, err := os.Create(deltaZipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
if !deltaFileSet[f.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
writer, err := zipWriter.Create(f.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(writer, rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractZip(zipPath, destDir string) error {
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
@@ -140,4 +283,4 @@ func createZipFromDir(srcDir, zipPath string) error {
|
||||
_, err = io.Copy(writer, file)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,9 @@ func SetupVersionRoutes(r *gin.RouterGroup) {
|
||||
versions.PUT("/:id", handleUpdateVersionGlobal)
|
||||
versions.DELETE("/batch", handleBatchDeleteVersions)
|
||||
versions.POST("/upload-zip", handleUploadVersionZip)
|
||||
versions.POST("/:id/publish", handlePublishVersion)
|
||||
versions.POST("/:id/deprecate", handleDeprecateVersion)
|
||||
versions.POST("/:id/rollback", handleRollbackVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +181,7 @@ func handleGetAllVersions(c *gin.Context) {
|
||||
type VersionWithAppName struct {
|
||||
model.Version
|
||||
ApplicationName string `json:"application_name"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
result := make([]VersionWithAppName, len(versions))
|
||||
@@ -185,25 +189,16 @@ func handleGetAllVersions(c *gin.Context) {
|
||||
appName := appNameMap[v.ApplicationID]
|
||||
log.Printf("[DEBUG] Mapping version %d: ApplicationID=%d -> AppName=%s\n", i, v.ApplicationID, appName)
|
||||
|
||||
isSuperseded := false
|
||||
if forcedIDs, ok := appForcedVersionIDs[v.ApplicationID]; ok {
|
||||
for _, forcedID := range forcedIDs {
|
||||
if forcedID > v.ID {
|
||||
isSuperseded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isSuperseded {
|
||||
v.Status = "superseded"
|
||||
} else {
|
||||
v.Status = "active"
|
||||
// 使用数据库中的 publish_status 作为状态
|
||||
status := v.PublishStatus
|
||||
if status == "" {
|
||||
status = "draft"
|
||||
}
|
||||
|
||||
result[i] = VersionWithAppName{
|
||||
Version: v,
|
||||
ApplicationName: appName,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,15 +233,6 @@ func handleGetVersionByID(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
computedStatus := "active"
|
||||
var forcedCount int64
|
||||
database.DB.Model(&model.Version{}).
|
||||
Where("application_id = ? AND id > ? AND update_strategy = ?", version.ApplicationID, version.ID, "forced").
|
||||
Count(&forcedCount)
|
||||
if forcedCount > 0 {
|
||||
computedStatus = "superseded"
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": version.ID,
|
||||
"application_id": version.ApplicationID,
|
||||
@@ -261,7 +247,11 @@ func handleGetVersionByID(c *gin.Context) {
|
||||
"update_method": version.UpdateMethod,
|
||||
"custom_download_url": version.CustomDownloadURL,
|
||||
"changelog": version.Changelog,
|
||||
"status": computedStatus,
|
||||
"status": version.PublishStatus,
|
||||
"publish_status": version.PublishStatus,
|
||||
"superseded_by": version.SupersededBy,
|
||||
"published_at": version.PublishedAt,
|
||||
"deprecated_at": version.DeprecatedAt,
|
||||
"base_version_id": version.BaseVersionID,
|
||||
"files": version.Files,
|
||||
"created_at": version.CreatedAt,
|
||||
@@ -323,7 +313,7 @@ func handleCreateVersionGlobal(c *gin.Context) {
|
||||
CustomDownloadURL: req.CustomDownloadURL,
|
||||
Description: req.Description,
|
||||
Changelog: req.Changelog,
|
||||
Status: "active",
|
||||
PublishStatus: "draft",
|
||||
}
|
||||
|
||||
if version.UpdateStrategy == "" {
|
||||
@@ -342,6 +332,32 @@ func handleCreateVersionGlobal(c *gin.Context) {
|
||||
version.FullPackagePath = version.FilePath
|
||||
version.FullPackageSize = version.FileSize
|
||||
version.FullPackageHash = version.FileHash
|
||||
|
||||
// 尝试生成增量包(与前一个版本对比)
|
||||
var prevVersion model.Version
|
||||
if err := database.DB.Where("application_id = ? AND update_type = ?",
|
||||
req.ApplicationID, "full").
|
||||
Order("id DESC").First(&prevVersion).Error; err == nil {
|
||||
// 有前一个版本,生成增量包
|
||||
prevFullPackagePath := prevVersion.FullPackagePath
|
||||
if prevFullPackagePath == "" {
|
||||
prevFullPackagePath = prevVersion.FilePath
|
||||
}
|
||||
|
||||
deltaPath, deltaSize, deltaHash, err := GenerateDeltaPackage(
|
||||
prevFullPackagePath,
|
||||
version.FilePath,
|
||||
req.ApplicationID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate delta package: %v\n", err)
|
||||
} else if deltaPath != "" {
|
||||
version.DeltaPackagePath = deltaPath
|
||||
version.DeltaPackageSize = deltaSize
|
||||
version.DeltaPackageHash = deltaHash
|
||||
log.Printf("[INFO] Delta package created: %s, size: %d\n", deltaPath, deltaSize)
|
||||
}
|
||||
}
|
||||
} else if version.UpdateType == "patch" {
|
||||
// 补丁包:需要找到基础全量包并合并
|
||||
var lastFullVersion model.Version
|
||||
@@ -571,6 +587,21 @@ func handleBatchDeleteVersions(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有其他补丁包依赖这些版本
|
||||
var dependentPatches []model.Version
|
||||
if err := database.DB.Where("base_version_id IN ?", req.IDs).Find(&dependentPatches).Error; err != nil {
|
||||
response.Error(c, 500, "检查依赖关系失败")
|
||||
return
|
||||
}
|
||||
if len(dependentPatches) > 0 {
|
||||
dependentIDs := make([]string, len(dependentPatches))
|
||||
for i, p := range dependentPatches {
|
||||
dependentIDs[i] = fmt.Sprintf("v%d.%s", p.ID, p.Version)
|
||||
}
|
||||
response.Error(c, 400, fmt.Sprintf("以下补丁包依赖所选版本,无法删除: %s", strings.Join(dependentIDs, ", ")))
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Where("id IN ? AND application_id IN ?", req.IDs, appIDs).Delete(&model.Version{}).Error; err != nil {
|
||||
response.Error(c, 500, "批量删除版本失败")
|
||||
return
|
||||
@@ -765,3 +796,195 @@ func getFileType(filename string) string {
|
||||
return "resource"
|
||||
}
|
||||
}
|
||||
|
||||
func handlePublishVersion(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
versionID := c.Param("id")
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
}
|
||||
|
||||
var version model.Version
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", versionID, appIDs).First(&version).Error; err != nil {
|
||||
response.Error(c, 404, "版本不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if version.PublishStatus != "draft" && version.PublishStatus != "deprecated" {
|
||||
response.Error(c, 400, "只能发布草稿或已废弃的版本")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
version.PublishStatus = "published"
|
||||
version.PublishedAt = &now
|
||||
|
||||
if err := database.DB.Save(&version).Error; err != nil {
|
||||
response.Error(c, 500, "发布版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是强制更新,标记旧版本为 superseded
|
||||
if version.UpdateStrategy == "forced" {
|
||||
database.DB.Model(&model.Version{}).
|
||||
Where("application_id = ? AND id < ? AND publish_status = ?", version.ApplicationID, version.ID, "published").
|
||||
Updates(map[string]interface{}{
|
||||
"publish_status": "superseded",
|
||||
"superseded_by": version.ID,
|
||||
"deprecated_at": now,
|
||||
})
|
||||
}
|
||||
|
||||
userIDPtr := &userID
|
||||
versionIDPtr := &version.ID
|
||||
service.CreateLog(service.LogParams{
|
||||
UserID: userIDPtr,
|
||||
LogType: "version",
|
||||
Action: "publish",
|
||||
Resource: "version",
|
||||
ResourceID: versionIDPtr,
|
||||
Details: fmt.Sprintf("发布版本: %s (应用ID: %d)", version.Version, version.ApplicationID),
|
||||
})
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": version.ID,
|
||||
"publish_status": version.PublishStatus,
|
||||
"published_at": version.PublishedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeprecateVersion(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
versionID := c.Param("id")
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
}
|
||||
|
||||
var version model.Version
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", versionID, appIDs).First(&version).Error; err != nil {
|
||||
response.Error(c, 404, "版本不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if version.PublishStatus != "published" {
|
||||
response.Error(c, 400, "只能废弃已发布的版本")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
version.PublishStatus = "deprecated"
|
||||
version.DeprecatedAt = &now
|
||||
|
||||
if err := database.DB.Save(&version).Error; err != nil {
|
||||
response.Error(c, 500, "废弃版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果有被该版本取代的版本,恢复它们为 published
|
||||
database.DB.Model(&model.Version{}).
|
||||
Where("superseded_by = ?", version.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"publish_status": "published",
|
||||
"superseded_by": nil,
|
||||
"deprecated_at": nil,
|
||||
})
|
||||
|
||||
userIDPtr := &userID
|
||||
versionIDPtr := &version.ID
|
||||
service.CreateLog(service.LogParams{
|
||||
UserID: userIDPtr,
|
||||
LogType: "version",
|
||||
Action: "deprecate",
|
||||
Resource: "version",
|
||||
ResourceID: versionIDPtr,
|
||||
Details: fmt.Sprintf("废弃版本: %s (应用ID: %d)", version.Version, version.ApplicationID),
|
||||
})
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": version.ID,
|
||||
"publish_status": version.PublishStatus,
|
||||
"deprecated_at": version.DeprecatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func handleRollbackVersion(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
versionID := c.Param("id")
|
||||
|
||||
var userApps []model.Application
|
||||
if err := database.DB.Where("user_id = ?", userID).Find(&userApps).Error; err != nil {
|
||||
response.Error(c, 500, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
appIDs := make([]uint, len(userApps))
|
||||
for i, app := range userApps {
|
||||
appIDs[i] = app.ID
|
||||
}
|
||||
|
||||
var version model.Version
|
||||
if err := database.DB.Where("id = ? AND application_id IN ?", versionID, appIDs).First(&version).Error; err != nil {
|
||||
response.Error(c, 404, "版本不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 只能回滚 superseded 状态的版本
|
||||
if version.PublishStatus != "superseded" {
|
||||
response.Error(c, 400, "只能回滚已被取代的版本")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
version.PublishStatus = "published"
|
||||
version.SupersededBy = nil
|
||||
version.DeprecatedAt = nil
|
||||
|
||||
if err := database.DB.Save(&version).Error; err != nil {
|
||||
response.Error(c, 500, "回滚版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 找到当前发布的版本,将其改为 superseded(如果有的话)
|
||||
var currentPublished model.Version
|
||||
err := database.DB.Where("application_id = ? AND publish_status = ? AND id != ?",
|
||||
version.ApplicationID, "published", version.ID).
|
||||
Order("id DESC").First(¤tPublished).Error
|
||||
if err == nil {
|
||||
currentPublished.PublishStatus = "superseded"
|
||||
currentPublished.SupersededBy = &version.ID
|
||||
currentPublished.DeprecatedAt = &now
|
||||
database.DB.Save(¤tPublished)
|
||||
}
|
||||
|
||||
userIDPtr := &userID
|
||||
versionIDPtr := &version.ID
|
||||
service.CreateLog(service.LogParams{
|
||||
UserID: userIDPtr,
|
||||
LogType: "version",
|
||||
Action: "rollback",
|
||||
Resource: "version",
|
||||
ResourceID: versionIDPtr,
|
||||
Details: fmt.Sprintf("回滚版本: %s (应用ID: %d)", version.Version, version.ApplicationID),
|
||||
})
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"id": version.ID,
|
||||
"publish_status": version.PublishStatus,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ const applicationOptions = computed(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const methodOptions = computed(() => [
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('admin.announcements.types.info'), value: 'info' },
|
||||
{ label: t('admin.announcements.types.warning'), value: 'warning' },
|
||||
{ label: t('admin.announcements.types.error'), value: 'error' },
|
||||
|
||||
@@ -243,7 +243,7 @@ async function initMapChart() {
|
||||
fetch('/world.json'),
|
||||
])
|
||||
|
||||
const echarts = echartsModule.default || echartsModule
|
||||
const echarts = (echartsModule as any).default || echartsModule
|
||||
|
||||
if (!mapChart.value) {
|
||||
console.warn('Map chart container was unmounted during initialization')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Download, Edit, Trash2 } from 'lucide-vue-next'
|
||||
import { Download, Edit, Trash2, Upload, Archive, ArrowDownToLine } from 'lucide-vue-next'
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
import { h } from 'vue'
|
||||
@@ -12,6 +12,10 @@ interface ColumnOptions {
|
||||
onDownload: (row: Version) => void
|
||||
onEdit: (row: Version) => void
|
||||
onDelete: (row: Version) => void
|
||||
onPublish?: (row: Version) => void
|
||||
onDeprecate?: (row: Version) => void
|
||||
onRollback?: (row: Version) => void
|
||||
onDownloadDelta?: (row: Version) => void
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
@@ -85,34 +89,37 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'update_method',
|
||||
header: () => t('admin.versions.columns.method'),
|
||||
cell: ({ row }) => {
|
||||
const methodClasses: Record<string, string> = {
|
||||
auto: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
manual: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
}
|
||||
const methodLabels: Record<string, string> = {
|
||||
auto: t('admin.versions.methods.auto'),
|
||||
manual: t('admin.versions.methods.manual'),
|
||||
}
|
||||
return h(Badge, { class: methodClasses[row.original.update_method] || methodClasses.manual }, () => methodLabels[row.original.update_method] || row.original.update_method)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
accessorKey: 'publish_status',
|
||||
header: () => t('admin.versions.columns.status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status
|
||||
const status = row.original.publish_status || row.original.status || 'draft'
|
||||
const statusClasses: Record<string, string> = {
|
||||
active: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
draft: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
published: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
deprecated: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
|
||||
superseded: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||
}
|
||||
const statusLabels: Record<string, string> = {
|
||||
active: t('admin.versions.status.active'),
|
||||
draft: t('admin.versions.status.draft'),
|
||||
published: t('admin.versions.status.published'),
|
||||
deprecated: t('admin.versions.status.deprecated'),
|
||||
superseded: t('admin.versions.status.superseded'),
|
||||
active: t('admin.versions.status.published'),
|
||||
}
|
||||
return h(Badge, { class: statusClasses[status] || statusClasses.superseded }, () => statusLabels[status] || status)
|
||||
return h(Badge, { class: statusClasses[status] || statusClasses.draft }, () => statusLabels[status] || status)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'delta_package',
|
||||
header: () => t('admin.versions.columns.deltaPackage'),
|
||||
cell: ({ row }) => {
|
||||
if (row.original.delta_package_path && row.original.delta_package_size) {
|
||||
return h('div', { class: 'flex items-center gap-1 text-xs text-muted-foreground' }, [
|
||||
h(Archive, { class: 'h-3 w-3' }),
|
||||
h('span', {}, formatFileSize(row.original.delta_package_size)),
|
||||
])
|
||||
}
|
||||
return h('span', { class: 'text-muted-foreground' }, '-')
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -127,33 +134,104 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
header: () => t('admin.versions.columns.actions'),
|
||||
cell: ({ row }) => {
|
||||
const buttons = []
|
||||
|
||||
// 下载按钮
|
||||
if (row.original.file_path) {
|
||||
buttons.push(
|
||||
h(Button, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
title: t('admin.versions.actions.download'),
|
||||
onClick: () => options.onDownload(row.original),
|
||||
}, () => [
|
||||
h(Download, { class: 'h-4 w-4' }),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
// 增量包下载
|
||||
if (row.original.delta_package_path && options.onDownloadDelta) {
|
||||
buttons.push(
|
||||
h(Button, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
title: t('admin.versions.actions.downloadDelta'),
|
||||
onClick: () => options.onDownloadDelta?.(row.original),
|
||||
}, () => [
|
||||
h(ArrowDownToLine, { class: 'h-4 w-4 text-orange-500' }),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
// 编辑按钮
|
||||
buttons.push(
|
||||
h(Button, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
title: t('admin.versions.actions.edit'),
|
||||
onClick: () => options.onEdit(row.original),
|
||||
}, () => [
|
||||
h(Edit, { class: 'h-4 w-4' }),
|
||||
]),
|
||||
)
|
||||
|
||||
// 发布状态操作按钮
|
||||
const publishStatus = row.original.publish_status || row.original.status || 'draft'
|
||||
|
||||
if (publishStatus === 'draft' && options.onPublish) {
|
||||
buttons.push(
|
||||
h(Button, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
title: t('admin.versions.actions.publish'),
|
||||
class: 'text-green-600 hover:text-green-700',
|
||||
onClick: () => options.onPublish?.(row.original),
|
||||
}, () => [
|
||||
h(Upload, { class: 'h-4 w-4' }),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
if (publishStatus === 'published' && options.onDeprecate) {
|
||||
buttons.push(
|
||||
h(Button, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
title: t('admin.versions.actions.deprecate'),
|
||||
class: 'text-orange-600 hover:text-orange-700',
|
||||
onClick: () => options.onDeprecate?.(row.original),
|
||||
}, () => [
|
||||
h(Archive, { class: 'h-4 w-4' }),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
if (publishStatus === 'superseded' && options.onRollback) {
|
||||
buttons.push(
|
||||
h(Button, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
title: t('admin.versions.actions.rollback'),
|
||||
class: 'text-blue-600 hover:text-blue-700',
|
||||
onClick: () => options.onRollback?.(row.original),
|
||||
}, () => [
|
||||
h(ArrowDownToLine, { class: 'h-4 w-4' }),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
buttons.push(
|
||||
h(Button, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
title: t('admin.versions.actions.delete'),
|
||||
onClick: () => options.onDelete(row.original),
|
||||
}, () => [
|
||||
h(Trash2, { class: 'h-4 w-4 text-destructive' }),
|
||||
]),
|
||||
)
|
||||
|
||||
return h('div', { class: 'flex items-center justify-end gap-1' }, buttons)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,6 +19,10 @@ const props = defineProps<Omit<DataTableProps<Version>, 'columns'> & {
|
||||
onDownload: (row: Version) => void
|
||||
onEdit: (row: Version) => void
|
||||
onDelete: (row: Version) => void
|
||||
onPublish?: (row: Version) => void
|
||||
onDeprecate?: (row: Version) => void
|
||||
onRollback?: (row: Version) => void
|
||||
onDownloadDelta?: (row: Version) => void
|
||||
searchFilter?: string
|
||||
}>()
|
||||
|
||||
@@ -38,6 +42,10 @@ const columns = computed(() => [
|
||||
onDownload: props.onDownload,
|
||||
onEdit: props.onEdit,
|
||||
onDelete: props.onDelete,
|
||||
onPublish: props.onPublish,
|
||||
onDeprecate: props.onDeprecate,
|
||||
onRollback: props.onRollback,
|
||||
onDownloadDelta: props.onDownloadDelta,
|
||||
}, t),
|
||||
])
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
||||
export const updateStrategySchema = z.enum(['optional', 'forced'])
|
||||
export const updateTypeSchema = z.enum(['full', 'patch'])
|
||||
export const updateMethodSchema = z.enum(['auto', 'manual'])
|
||||
export const versionStatusSchema = z.enum(['active', 'superseded'])
|
||||
export const publishStatusSchema = z.enum(['draft', 'published', 'deprecated', 'superseded'])
|
||||
|
||||
export const versionSchema = z.object({
|
||||
id: z.number(),
|
||||
@@ -13,11 +13,20 @@ export const versionSchema = z.object({
|
||||
description: z.string(),
|
||||
file_path: z.string(),
|
||||
file_size: z.number(),
|
||||
file_hash: z.string().optional(),
|
||||
update_strategy: updateStrategySchema,
|
||||
update_type: updateTypeSchema,
|
||||
update_method: updateMethodSchema,
|
||||
status: versionStatusSchema,
|
||||
status: z.string(), // 兼容旧字段
|
||||
publish_status: publishStatusSchema,
|
||||
superseded_by: z.number().nullable().optional(),
|
||||
published_at: z.string().nullable().optional(),
|
||||
deprecated_at: z.string().nullable().optional(),
|
||||
changelog: z.string(),
|
||||
delta_package_path: z.string().optional(),
|
||||
delta_package_size: z.number().optional(),
|
||||
full_package_path: z.string().optional(),
|
||||
full_package_size: z.number().optional(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
@@ -45,6 +45,12 @@ const batchDeleteIds = ref<number[]>([])
|
||||
const exportDialogOpen = ref(false)
|
||||
const exportTarget = ref<Version | null>(null)
|
||||
const batchExportDialogOpen = ref(false)
|
||||
const publishDialogOpen = ref(false)
|
||||
const publishTarget = ref<Version | null>(null)
|
||||
const deprecateDialogOpen = ref(false)
|
||||
const deprecateTarget = ref<Version | null>(null)
|
||||
const rollbackDialogOpen = ref(false)
|
||||
const rollbackTarget = ref<Version | null>(null)
|
||||
|
||||
const applicationOptions = computed(() => {
|
||||
return applications.value.map(app => ({
|
||||
@@ -226,6 +232,83 @@ async function handleBatchDelete() {
|
||||
}
|
||||
}
|
||||
|
||||
function downloadDelta(version: Version) {
|
||||
if (!version.delta_package_path)
|
||||
return
|
||||
const token = localStorage.getItem('token')
|
||||
const downloadUrl = `${version.delta_package_path}?token=${token}`
|
||||
window.open(downloadUrl, '_blank')
|
||||
}
|
||||
|
||||
function confirmPublish(version: Version) {
|
||||
publishTarget.value = version
|
||||
publishDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handlePublish() {
|
||||
if (!publishTarget.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await api.post(`/dev/versions/${publishTarget.value.id}/publish`)
|
||||
toast.success(t('admin.versions.publishSuccess'))
|
||||
fetchVersions()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('发布版本失败:', error)
|
||||
toast.error(error.message || t('common.error'))
|
||||
}
|
||||
finally {
|
||||
publishTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeprecate(version: Version) {
|
||||
deprecateTarget.value = version
|
||||
deprecateDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDeprecate() {
|
||||
if (!deprecateTarget.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await api.post(`/dev/versions/${deprecateTarget.value.id}/deprecate`)
|
||||
toast.success(t('admin.versions.deprecateSuccess'))
|
||||
fetchVersions()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('废弃版本失败:', error)
|
||||
toast.error(error.message || t('common.error'))
|
||||
}
|
||||
finally {
|
||||
deprecateTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function confirmRollback(version: Version) {
|
||||
rollbackTarget.value = version
|
||||
rollbackDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleRollback() {
|
||||
if (!rollbackTarget.value)
|
||||
return
|
||||
|
||||
try {
|
||||
await api.post(`/dev/versions/${rollbackTarget.value.id}/rollback`)
|
||||
toast.success(t('admin.versions.rollbackSuccess'))
|
||||
fetchVersions()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('回滚版本失败:', error)
|
||||
toast.error(error.message || t('common.error'))
|
||||
}
|
||||
finally {
|
||||
rollbackTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchApplications()
|
||||
fetchVersions()
|
||||
@@ -327,6 +410,10 @@ watch([appFilter, strategyFilter, typeFilter, methodFilter, searchFilter], () =>
|
||||
:on-download="downloadVersion"
|
||||
:on-edit="goToEdit"
|
||||
:on-delete="confirmDelete"
|
||||
:on-publish="confirmPublish"
|
||||
:on-deprecate="confirmDeprecate"
|
||||
:on-rollback="confirmRollback"
|
||||
:on-download-delta="downloadDelta"
|
||||
:search-filter="searchFilter"
|
||||
@refresh="fetchVersions"
|
||||
@update:search-filter="searchFilter = $event"
|
||||
@@ -390,5 +477,48 @@ watch([appFilter, strategyFilter, typeFilter, methodFilter, searchFilter], () =>
|
||||
{{ t('admin.versions.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="publishDialogOpen"
|
||||
:confirm-button-text="t('admin.versions.actions.publish')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handlePublish"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.versions.publishTitle') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.versions.publishConfirm', { version: publishTarget?.version }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="deprecateDialogOpen"
|
||||
destructive
|
||||
:confirm-button-text="t('admin.versions.actions.deprecate')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleDeprecate"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.versions.deprecateTitle') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.versions.deprecateConfirm', { version: deprecateTarget?.version }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="rollbackDialogOpen"
|
||||
:confirm-button-text="t('admin.versions.actions.rollback')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleRollback"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('admin.versions.rollbackTitle') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ t('admin.versions.rollbackConfirm', { version: rollbackTarget?.version }) }}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -1415,6 +1415,15 @@
|
||||
"batchExportBtn": "Batch Export",
|
||||
"batchDeleteConfirm": "Are you sure you want to delete {count} selected versions? This action cannot be undone.",
|
||||
"batchDeleteSuccess": "Batch delete successful",
|
||||
"publishTitle": "Publish Version",
|
||||
"publishConfirm": "Are you sure you want to publish version \"v{version}\"? This will make it available to users.",
|
||||
"publishSuccess": "Version published successfully",
|
||||
"deprecateTitle": "Deprecate Version",
|
||||
"deprecateConfirm": "Are you sure you want to deprecate version \"v{version}\"? This will stop distributing it to users.",
|
||||
"deprecateSuccess": "Version deprecated successfully",
|
||||
"rollbackTitle": "Rollback Version",
|
||||
"rollbackConfirm": "Are you sure you want to rollback version \"v{version}\"? This will restore it to published status.",
|
||||
"rollbackSuccess": "Version rolled back successfully",
|
||||
"select": "Select",
|
||||
"columns": {
|
||||
"version": "Version",
|
||||
@@ -1425,9 +1434,19 @@
|
||||
"type": "Type",
|
||||
"method": "Method",
|
||||
"status": "Status",
|
||||
"deltaPackage": "Delta",
|
||||
"createdAt": "Created At",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"actions": {
|
||||
"download": "Download",
|
||||
"downloadDelta": "Download Delta",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"publish": "Publish",
|
||||
"deprecate": "Deprecate",
|
||||
"rollback": "Rollback"
|
||||
},
|
||||
"strategies": {
|
||||
"optional": "Optional",
|
||||
"forced": "Forced"
|
||||
@@ -1441,8 +1460,11 @@
|
||||
"manual": "Manual Update"
|
||||
},
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"superseded": "Superseded"
|
||||
"draft": "Draft",
|
||||
"published": "Published",
|
||||
"deprecated": "Deprecated",
|
||||
"superseded": "Superseded",
|
||||
"active": "Active"
|
||||
},
|
||||
"createForm": {
|
||||
"title": "Create Version",
|
||||
|
||||
@@ -1393,6 +1393,15 @@
|
||||
"batchExportBtn": "批量导出",
|
||||
"batchDeleteConfirm": "确定要删除选中的 {count} 个版本吗?此操作不可撤销。",
|
||||
"batchDeleteSuccess": "批量删除成功",
|
||||
"publishTitle": "发布版本",
|
||||
"publishConfirm": "确定要发布版本「v{version}」吗?发布后将分发给用户。",
|
||||
"publishSuccess": "版本发布成功",
|
||||
"deprecateTitle": "废弃版本",
|
||||
"deprecateConfirm": "确定要废弃版本「v{version}」吗?废弃后将停止分发给用户。",
|
||||
"deprecateSuccess": "版本废弃成功",
|
||||
"rollbackTitle": "回滚版本",
|
||||
"rollbackConfirm": "确定要回滚版本「v{version}」吗?回滚后将恢复为已发布状态。",
|
||||
"rollbackSuccess": "版本回滚成功",
|
||||
"select": "选择",
|
||||
"columns": {
|
||||
"version": "版本号",
|
||||
@@ -1403,9 +1412,19 @@
|
||||
"type": "更新类型",
|
||||
"method": "更新方式",
|
||||
"status": "状态",
|
||||
"deltaPackage": "增量包",
|
||||
"createdAt": "创建时间",
|
||||
"actions": "操作"
|
||||
},
|
||||
"actions": {
|
||||
"download": "下载",
|
||||
"downloadDelta": "下载增量包",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"publish": "发布",
|
||||
"deprecate": "废弃",
|
||||
"rollback": "回滚"
|
||||
},
|
||||
"strategies": {
|
||||
"optional": "可选更新",
|
||||
"forced": "强制更新"
|
||||
@@ -1419,8 +1438,11 @@
|
||||
"manual": "手动更新"
|
||||
},
|
||||
"status": {
|
||||
"active": "有效",
|
||||
"superseded": "已失效"
|
||||
"draft": "草稿",
|
||||
"published": "已发布",
|
||||
"deprecated": "已废弃",
|
||||
"superseded": "已失效",
|
||||
"active": "有效"
|
||||
},
|
||||
"createForm": {
|
||||
"title": "创建版本",
|
||||
|
||||
Vendored
+70
-5
@@ -518,9 +518,23 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/agent/cloud-data/': RouteRecordInfo<
|
||||
'/agent/cloud-data/',
|
||||
'/agent/cloud-data',
|
||||
'/agent/cloud-variables/': RouteRecordInfo<
|
||||
'/agent/cloud-variables/',
|
||||
'/agent/cloud-variables',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/agent/cloud-variables/[id]/records': RouteRecordInfo<
|
||||
'/agent/cloud-variables/[id]/records',
|
||||
'/agent/cloud-variables/:id/records',
|
||||
{ id: ParamValue<true> },
|
||||
{ id: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/agent/devices/': RouteRecordInfo<
|
||||
'/agent/devices/',
|
||||
'/agent/devices',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
@@ -532,6 +546,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/agent/sessions/': RouteRecordInfo<
|
||||
'/agent/sessions/',
|
||||
'/agent/sessions',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/agent/users/': RouteRecordInfo<
|
||||
'/agent/users/',
|
||||
'/agent/users',
|
||||
@@ -539,6 +560,20 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/agent/users/[id]': RouteRecordInfo<
|
||||
'/agent/users/[id]',
|
||||
'/agent/users/:id',
|
||||
{ id: ParamValue<true> },
|
||||
{ id: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/agent/users/create': RouteRecordInfo<
|
||||
'/agent/users/create',
|
||||
'/agent/users/create',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/auth/login': RouteRecordInfo<
|
||||
'/auth/login',
|
||||
'/auth/login',
|
||||
@@ -1058,9 +1093,21 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/agent/cloud-data/index.vue': {
|
||||
'src/pages/agent/cloud-variables/index.vue': {
|
||||
routes:
|
||||
| '/agent/cloud-data/'
|
||||
| '/agent/cloud-variables/'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/agent/cloud-variables/[id]/records.vue': {
|
||||
routes:
|
||||
| '/agent/cloud-variables/[id]/records'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/agent/devices/index.vue': {
|
||||
routes:
|
||||
| '/agent/devices/'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
@@ -1070,12 +1117,30 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/agent/sessions/index.vue': {
|
||||
routes:
|
||||
| '/agent/sessions/'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/agent/users/index.vue': {
|
||||
routes:
|
||||
| '/agent/users/'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/agent/users/[id].vue': {
|
||||
routes:
|
||||
| '/agent/users/[id]'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/agent/users/create.vue': {
|
||||
routes:
|
||||
| '/agent/users/create'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/auth/login.vue': {
|
||||
routes:
|
||||
| '/auth/login'
|
||||
|
||||
Reference in New Issue
Block a user