From 40f02889fe6bb4a6f463e592a94b050948d60c0b Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:15:35 +0800 Subject: [PATCH] feat: Add import/export support for cronjob (#9663) Refs #5157 --- agent/app/api/v2/cronjob.go | 45 ++++ agent/app/dto/common_req.go | 3 + agent/app/dto/cronjob.go | 47 ++++ agent/app/repo/app_install.go | 21 ++ agent/app/service/cronjob.go | 201 ++++++++++++++++++ agent/app/service/cronjob_backup.go | 34 +-- agent/constant/status.go | 1 + agent/router/ro_cronjob.go | 2 + frontend/src/api/interface/cronjob.ts | 42 ++++ frontend/src/api/modules/cronjob.ts | 7 + frontend/src/lang/modules/en.ts | 5 + frontend/src/lang/modules/ja.ts | 5 + frontend/src/lang/modules/ko.ts | 5 + frontend/src/lang/modules/ms.ts | 5 + frontend/src/lang/modules/pt-br.ts | 5 + frontend/src/lang/modules/ru.ts | 5 + frontend/src/lang/modules/tr.ts | 5 + frontend/src/lang/modules/zh-Hant.ts | 5 + frontend/src/lang/modules/zh.ts | 5 + .../views/cronjob/cronjob/import/index.vue | 149 +++++++++++++ frontend/src/views/cronjob/cronjob/index.vue | 61 +++++- .../views/cronjob/cronjob/operate/index.vue | 8 +- 22 files changed, 644 insertions(+), 22 deletions(-) create mode 100644 frontend/src/views/cronjob/cronjob/import/index.vue diff --git a/agent/app/api/v2/cronjob.go b/agent/app/api/v2/cronjob.go index be9b33de4..4fcecf482 100644 --- a/agent/app/api/v2/cronjob.go +++ b/agent/app/api/v2/cronjob.go @@ -1,6 +1,8 @@ package v2 import ( + "net/http" + "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" @@ -53,6 +55,49 @@ func (b *BaseApi) LoadCronjobInfo(c *gin.Context) { helper.SuccessWithData(c, data) } +// @Tags Cronjob +// @Summary Export cronjob list +// @Accept json +// @Param request body dto.OperateByIDs true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /cronjobs/export [post] +func (b *BaseApi) ExportCronjob(c *gin.Context) { + var req dto.OperateByIDs + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + content, err := cronjobService.Export(req) + if err != nil { + helper.InternalServer(c, err) + return + } + http.ServeContent(c.Writer, c.Request, "", time.Now(), strings.NewReader(content)) +} + +// @Tags Cronjob +// @Summary Import cronjob list +// @Accept json +// @Param request body dto.CronjobImport true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /cronjobs/import [post] +func (b *BaseApi) ImportCronjob(c *gin.Context) { + var req dto.CronjobImport + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := cronjobService.Import(req.Cronjobs); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + // @Tags Cronjob // @Summary Load script options // @Success 200 {array} dto.ScriptOptions diff --git a/agent/app/dto/common_req.go b/agent/app/dto/common_req.go index 89f419400..311dce14a 100644 --- a/agent/app/dto/common_req.go +++ b/agent/app/dto/common_req.go @@ -28,6 +28,9 @@ type OperationWithName struct { type OperateByID struct { ID uint `json:"id" validate:"required"` } +type OperateByIDs struct { + IDs []uint `json:"ids"` +} type Operate struct { Operation string `json:"operation" validate:"required"` diff --git a/agent/app/dto/cronjob.go b/agent/app/dto/cronjob.go index 1f397e6c8..823fbe8cf 100644 --- a/agent/app/dto/cronjob.go +++ b/agent/app/dto/cronjob.go @@ -123,6 +123,53 @@ type CronjobInfo struct { AlertCount uint `json:"alertCount"` } +type CronjobImport struct { + Cronjobs []CronjobTrans `json:"cronjobs"` +} +type CronjobTrans struct { + Name string `json:"name"` + Type string `json:"type"` + SpecCustom bool `json:"specCustom"` + Spec string `json:"spec"` + + Executor string `json:"executor"` + ScriptMode string `json:"scriptMode"` + Script string `json:"script"` + Command string `json:"command"` + ContainerName string `json:"containerName"` + User string `json:"user"` + URL string `json:"url"` + + ScriptName string `json:"scriptName"` + Apps []TransHelper `json:"apps"` + Websites []string `json:"websites"` + DBType string `json:"dbType"` + DBNames []TransHelper `json:"dbName"` + + ExclusionRules string `json:"exclusionRules"` + + IsDir bool `json:"isDir"` + SourceDir string `json:"sourceDir"` + + RetainCopies uint64 `json:"retainCopies"` + RetryTimes uint `json:"retryTimes"` + Timeout uint `json:"timeout"` + IgnoreErr bool `json:"ignoreErr"` + SnapshotRule string `json:"snapshotRule"` + Secret string `json:"secret"` + + SourceAccounts []string `json:"sourceAccounts"` + DownloadAccount string `json:"downloadAccount"` + + AlertCount uint `json:"alertCount"` + AlertTitle string `json:"alertTitle"` + AlertMethod string `json:"alertMethod"` +} +type TransHelper struct { + Name string `json:"name"` + DetailName string `json:"detailName"` +} + type ScriptOptions struct { ID uint `json:"id"` Name string `json:"name"` diff --git a/agent/app/repo/app_install.go b/agent/app/repo/app_install.go index ec1c80561..20ea7707f 100644 --- a/agent/app/repo/app_install.go +++ b/agent/app/repo/app_install.go @@ -36,6 +36,7 @@ type IAppInstallRepo interface { Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error LoadBaseInfo(key string, name string) (*RootInfo, error) + LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error) GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error) } @@ -246,3 +247,23 @@ func (a *AppInstallRepo) LoadBaseInfo(key string, name string) (*RootInfo, error info.Status = appInstall.Status return &info, nil } + +func (a *AppInstallRepo) LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error) { + var ( + app model.App + appInstall model.AppInstall + ) + if err := global.DB.Where("key = ?", key).First(&app).Error; err != nil { + return nil, err + } + if len(name) == 0 { + if err := global.DB.Where("app_id = ?", app.ID).First(&appInstall).Error; err != nil { + return nil, err + } + } else { + if err := global.DB.Where("app_id = ? AND name = ?", app.ID, name).First(&appInstall).Error; err != nil { + return nil, err + } + } + return &appInstall, nil +} diff --git a/agent/app/service/cronjob.go b/agent/app/service/cronjob.go index bfc124ff8..cb55343ce 100644 --- a/agent/app/service/cronjob.go +++ b/agent/app/service/cronjob.go @@ -35,6 +35,8 @@ type ICronjobService interface { StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error) CleanRecord(req dto.CronjobClean) error + Export(req dto.OperateByIDs) (string, error) + Import(req []dto.CronjobTrans) error LoadScriptOptions() []dto.ScriptOptions LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error) @@ -102,6 +104,202 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err return &item, err } +func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) { + cronjobs, err := cronjobRepo.List(repo.WithByIDs(req.IDs)) + if err != nil { + return "", err + } + var data []dto.CronjobTrans + for _, cronjob := range cronjobs { + item := dto.CronjobTrans{ + Name: cronjob.Name, + Type: cronjob.Type, + SpecCustom: cronjob.SpecCustom, + Spec: cronjob.Spec, + Executor: cronjob.Executor, + ScriptMode: cronjob.ScriptMode, + Script: cronjob.Script, + Command: cronjob.Command, + ContainerName: cronjob.ContainerName, + User: cronjob.User, + URL: cronjob.URL, + DBType: cronjob.DBType, + ExclusionRules: cronjob.ExclusionRules, + IsDir: cronjob.IsDir, + SourceDir: cronjob.SourceDir, + RetainCopies: cronjob.RetainCopies, + RetryTimes: cronjob.RetryTimes, + Timeout: cronjob.Timeout, + IgnoreErr: cronjob.IgnoreErr, + Secret: cronjob.Secret, + SnapshotRule: cronjob.SnapshotRule, + } + switch cronjob.Type { + case "app": + if cronjob.AppID == "all" { + break + } + apps := loadAppsForJob(cronjob) + for _, app := range apps { + item.Apps = append(item.Apps, dto.TransHelper{Name: app.App.Key, DetailName: app.Name}) + } + case "website": + if cronjob.Website == "all" { + break + } + websites := loadWebsForJob(cronjob) + for _, website := range websites { + item.Websites = append(item.Websites, website.Alias) + } + case "database": + if cronjob.DBName == "all" { + break + } + databases := loadDbsForJob(cronjob) + for _, db := range databases { + item.DBNames = append(item.DBNames, dto.TransHelper{Name: db.Database, DetailName: db.Name}) + } + } + item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID) + alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable)) + if alertInfo.SendCount != 0 { + item.AlertCount = alertInfo.SendCount + item.AlertTitle = alertInfo.Title + item.AlertMethod = alertInfo.Method + } else { + item.AlertCount = 0 + } + data = append(data, item) + } + itemJson, err := json.Marshal(&data) + if err != nil { + return "", err + } + return string(itemJson), nil +} + +func (u *CronjobService) Import(req []dto.CronjobTrans) error { + for _, item := range req { + cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name)) + if cronjobItem.ID != 0 { + continue + } + cronjob := model.Cronjob{ + Name: item.Name, + Type: item.Type, + SpecCustom: item.SpecCustom, + Spec: item.Spec, + Executor: item.Executor, + ScriptMode: item.ScriptMode, + Script: item.Script, + Command: item.Command, + ContainerName: item.ContainerName, + User: item.User, + URL: item.URL, + DBType: item.DBType, + ExclusionRules: item.ExclusionRules, + IsDir: item.IsDir, + SourceDir: item.SourceDir, + RetainCopies: item.RetainCopies, + RetryTimes: item.RetryTimes, + Timeout: item.Timeout, + IgnoreErr: item.IgnoreErr, + Secret: item.Secret, + SnapshotRule: item.SnapshotRule, + } + hasNotFound := false + switch item.Type { + case "app": + if len(item.Apps) == 0 { + cronjob.AppID = "all" + break + } + var appIDs []string + for _, app := range item.Apps { + appItem, err := appInstallRepo.LoadInstallAppByKeyAndName(app.Name, app.DetailName) + if err != nil { + hasNotFound = true + continue + } + appIDs = append(appIDs, fmt.Sprintf("%v", appItem.ID)) + } + cronjob.AppID = strings.Join(appIDs, ",") + case "website": + if len(item.Websites) == 0 { + cronjob.Website = "all" + break + } + var webIDs []string + for _, web := range item.Websites { + webItem, err := websiteRepo.GetFirst(websiteRepo.WithAlias(web)) + if err != nil { + hasNotFound = true + continue + } + webIDs = append(webIDs, fmt.Sprintf("%v", webItem.ID)) + } + cronjob.Website = strings.Join(webIDs, ",") + case "database": + if len(item.DBNames) == 0 { + cronjob.DBName = "all" + break + } + var dbIDs []string + if cronjob.DBType == "postgresql" { + for _, db := range item.DBNames { + dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(db.Name), repo.WithByName(db.DetailName)) + if err != nil { + hasNotFound = true + continue + } + dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID)) + } + } else { + for _, db := range item.DBNames { + dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(db.Name), repo.WithByName(db.DetailName)) + if err != nil { + hasNotFound = true + continue + } + dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID)) + } + } + cronjob.DBName = strings.Join(dbIDs, ",") + } + var acIDs []string + for _, ac := range item.SourceAccounts { + backup, err := backupRepo.Get(repo.WithByName(ac)) + if err != nil { + hasNotFound = true + continue + } + if ac == item.DownloadAccount { + cronjob.DownloadAccountID = backup.ID + } + acIDs = append(acIDs, fmt.Sprintf("%v", backup.ID)) + } + cronjob.SourceAccountIDs = strings.Join(acIDs, ",") + if hasNotFound { + cronjob.Status = constant.StatusPending + } else { + cronjob.Status = constant.StatusDisable + } + if item.AlertCount != 0 { + createAlert := dto.AlertCreate{ + Title: item.AlertTitle, + SendCount: item.AlertCount, + Method: item.AlertMethod, + Type: cronjob.Type, + Project: strconv.Itoa(int(cronjob.ID)), + Status: constant.AlertEnable, + } + _ = NewIAlertService().CreateAlert(createAlert) + } + _ = cronjobRepo.Create(&cronjob) + } + return nil +} + func (u *CronjobService) LoadScriptOptions() []dto.ScriptOptions { scripts, err := scriptRepo.List() if err != nil { @@ -411,6 +609,9 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error { } } + if cronModel.Status == constant.StatusPending { + upMap["status"] = constant.StatusEnable + } upMap["name"] = req.Name upMap["spec_custom"] = req.SpecCustom upMap["spec"] = spec diff --git a/agent/app/service/cronjob_backup.go b/agent/app/service/cronjob_backup.go index 3722a4870..c5765681f 100644 --- a/agent/app/service/cronjob_backup.go +++ b/agent/app/service/cronjob_backup.go @@ -24,22 +24,7 @@ import ( ) func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error { - var apps []model.AppInstall - if cronjob.AppID == "all" { - apps, _ = appInstallRepo.ListBy(context.Background()) - } else { - appIds := strings.Split(cronjob.AppID, ",") - var idItems []uint - for i := 0; i < len(appIds); i++ { - itemID, _ := strconv.Atoi(appIds[i]) - idItems = append(idItems, uint(itemID)) - } - appItems, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(idItems)) - if err != nil { - return err - } - apps = appItems - } + apps := loadAppsForJob(cronjob) if len(apps) == 0 { return errors.New("no such app in database!") } @@ -344,6 +329,23 @@ func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.J return nil } +func loadAppsForJob(cronjob model.Cronjob) []model.AppInstall { + var apps []model.AppInstall + if cronjob.AppID == "all" { + apps, _ = appInstallRepo.ListBy(context.Background()) + } else { + appIds := strings.Split(cronjob.AppID, ",") + var idItems []uint + for i := 0; i < len(appIds); i++ { + itemID, _ := strconv.Atoi(appIds[i]) + idItems = append(idItems, uint(itemID)) + } + appItems, _ := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(idItems)) + apps = appItems + } + return apps +} + type DatabaseHelper struct { ID uint DBType string diff --git a/agent/constant/status.go b/agent/constant/status.go index f772bf406..315ed354a 100644 --- a/agent/constant/status.go +++ b/agent/constant/status.go @@ -5,6 +5,7 @@ const ( StatusCanceled = "Canceled" StatusDone = "Done" StatusWaiting = "Waiting" + StatusPending = "Pending" StatusSuccess = "Success" StatusFailed = "Failed" StatusUploading = "Uploading" diff --git a/agent/router/ro_cronjob.go b/agent/router/ro_cronjob.go index c8e1fb1a6..277938a57 100644 --- a/agent/router/ro_cronjob.go +++ b/agent/router/ro_cronjob.go @@ -13,6 +13,8 @@ func (s *CronjobRouter) InitRouter(Router *gin.RouterGroup) { { cmdRouter.POST("", baseApi.CreateCronjob) cmdRouter.POST("/next", baseApi.LoadNextHandle) + cmdRouter.POST("/import", baseApi.ImportCronjob) + cmdRouter.POST("/export", baseApi.ExportCronjob) cmdRouter.POST("/load/info", baseApi.LoadCronjobInfo) cmdRouter.GET("/script/options", baseApi.LoadScriptOptions) cmdRouter.POST("/del", baseApi.DeleteCronjob) diff --git a/frontend/src/api/interface/cronjob.ts b/frontend/src/api/interface/cronjob.ts index 007d4745e..35e03a538 100644 --- a/frontend/src/api/interface/cronjob.ts +++ b/frontend/src/api/interface/cronjob.ts @@ -101,6 +101,48 @@ export namespace Cronjob { alertTitle: string; alertMethod: string; } + export interface CronjobTrans { + name: string; + type: string; + specCustom: boolean; + spec: string; + group: string; + + executor: string; + scriptMode: string; + script: string; + command: string; + containerName: string; + user: string; + url: string; + + scriptName: string; + apps: Array; + websites: Array; + dbType: string; + dbNames: Array; + + exclusionRules: string; + + isDir: boolean; + sourceDir: string; + + retainCopies: number; + retryTimes: number; + timeout: number; + ignoreErr: boolean; + snapshotRule: string; + secret: string; + + sourceAccounts: Array; + downloadAccount: string; + + alertCount: number; + } + export interface TransHelper { + name: string; + detailName: string; + } export interface snapshotRule { withImage: boolean; ignoreAppIDs: Array; diff --git a/frontend/src/api/modules/cronjob.ts b/frontend/src/api/modules/cronjob.ts index 4ae4796a1..92258fcc2 100644 --- a/frontend/src/api/modules/cronjob.ts +++ b/frontend/src/api/modules/cronjob.ts @@ -11,6 +11,13 @@ export const loadNextHandle = (spec: string) => { return http.post>(`/cronjobs/next`, { spec: spec }); }; +export const importCronjob = (trans: Array) => { + return http.post('cronjobs/import', { cronjobs: trans }, TimeoutEnum.T_60S); +}; +export const exportCronjob = (params: { ids: Array }) => { + return http.download('cronjobs/export', params, { responseType: 'blob', timeout: TimeoutEnum.T_40S }); +}; + export const loadCronjobInfo = (id: number) => { return http.post(`/cronjobs/load/info`, { id: id }); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a1cdcf091..b915b1840 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -50,6 +50,7 @@ const message = { verify: 'Verify', saveAndEnable: 'Save and enable', import: 'Import', + export: 'Export', power: 'Authorization', search: 'Search', refresh: 'Refresh', @@ -298,6 +299,7 @@ const message = { normal: 'Normal', building: 'Building', upgrading: 'Upgrading', + pending: 'Pending Edit', rebuilding: 'Rebuilding', deny: 'Denied', accept: 'Accepted', @@ -988,6 +990,9 @@ const message = { cronjob: { create: 'Create cron job', edit: 'Edit cron job', + errImport: 'File content exception:', + importHelper: + 'Duplicate scheduled tasks will be automatically skipped during import. Tasks will be set to [Disabled] status by default, and set to [Pending Edit] status when data association is abnormal.', changeStatus: 'Change status', disableMsg: 'This will stop the scheduled task from automatically executing. Do you want to continue?', enableMsg: 'This will allow the scheduled task to automatically execute. Do you want to continue?', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index b143196a6..585055f49 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -48,6 +48,7 @@ const message = { verify: '確認する', saveAndEnable: '保存して有効にします', import: '輸入', + export: 'エクスポート', power: '認可', search: '検索', refresh: 'リロード', @@ -287,6 +288,7 @@ const message = { normal: '普通', building: '建物', upgrading: 'アップグレード', + pending: '編集待ち', rebuilding: '再構築', deny: '拒否されました', accept: '受け入れられました', @@ -958,6 +960,9 @@ const message = { cronjob: { create: 'Cronジョブを作成します', edit: 'Cronジョブを編集します', + errImport: 'ファイル内容異常:', + importHelper: + 'インポート時に同名のスケジュールタスクは自動的にスキップされます。タスクはデフォルトで【無効】状態に設定され、データ関連付け異常時には【編集待ち】状態に設定されます。', changeStatus: 'ステータスを変更します', disableMsg: 'これにより、スケジュールされたタスクが自動的に実行されなくなります。続けたいですか?', enableMsg: 'これにより、スケジュールされたタスクが自動的に実行されます。続けたいですか?', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index b4e81331c..1151db8d3 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -48,6 +48,7 @@ const message = { verify: '검증', saveAndEnable: '저장 및 활성화', import: '가져오기', + export: '내보내기', power: '권한 부여', search: '검색', refresh: '새로고침', @@ -289,6 +290,7 @@ const message = { normal: '정상', building: '빌드 중', upgrading: '업그레이드 중', + pending: '편집 대기', rebuilding: '재빌드 중', deny: '거부됨', accept: '수락됨', @@ -948,6 +950,9 @@ const message = { cronjob: { create: '크론 작업 생성', edit: '크론 작업 수정', + errImport: '파일 내용 이상:', + importHelper: + '가져오기 시 동일한 이름의 예약 작업은 자동으로 건너뜁니다. 작업은 기본적으로 【비활성화】 상태로 설정되며, 데이터 연동 이상 시 【편집 대기】 상태로 설정됩니다.', changeStatus: '상태 변경', disableMsg: '이 작업은 예약된 작업이 자동으로 실행되지 않도록 멈춥니다. 계속하시겠습니까?', enableMsg: '이 작업은 예약된 작업이 자동으로 실행되도록 허용합니다. 계속하시겠습니까?', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index eaaa4f5f1..3727748c9 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -48,6 +48,7 @@ const message = { verify: 'Sahkan', saveAndEnable: 'Simpan dan aktifkan', import: 'Import', + export: 'Eksport', power: 'Pemberian Kuasa', search: 'Cari', refresh: 'Segarkan', @@ -295,6 +296,7 @@ const message = { normal: 'Normal', building: 'Sedang Membina', upgrading: 'Sedang Meningkatkan', + pending: 'Menunggu Edit', rebuilding: 'Sedang Membina Semula', deny: 'Ditolak', accept: 'Diterima', @@ -979,6 +981,9 @@ const message = { cronjob: { create: 'Cipta tugas cron', edit: 'Edit tugas cron', + errImport: 'Kandungan fail tidak normal:', + importHelper: + 'Tugas terjadual dengan nama sama akan dilangkau secara automatik semasa import. Tugas akan ditetapkan ke status 【Lumpuh】 secara lalai, dan ditetapkan ke status 【Menunggu Edit】 apabila perkaitan data tidak normal.', changeStatus: 'Tukar status', disableMsg: 'Ini akan menghentikan tugas berjadual daripada dilaksanakan secara automatik. Adakah anda mahu meneruskan?', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index a15d3ed17..652e0f052 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -48,6 +48,7 @@ const message = { verify: 'Verificar', saveAndEnable: 'Salvar e ativar', import: 'Importar', + export: 'Exportar', power: 'Autorização', search: 'Pesquisar', refresh: 'Atualizar', @@ -293,6 +294,7 @@ const message = { normal: 'Normal', building: 'Construindo', upgrading: 'Atualizando', + pending: 'Aguardando Edição', rebuilding: 'Reconstruindo', deny: 'Negado', accept: 'Aceito', @@ -975,6 +977,9 @@ const message = { cronjob: { create: 'Criar tarefa cron', edit: 'Editar tarefa cron', + errImport: 'Conteúdo do arquivo anormal:', + importHelper: + 'Tarefas agendadas duplicadas serão automaticamente ignoradas durante a importação. As tarefas serão definidas como status 【Desativado】 por padrão, e como status 【Aguardando Edição】 quando a associação de dados for anormal.', changeStatus: 'Alterar status', disableMsg: 'Isso irá parar a execução automática da tarefa agendada. Você deseja continuar?', enableMsg: 'Isso permitirá que a tarefa agendada seja executada automaticamente. Você deseja continuar?', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 04cc81cc6..288a5c74d 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -48,6 +48,7 @@ const message = { verify: 'Проверить', saveAndEnable: 'Сохранить и включить', import: 'Импорт', + export: 'Экспорт', power: 'Авторизация', search: 'Поиск', refresh: 'Обновить', @@ -290,6 +291,7 @@ const message = { normal: 'Нормально', building: 'Сборка', upgrading: 'Обновление', + pending: 'Ожидает редактирования', rebuilding: 'Пересборка', deny: 'Отказано', accept: 'Принято', @@ -972,6 +974,9 @@ const message = { cronjob: { create: 'Создать задачу cron', edit: 'Редактировать задачу cron', + errImport: 'Аномальное содержимое файла:', + importHelper: + 'Повторяющиеся запланированные задачи будут автоматически пропущены при импорте. По умолчанию задачи устанавливаются в статус 【Отключено】, а при аномальной ассоциации данных - в статус 【Ожидает редактирования】.', changeStatus: 'Изменить статус', disableMsg: 'Это остановит автоматическое выполнение запланированной задачи. Хотите продолжить?', enableMsg: 'Это позволит запланированной задаче автоматически выполняться. Хотите продолжить?', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 466b48541..7e97ea0bc 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -50,6 +50,7 @@ const message = { verify: 'Doğrula', saveAndEnable: 'Kaydet ve etkinleştir', import: 'İçe Aktar', + export: 'Dışa Aktar', power: 'Yetkilendirme', search: 'Ara', refresh: 'Yenile', @@ -302,6 +303,7 @@ const message = { normal: 'Normal', building: 'İnşa Ediliyor', upgrading: 'Yükseltiliyor', + pending: 'Düzenleme Bekliyor', rebuilding: 'Yeniden İnşa Ediliyor', deny: 'Reddedildi', accept: 'Kabul Edildi', @@ -1000,6 +1002,9 @@ const message = { cronjob: { create: 'Cron görevi oluştur', edit: 'Cron görevini düzenle', + errImport: 'Dosya içeriği anormal:', + importHelper: + 'İçe aktarım sırasında aynı isimli zamanlanmış görevler otomatik olarak atlanacaktır. Görevler varsayılan olarak 【Devre Dışı】 durumuna ayarlanır ve veri ilişkilendirme anormalse 【Düzenleme Bekliyor】 durumuna ayarlanır.', changeStatus: 'Durumu değiştir', disableMsg: 'Bu, zamanlanmış görevin otomatik olarak yürütülmesini durduracaktır. Devam etmek istiyor musunuz?', enableMsg: diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index a1b895c2d..9116c00bf 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -51,6 +51,7 @@ const message = { saveAndEnable: '保存並啟用', import: '導入', power: '授權', + export: '導出', search: '搜索', refresh: '刷新', get: '獲取', @@ -289,6 +290,7 @@ const message = { normal: '正常', building: '製作鏡像中', upgrading: '升級中', + pending: '待編輯', rebuilding: '重建中', deny: '已屏蔽', accept: '已放行', @@ -942,6 +944,9 @@ const message = { cronjob: { create: '創建計劃任務', edit: '編輯計劃任務', + errImport: '文件內容異常:', + importHelper: + '導入時將自動跳過重名計劃任務。任務默認設置為【停用】狀態,數據關聯異常時,設置為【待編輯】狀態。', changeStatus: '狀態修改', disableMsg: '停止計劃任務會導致該任務不再自動執行。是否繼續?', enableMsg: '啟用計劃任務會讓該任務定期自動執行。是否繼續?', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 4a1b49f7e..e035882e6 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -50,6 +50,7 @@ const message = { verify: '验证', saveAndEnable: '保存并启用', import: '导入', + export: '导出', power: '授权', search: '搜索', refresh: '刷新', @@ -287,6 +288,7 @@ const message = { normal: '正常', building: '制作镜像中', upgrading: '升级中', + pending: '待编辑', rebuilding: '重建中', deny: '已屏蔽', accept: '已放行', @@ -940,6 +942,9 @@ const message = { cronjob: { create: '创建计划任务', edit: '编辑计划任务', + errImport: '文件内容异常:', + importHelper: + '导入时将自动跳过重名计划任务。任务默认设置为【停用】状态,数据关联异常时,设置为【待编辑】状态。', changeStatus: '状态修改', disableMsg: '停止计划任务会导致该任务不再自动执行。是否继续?', enableMsg: '启用计划任务会让该任务定期自动执行。是否继续?', diff --git a/frontend/src/views/cronjob/cronjob/import/index.vue b/frontend/src/views/cronjob/cronjob/import/index.vue new file mode 100644 index 000000000..713d97182 --- /dev/null +++ b/frontend/src/views/cronjob/cronjob/import/index.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/frontend/src/views/cronjob/cronjob/index.vue b/frontend/src/views/cronjob/cronjob/index.vue index 163bdaf96..69785fc2b 100644 --- a/frontend/src/views/cronjob/cronjob/index.vue +++ b/frontend/src/views/cronjob/cronjob/index.vue @@ -16,6 +16,15 @@ {{ $t('commons.button.delete') }} + + + + {{ $t('commons.button.import') }} + + + {{ $t('commons.button.export') }} + + @@ -166,7 +176,9 @@ + + @@ -174,8 +186,9 @@