diff --git a/backend/app.yaml b/backend/app.yaml index 02c0467d1..d0850568b 100644 --- a/backend/app.yaml +++ b/backend/app.yaml @@ -5,7 +5,6 @@ system: data_dir: /opt/1Panel/data resource_dir: /opt/1Panel/data/resource app_dir: /opt/1Panel/data/apps - app_oss: mysql: path: localhost diff --git a/backend/app/api/v1/app.go b/backend/app/api/v1/app.go index e94b2e983..1bec69337 100644 --- a/backend/app/api/v1/app.go +++ b/backend/app/api/v1/app.go @@ -75,6 +75,21 @@ func (b *BaseApi) InstallApp(c *gin.Context) { helper.SuccessWithData(c, nil) } + +func (b *BaseApi) DeleteAppBackup(c *gin.Context) { + var req dto.AppBackupDeleteRequest + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := appService.DeleteBackup(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + func (b *BaseApi) SearchInstalled(c *gin.Context) { var req dto.AppInstalledRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -93,6 +108,24 @@ func (b *BaseApi) SearchInstalled(c *gin.Context) { }) } +func (b *BaseApi) SearchInstalledBackup(c *gin.Context) { + var req dto.AppBackupRequest + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + total, list, err := appService.PageInstallBackups(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + func (b *BaseApi) OperateInstalled(c *gin.Context) { var req dto.AppInstallOperate if err := c.ShouldBindJSON(&req); err != nil { diff --git a/backend/app/dto/app.go b/backend/app/dto/app.go index a640d5dec..0925a0547 100644 --- a/backend/app/dto/app.go +++ b/backend/app/dto/app.go @@ -92,6 +92,15 @@ type AppInstalledRequest struct { PageInfo } +type AppBackupRequest struct { + PageInfo + AppInstallID uint `json:"appInstallID"` +} + +type AppBackupDeleteRequest struct { + Ids []uint `json:"ids"` +} + type AppOperate string var ( @@ -100,6 +109,7 @@ var ( Restart AppOperate = "restart" Delete AppOperate = "delete" Sync AppOperate = "sync" + Backup AppOperate = "backup" ) type AppInstallOperate struct { diff --git a/backend/app/model/app_install.go b/backend/app/model/app_install.go index c9d0cc0e3..73602eee9 100644 --- a/backend/app/model/app_install.go +++ b/backend/app/model/app_install.go @@ -1,34 +1,35 @@ package model import ( - "github.com/1Panel-dev/1Panel/global" + "github.com/1Panel-dev/1Panel/constant" "path" ) type AppInstall struct { BaseModel - Name string `json:"name" gorm:"type:varchar(64);not null"` - AppId uint `json:"appId" gorm:"type:integer;not null"` - AppDetailId uint `json:"appDetailId" gorm:"type:integer;not null"` - Version string `json:"version" gorm:"type:varchar(64);not null"` - Param string `json:"param" gorm:"type:longtext;"` - Env string `json:"env" gorm:"type:longtext;"` - DockerCompose string `json:"dockerCompose" gorm:"type:longtext;"` - Status string `json:"status" gorm:"type:varchar(256);not null"` - Description string `json:"description" gorm:"type:varchar(256);"` - Message string `json:"message" gorm:"type:longtext;"` - CanUpdate bool `json:"canUpdate"` - ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"` - ServiceName string `json:"ServiceName" gorm:"type:varchar(256);not null"` - HttpPort int `json:"httpPort" gorm:"type:integer;not null"` - HttpsPort int `json:"httpsPort" gorm:"type:integer;not null"` - App App `json:"-"` + Name string `json:"name" gorm:"type:varchar(64);not null"` + AppId uint `json:"appId" gorm:"type:integer;not null"` + AppDetailId uint `json:"appDetailId" gorm:"type:integer;not null"` + Version string `json:"version" gorm:"type:varchar(64);not null"` + Param string `json:"param" gorm:"type:longtext;"` + Env string `json:"env" gorm:"type:longtext;"` + DockerCompose string `json:"dockerCompose" gorm:"type:longtext;"` + Status string `json:"status" gorm:"type:varchar(256);not null"` + Description string `json:"description" gorm:"type:varchar(256);"` + Message string `json:"message" gorm:"type:longtext;"` + CanUpdate bool `json:"canUpdate"` + ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"` + ServiceName string `json:"serviceName" gorm:"type:varchar(256);not null"` + HttpPort int `json:"httpPort" gorm:"type:integer;not null"` + HttpsPort int `json:"httpsPort" gorm:"type:integer;not null"` + App App `json:"app"` + Backups []AppInstallBackup `json:"backups"` } func (i *AppInstall) GetPath() string { - return path.Join(global.CONF.System.AppDir, i.App.Key, i.Name) + return path.Join(constant.AppInstallDir, i.App.Key, i.Name) } func (i *AppInstall) GetComposePath() string { - return path.Join(global.CONF.System.AppDir, i.App.Key, i.Name, "docker-compose.yml") + return path.Join(constant.AppInstallDir, i.App.Key, i.Name, "docker-compose.yml") } diff --git a/backend/app/model/app_install_backup.go b/backend/app/model/app_install_backup.go new file mode 100644 index 000000000..07302dea2 --- /dev/null +++ b/backend/app/model/app_install_backup.go @@ -0,0 +1,9 @@ +package model + +type AppInstallBackup struct { + BaseModel + Name string `gorm:"type:varchar(64);not null" json:"name"` + Path string `gorm:"type:varchar(64);not null" json:"path"` + AppDetailId uint `gorm:"type:varchar(64);not null" json:"app_detail_id"` + AppInstallId uint `gorm:"type:integer;not null" json:"app_install_id"` +} diff --git a/backend/app/repo/app_install.go b/backend/app/repo/app_install.go index f50645e32..fefff2475 100644 --- a/backend/app/repo/app_install.go +++ b/backend/app/repo/app_install.go @@ -40,14 +40,14 @@ func (a AppInstallRepo) WithServiceName(serviceName string) DBOption { func (a AppInstallRepo) GetBy(opts ...DBOption) ([]model.AppInstall, error) { var install []model.AppInstall db := getDb(opts...).Model(&model.AppInstall{}) - err := db.Preload("App").Find(&install).Error + err := db.Preload("App").Preload("Backups").Find(&install).Error return install, err } func (a AppInstallRepo) GetFirst(opts ...DBOption) (model.AppInstall, error) { var install model.AppInstall db := getDb(opts...).Model(&model.AppInstall{}) - err := db.Preload("App").First(&install).Error + err := db.Preload("App").Preload("Backups").First(&install).Error return install, err } @@ -74,7 +74,7 @@ func (a AppInstallRepo) Page(page, size int, opts ...DBOption) (int64, []model.A db := getDb(opts...).Model(&model.AppInstall{}) count := int64(0) db = db.Count(&count) - err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("App").Find(&apps).Error + err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("App").Preload("Backups").Find(&apps).Error return count, apps, err } diff --git a/backend/app/repo/app_install_backup.go b/backend/app/repo/app_install_backup.go new file mode 100644 index 000000000..7142dd376 --- /dev/null +++ b/backend/app/repo/app_install_backup.go @@ -0,0 +1,41 @@ +package repo + +import ( + "context" + "github.com/1Panel-dev/1Panel/app/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type AppInstallBackupRepo struct { +} + +func (a AppInstallBackupRepo) WithAppInstallID(appInstallID uint) DBOption { + return func(db *gorm.DB) *gorm.DB { + return db.Where("app_install_id = ?", appInstallID) + } +} + +func (a AppInstallBackupRepo) Create(ctx context.Context, backup model.AppInstallBackup) error { + return getTx(ctx).Omit(clause.Associations).Create(&backup).Error +} + +func (a AppInstallBackupRepo) Delete(opts ...DBOption) error { + return getDb(opts...).Omit(clause.Associations).Delete(&model.AppInstallBackup{}).Error +} +func (a AppInstallBackupRepo) GetBy(opts ...DBOption) ([]model.AppInstallBackup, error) { + var backups []model.AppInstallBackup + if err := getDb(opts...).Find(&backups); err != nil { + return backups, nil + } + return backups, nil +} + +func (a AppInstallBackupRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstallBackup, error) { + var backups []model.AppInstallBackup + db := getDb(opts...).Model(&model.AppInstallBackup{}) + count := int64(0) + db = db.Count(&count) + err := db.Debug().Limit(size).Offset(size * (page - 1)).Find(&backups).Error + return count, backups, err +} diff --git a/backend/app/repo/entry.go b/backend/app/repo/entry.go index 025b9b643..e8e1193cd 100644 --- a/backend/app/repo/entry.go +++ b/backend/app/repo/entry.go @@ -16,6 +16,7 @@ type RepoGroup struct { AppInstallRepo AppInstallResourceRpo DatabaseRepo + AppInstallBackupRepo } var RepoGroupApp = new(RepoGroup) diff --git a/backend/app/service/app.go b/backend/app/service/app.go index 004bb8d03..42c71ea2d 100644 --- a/backend/app/service/app.go +++ b/backend/app/service/app.go @@ -117,8 +117,6 @@ func (a AppService) PageInstalled(req dto.AppInstalledRequest) (int64, []dto.App for _, in := range installed { installDto := dto.AppInstalled{ AppInstall: in, - AppName: in.App.Name, - Icon: in.App.Icon, } installDTOs = append(installDTOs, installDto) } @@ -145,6 +143,10 @@ func (a AppService) GetAppDetail(appId uint, version string) (dto.AppDetailDTO, return appDetailDTO, nil } +func (a AppService) PageInstallBackups(req dto.AppBackupRequest) (int64, []model.AppInstallBackup, error) { + return appInstallBackupRepo.Page(req.Page, req.PageSize, appInstallBackupRepo.WithAppInstallID(req.AppInstallID)) +} + func (a AppService) OperateInstall(req dto.AppInstallOperate) error { install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallId)) if err != nil { @@ -186,9 +188,7 @@ func (a AppService) OperateInstall(req dto.AppInstallOperate) error { } } - tx := global.DB.Begin() - ctx := context.WithValue(context.Background(), "db", tx) - + tx, ctx := getTxAndContext() if err := appInstallRepo.Delete(ctx, install); err != nil { tx.Rollback() return err @@ -204,6 +204,13 @@ func (a AppService) OperateInstall(req dto.AppInstallOperate) error { return err } return nil + case dto.Backup: + tx, ctx := getTxAndContext() + if err := backupInstall(ctx, install); err != nil { + tx.Rollback() + } + tx.Commit() + return nil default: return errors.New("operate not support") } @@ -284,8 +291,7 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int return err } - tx := global.DB.Begin() - ctx := context.WithValue(context.Background(), "db", tx) + tx, ctx := getTxAndContext() if err := appInstallRepo.Create(ctx, &appInstall); err != nil { tx.Rollback() return err @@ -314,6 +320,24 @@ func (a AppService) SyncAllInstalled() error { return nil } +func (a AppService) DeleteBackup(req dto.AppBackupDeleteRequest) error { + + backups, err := appInstallBackupRepo.GetBy(commonRepo.WithIdsIn(req.Ids)) + if err != nil { + return err + } + fileOp := files.NewFileOp() + + for _, backup := range backups { + dst := path.Join(backup.Path, backup.Name) + if err := fileOp.DeleteFile(dst); err != nil { + continue + } + appInstallBackupRepo.Delete(commonRepo.WithIdsIn(req.Ids)) + } + return nil +} + func (a AppService) GetServices(key string) ([]dto.AppService, error) { app, err := appRepo.GetFirst(appRepo.WithKey(key)) if err != nil { @@ -425,7 +449,7 @@ func (a AppService) SyncInstalled(installId uint) error { func (a AppService) SyncAppList() error { //TODO 从 oss 拉取最新列表 - appDir := path.Join(global.CONF.System.ResourceDir, "apps") + appDir := constant.AppResourceDir iconDir := path.Join(appDir, "icons") listFile := path.Join(appDir, "list.json") diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go index cc8017b11..9ac011cea 100644 --- a/backend/app/service/app_utils.go +++ b/backend/app/service/app_utils.go @@ -7,7 +7,6 @@ import ( "github.com/1Panel-dev/1Panel/app/dto" "github.com/1Panel-dev/1Panel/app/model" "github.com/1Panel-dev/1Panel/constant" - "github.com/1Panel-dev/1Panel/global" "github.com/1Panel-dev/1Panel/utils/cmd" "github.com/1Panel-dev/1Panel/utils/common" "github.com/1Panel-dev/1Panel/utils/compose" @@ -18,6 +17,7 @@ import ( "path" "reflect" "strconv" + "time" ) type DatabaseOp string @@ -52,8 +52,8 @@ func getSqlStr(key string, operate DatabaseOp, exec dto.ContainerExec) string { switch key { case "mysql": if operate == Add { - str = fmt.Sprintf("docker exec -i %s mysql -uroot -p%s -e \"CREATE USER '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%';\"", - exec.ContainerName, exec.Auth.RootPassword, param.DbUser, param.Password, param.DbName, param.DbName, param.DbUser) + str = fmt.Sprintf("docker exec -i %s mysql -uroot -p%s -e \"CREATE USER '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%' IDENTIFIED BY '%s';\"", + exec.ContainerName, exec.Auth.RootPassword, param.DbUser, param.Password, param.DbName, param.DbName, param.DbUser, param.Password) } if operate == Delete { str = fmt.Sprintf("docker exec -i %s mysql -uroot -p%s -e \"drop database %s;\" -e \"drop user %s;\" ", @@ -160,6 +160,28 @@ func deleteLink(ctx context.Context, install *model.AppInstall) error { return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID)) } +func backupInstall(ctx context.Context, install model.AppInstall) error { + var backup model.AppInstallBackup + appPath := install.GetPath() + backupDir := path.Join(constant.BackupDir, install.App.Key, install.Name) + fileOp := files.NewFileOp() + now := time.Now() + day := now.Format("2006-01-02-15-04") + fileName := fmt.Sprintf("%s-%s-%s%s", install.Name, install.Version, day, ".tar.gz") + if err := fileOp.Compress([]string{appPath}, backupDir, fileName, files.TarGz); err != nil { + return err + } + backup.Name = fileName + backup.Path = backupDir + backup.AppInstallId = install.ID + backup.AppDetailId = install.AppDetailId + + if err := appInstallBackupRepo.Create(ctx, backup); err != nil { + return err + } + return nil +} + func getContainerNames(install model.AppInstall) ([]string, error) { composeMap := install.DockerCompose envMap := make(map[string]string) @@ -220,8 +242,8 @@ func checkRequiredAndLimit(app model.App) error { } func copyAppData(key, version, installName string, params map[string]interface{}) (err error) { - resourceDir := path.Join(global.CONF.System.ResourceDir, "apps", key, version) - installDir := path.Join(global.CONF.System.AppDir, key) + resourceDir := path.Join(constant.AppResourceDir, key, version) + installDir := path.Join(constant.AppInstallDir, key) installVersionDir := path.Join(installDir, version) fileOp := files.NewFileOp() if err = fileOp.Copy(resourceDir, installVersionDir); err != nil { diff --git a/backend/app/service/entry.go b/backend/app/service/entry.go index d3ad3316d..27f800ce3 100644 --- a/backend/app/service/entry.go +++ b/backend/app/service/entry.go @@ -33,4 +33,5 @@ var ( appInstallRepo = repo.RepoGroupApp.AppInstallRepo appInstallResourceRepo = repo.RepoGroupApp.AppInstallResourceRpo dataBaseRepo = repo.RepoGroupApp.DatabaseRepo + appInstallBackupRepo = repo.RepoGroupApp.AppInstallBackupRepo ) diff --git a/backend/app/service/helper.go b/backend/app/service/helper.go new file mode 100644 index 000000000..227dbe921 --- /dev/null +++ b/backend/app/service/helper.go @@ -0,0 +1,13 @@ +package service + +import ( + "context" + "github.com/1Panel-dev/1Panel/global" + "gorm.io/gorm" +) + +func getTxAndContext() (tx *gorm.DB, ctx context.Context) { + tx = global.DB.Begin() + ctx = context.WithValue(context.Background(), "db", tx) + return +} diff --git a/backend/configs/system.go b/backend/configs/system.go index c1b72ece3..076efa413 100644 --- a/backend/configs/system.go +++ b/backend/configs/system.go @@ -1,11 +1,9 @@ package configs type System struct { - Port int `mapstructure:"port"` - DbType string `mapstructure:"db_type"` - Level string `mapstructure:"level"` - DataDir string `mapstructure:"data_dir"` - ResourceDir string `mapstructure:"resource_dir"` - AppDir string `mapstructure:"app_dir"` - AppOss string `mapstructure:"app_oss"` + Port int `mapstructure:"port"` + DbType string `mapstructure:"db_type"` + Level string `mapstructure:"level"` + DataDir string `mapstructure:"data_dir"` + AppOss string `mapstructure:"app_oss"` } diff --git a/backend/constant/dir.go b/backend/constant/dir.go new file mode 100644 index 000000000..a0098a519 --- /dev/null +++ b/backend/constant/dir.go @@ -0,0 +1,14 @@ +package constant + +import ( + "path" +) + +var ( + DefaultDataDir = "/opt/1Panel/data" + ResourceDir = path.Join(DefaultDataDir, "resource") + AppResourceDir = path.Join(ResourceDir, "apps") + AppInstallDir = path.Join(DefaultDataDir, "apps") + BackupDir = path.Join(DefaultDataDir, "backup") + AppBackupDir = path.Join(BackupDir, "apps") +) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index c3fc6c03f..797c792d1 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -150,6 +150,6 @@ var AddTableCronjob = &gormigrate.Migration{ var AddTableApp = &gormigrate.Migration{ ID: "20200921-add-table-app", Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppInstall{}, &model.AppInstallResource{}, &model.Database{}) + return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppInstall{}, &model.AppInstallResource{}, &model.Database{}, &model.AppInstallBackup{}) }, } diff --git a/backend/init/viper/viper.go b/backend/init/viper/viper.go index e646d08f3..db2180c92 100644 --- a/backend/init/viper/viper.go +++ b/backend/init/viper/viper.go @@ -2,9 +2,10 @@ package viper import ( "fmt" - "github.com/1Panel-dev/1Panel/configs" + "github.com/1Panel-dev/1Panel/constant" "github.com/1Panel-dev/1Panel/global" + "path" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" @@ -30,4 +31,16 @@ func Init() { panic(err) } global.CONF = serverConfig + InitDir() +} + +func InitDir() { + constant.DefaultDataDir = "/opt/1Panel/data" + constant.ResourceDir = path.Join(constant.DefaultDataDir, "resource") + constant.AppResourceDir = path.Join(constant.ResourceDir, "apps") + constant.AppInstallDir = path.Join(constant.DefaultDataDir, "apps") + constant.BackupDir = path.Join(constant.DefaultDataDir, "backup") + constant.AppBackupDir = path.Join(constant.BackupDir, "apps") + + //TODO 创建文件夹 } diff --git a/backend/router/ro_app.go b/backend/router/ro_app.go index 9dd23ac35..cb36fc340 100644 --- a/backend/router/ro_app.go +++ b/backend/router/ro_app.go @@ -23,6 +23,8 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) { appRouter.POST("/installed", baseApi.SearchInstalled) appRouter.POST("/installed/op", baseApi.OperateInstalled) appRouter.POST("/installed/sync", baseApi.SyncInstalled) + appRouter.POST("/installed/backups", baseApi.SearchInstalledBackup) + appRouter.POST("/installed/backups/del", baseApi.DeleteAppBackup) appRouter.GET("/services/:key", baseApi.GetServices) } } diff --git a/backend/utils/files/file_op.go b/backend/utils/files/file_op.go index 82ca289a6..bfaddc382 100644 --- a/backend/utils/files/file_op.go +++ b/backend/utils/files/file_op.go @@ -341,6 +341,10 @@ func (f FileOp) Compress(srcRiles []string, dst string, name string, cType Compr fileMaps[s] = base } + if !f.Stat(dst) { + _ = f.CreateDir(dst, 0755) + } + files, err := archiver.FilesFromDisk(nil, fileMaps) if err != nil { return err diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index ccffbfb6d..752c1bdaa 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -66,12 +66,12 @@ export namespace App { name: string; appId: string; appDetailId: string; - params: string; + env: string; status: string; description: string; message: string; - appName: string; icon: string; + canUpdate: boolean; app: App; } @@ -84,4 +84,19 @@ export namespace App { label: string; value: string; } + + export interface AppBackupReq extends ReqPage { + appInstallId: number; + } + + export interface AppBackupDelReq { + ids: number[]; + } + + export interface AppBackup extends CommonModel { + name: string; + path: string; + appInstallId: string; + appDetail: AppDetail; + } } diff --git a/frontend/src/api/modules/app.ts b/frontend/src/api/modules/app.ts index a64534aa9..340aaa041 100644 --- a/frontend/src/api/modules/app.ts +++ b/frontend/src/api/modules/app.ts @@ -37,3 +37,11 @@ export const SyncInstalledApp = () => { export const GetAppService = (key: string | undefined) => { return http.get(`apps/services/${key}`); }; + +export const GetAppBackups = (info: App.AppBackupReq) => { + return http.post>('apps/installed/backups', info); +}; + +export const DelAppBackups = (req: App.AppBackupDelReq) => { + return http.post('apps/installed/backups/del', req); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 97c8ffa56..0de8daf48 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -418,5 +418,9 @@ export default { deleteWarn: 'Delete the operation data and delete the operation data. This operation cannot be rolled back. Do you want to continue?', canUpdate: 'CanUpdate', + backup: 'Backup', + backupName: 'Filename', + backupPath: 'Filepath', + backupdate: 'Backup Date', }, }; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 73fcfb1c4..d96d634af 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -410,5 +410,9 @@ export default { deleteWarn: '删除操作会把数据一并删除,此操作不可回滚,是否继续?', syncSuccess: '同步成功', canUpdate: '可更新', + backup: '备份', + backupName: '文件名称', + backupPath: '文件路径', + backupdate: '备份时间', }, }; diff --git a/frontend/src/views/app-store/installed/backups.vue b/frontend/src/views/app-store/installed/backups.vue new file mode 100644 index 000000000..87d1caecc --- /dev/null +++ b/frontend/src/views/app-store/installed/backups.vue @@ -0,0 +1,109 @@ + + + diff --git a/frontend/src/views/app-store/installed/index.vue b/frontend/src/views/app-store/installed/index.vue index cba490ab6..c77d07ee6 100644 --- a/frontend/src/views/app-store/installed/index.vue +++ b/frontend/src/views/app-store/installed/index.vue @@ -10,8 +10,15 @@ - + + + + +