feat: Add import/export support for cronjob (#9663)

Refs #5157
This commit is contained in:
ssongliu 2025-07-25 15:15:35 +08:00 committed by GitHub
parent 4f8fb26d89
commit 40f02889fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 644 additions and 22 deletions

View file

@ -1,6 +1,8 @@
package v2 package v2
import ( import (
"net/http"
"strings"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "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) 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 // @Tags Cronjob
// @Summary Load script options // @Summary Load script options
// @Success 200 {array} dto.ScriptOptions // @Success 200 {array} dto.ScriptOptions

View file

@ -28,6 +28,9 @@ type OperationWithName struct {
type OperateByID struct { type OperateByID struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
} }
type OperateByIDs struct {
IDs []uint `json:"ids"`
}
type Operate struct { type Operate struct {
Operation string `json:"operation" validate:"required"` Operation string `json:"operation" validate:"required"`

View file

@ -123,6 +123,53 @@ type CronjobInfo struct {
AlertCount uint `json:"alertCount"` 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 { type ScriptOptions struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`

View file

@ -36,6 +36,7 @@ type IAppInstallRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error)
BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error
LoadBaseInfo(key string, name string) (*RootInfo, 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) 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 info.Status = appInstall.Status
return &info, nil 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
}

View file

@ -35,6 +35,8 @@ type ICronjobService interface {
StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error) StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error)
CleanRecord(req dto.CronjobClean) error CleanRecord(req dto.CronjobClean) error
Export(req dto.OperateByIDs) (string, error)
Import(req []dto.CronjobTrans) error
LoadScriptOptions() []dto.ScriptOptions LoadScriptOptions() []dto.ScriptOptions
LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error)
@ -102,6 +104,202 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
return &item, 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 { func (u *CronjobService) LoadScriptOptions() []dto.ScriptOptions {
scripts, err := scriptRepo.List() scripts, err := scriptRepo.List()
if err != nil { 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["name"] = req.Name
upMap["spec_custom"] = req.SpecCustom upMap["spec_custom"] = req.SpecCustom
upMap["spec"] = spec upMap["spec"] = spec

View file

@ -24,22 +24,7 @@ import (
) )
func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error { func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
var apps []model.AppInstall apps := loadAppsForJob(cronjob)
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
}
if len(apps) == 0 { if len(apps) == 0 {
return errors.New("no such app in database!") return errors.New("no such app in database!")
} }
@ -344,6 +329,23 @@ func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.J
return nil 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 { type DatabaseHelper struct {
ID uint ID uint
DBType string DBType string

View file

@ -5,6 +5,7 @@ const (
StatusCanceled = "Canceled" StatusCanceled = "Canceled"
StatusDone = "Done" StatusDone = "Done"
StatusWaiting = "Waiting" StatusWaiting = "Waiting"
StatusPending = "Pending"
StatusSuccess = "Success" StatusSuccess = "Success"
StatusFailed = "Failed" StatusFailed = "Failed"
StatusUploading = "Uploading" StatusUploading = "Uploading"

View file

@ -13,6 +13,8 @@ func (s *CronjobRouter) InitRouter(Router *gin.RouterGroup) {
{ {
cmdRouter.POST("", baseApi.CreateCronjob) cmdRouter.POST("", baseApi.CreateCronjob)
cmdRouter.POST("/next", baseApi.LoadNextHandle) cmdRouter.POST("/next", baseApi.LoadNextHandle)
cmdRouter.POST("/import", baseApi.ImportCronjob)
cmdRouter.POST("/export", baseApi.ExportCronjob)
cmdRouter.POST("/load/info", baseApi.LoadCronjobInfo) cmdRouter.POST("/load/info", baseApi.LoadCronjobInfo)
cmdRouter.GET("/script/options", baseApi.LoadScriptOptions) cmdRouter.GET("/script/options", baseApi.LoadScriptOptions)
cmdRouter.POST("/del", baseApi.DeleteCronjob) cmdRouter.POST("/del", baseApi.DeleteCronjob)

View file

@ -101,6 +101,48 @@ export namespace Cronjob {
alertTitle: string; alertTitle: string;
alertMethod: 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<TransHelper>;
websites: Array<string>;
dbType: string;
dbNames: Array<TransHelper>;
exclusionRules: string;
isDir: boolean;
sourceDir: string;
retainCopies: number;
retryTimes: number;
timeout: number;
ignoreErr: boolean;
snapshotRule: string;
secret: string;
sourceAccounts: Array<string>;
downloadAccount: string;
alertCount: number;
}
export interface TransHelper {
name: string;
detailName: string;
}
export interface snapshotRule { export interface snapshotRule {
withImage: boolean; withImage: boolean;
ignoreAppIDs: Array<Number>; ignoreAppIDs: Array<Number>;

View file

@ -11,6 +11,13 @@ export const loadNextHandle = (spec: string) => {
return http.post<Array<String>>(`/cronjobs/next`, { spec: spec }); return http.post<Array<String>>(`/cronjobs/next`, { spec: spec });
}; };
export const importCronjob = (trans: Array<Cronjob.CronjobTrans>) => {
return http.post('cronjobs/import', { cronjobs: trans }, TimeoutEnum.T_60S);
};
export const exportCronjob = (params: { ids: Array<number> }) => {
return http.download<BlobPart>('cronjobs/export', params, { responseType: 'blob', timeout: TimeoutEnum.T_40S });
};
export const loadCronjobInfo = (id: number) => { export const loadCronjobInfo = (id: number) => {
return http.post<Cronjob.CronjobOperate>(`/cronjobs/load/info`, { id: id }); return http.post<Cronjob.CronjobOperate>(`/cronjobs/load/info`, { id: id });
}; };

View file

@ -50,6 +50,7 @@ const message = {
verify: 'Verify', verify: 'Verify',
saveAndEnable: 'Save and enable', saveAndEnable: 'Save and enable',
import: 'Import', import: 'Import',
export: 'Export',
power: 'Authorization', power: 'Authorization',
search: 'Search', search: 'Search',
refresh: 'Refresh', refresh: 'Refresh',
@ -298,6 +299,7 @@ const message = {
normal: 'Normal', normal: 'Normal',
building: 'Building', building: 'Building',
upgrading: 'Upgrading', upgrading: 'Upgrading',
pending: 'Pending Edit',
rebuilding: 'Rebuilding', rebuilding: 'Rebuilding',
deny: 'Denied', deny: 'Denied',
accept: 'Accepted', accept: 'Accepted',
@ -988,6 +990,9 @@ const message = {
cronjob: { cronjob: {
create: 'Create cron job', create: 'Create cron job',
edit: 'Edit 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', changeStatus: 'Change status',
disableMsg: 'This will stop the scheduled task from automatically executing. Do you want to continue?', 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?', enableMsg: 'This will allow the scheduled task to automatically execute. Do you want to continue?',

View file

@ -48,6 +48,7 @@ const message = {
verify: '確認する', verify: '確認する',
saveAndEnable: '保存して有効にします', saveAndEnable: '保存して有効にします',
import: '輸入', import: '輸入',
export: 'エクスポート',
power: '認可', power: '認可',
search: '検索', search: '検索',
refresh: 'リロード', refresh: 'リロード',
@ -287,6 +288,7 @@ const message = {
normal: '普通', normal: '普通',
building: '建物', building: '建物',
upgrading: 'アップグレード', upgrading: 'アップグレード',
pending: '編集待ち',
rebuilding: '再構築', rebuilding: '再構築',
deny: '拒否されました', deny: '拒否されました',
accept: '受け入れられました', accept: '受け入れられました',
@ -958,6 +960,9 @@ const message = {
cronjob: { cronjob: {
create: 'Cronジョブを作成します', create: 'Cronジョブを作成します',
edit: 'Cronジョブを編集します', edit: 'Cronジョブを編集します',
errImport: 'ファイル内容異常:',
importHelper:
'インポート時に同名のスケジュールタスクは自動的にスキップされますタスクはデフォルトで無効状態に設定されデータ関連付け異常時には編集待ち状態に設定されます',
changeStatus: 'ステータスを変更します', changeStatus: 'ステータスを変更します',
disableMsg: 'これによりスケジュールされたタスクが自動的に実行されなくなります続けたいですか', disableMsg: 'これによりスケジュールされたタスクが自動的に実行されなくなります続けたいですか',
enableMsg: 'これによりスケジュールされたタスクが自動的に実行されます続けたいですか', enableMsg: 'これによりスケジュールされたタスクが自動的に実行されます続けたいですか',

View file

@ -48,6 +48,7 @@ const message = {
verify: '검증', verify: '검증',
saveAndEnable: '저장 활성화', saveAndEnable: '저장 활성화',
import: '가져오기', import: '가져오기',
export: '내보내기',
power: '권한 부여', power: '권한 부여',
search: '검색', search: '검색',
refresh: '새로고침', refresh: '새로고침',
@ -289,6 +290,7 @@ const message = {
normal: '정상', normal: '정상',
building: '빌드 ', building: '빌드 ',
upgrading: '업그레이드 ', upgrading: '업그레이드 ',
pending: '편집 대기',
rebuilding: '재빌드 ', rebuilding: '재빌드 ',
deny: '거부됨', deny: '거부됨',
accept: '수락됨', accept: '수락됨',
@ -948,6 +950,9 @@ const message = {
cronjob: { cronjob: {
create: '크론 작업 생성', create: '크론 작업 생성',
edit: '크론 작업 수정', edit: '크론 작업 수정',
errImport: '파일 내용 이상:',
importHelper:
'가져오기 동일한 이름의 예약 작업은 자동으로 건너뜁니다. 작업은 기본적으로 비활성화 상태로 설정되며, 데이터 연동 이상 편집 대기 상태로 설정됩니다.',
changeStatus: '상태 변경', changeStatus: '상태 변경',
disableMsg: ' 작업은 예약된 작업이 자동으로 실행되지 않도록 멈춥니다. 계속하시겠습니까?', disableMsg: ' 작업은 예약된 작업이 자동으로 실행되지 않도록 멈춥니다. 계속하시겠습니까?',
enableMsg: ' 작업은 예약된 작업이 자동으로 실행되도록 허용합니다. 계속하시겠습니까?', enableMsg: ' 작업은 예약된 작업이 자동으로 실행되도록 허용합니다. 계속하시겠습니까?',

View file

@ -48,6 +48,7 @@ const message = {
verify: 'Sahkan', verify: 'Sahkan',
saveAndEnable: 'Simpan dan aktifkan', saveAndEnable: 'Simpan dan aktifkan',
import: 'Import', import: 'Import',
export: 'Eksport',
power: 'Pemberian Kuasa', power: 'Pemberian Kuasa',
search: 'Cari', search: 'Cari',
refresh: 'Segarkan', refresh: 'Segarkan',
@ -295,6 +296,7 @@ const message = {
normal: 'Normal', normal: 'Normal',
building: 'Sedang Membina', building: 'Sedang Membina',
upgrading: 'Sedang Meningkatkan', upgrading: 'Sedang Meningkatkan',
pending: 'Menunggu Edit',
rebuilding: 'Sedang Membina Semula', rebuilding: 'Sedang Membina Semula',
deny: 'Ditolak', deny: 'Ditolak',
accept: 'Diterima', accept: 'Diterima',
@ -979,6 +981,9 @@ const message = {
cronjob: { cronjob: {
create: 'Cipta tugas cron', create: 'Cipta tugas cron',
edit: 'Edit 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', changeStatus: 'Tukar status',
disableMsg: disableMsg:
'Ini akan menghentikan tugas berjadual daripada dilaksanakan secara automatik. Adakah anda mahu meneruskan?', 'Ini akan menghentikan tugas berjadual daripada dilaksanakan secara automatik. Adakah anda mahu meneruskan?',

View file

@ -48,6 +48,7 @@ const message = {
verify: 'Verificar', verify: 'Verificar',
saveAndEnable: 'Salvar e ativar', saveAndEnable: 'Salvar e ativar',
import: 'Importar', import: 'Importar',
export: 'Exportar',
power: 'Autorização', power: 'Autorização',
search: 'Pesquisar', search: 'Pesquisar',
refresh: 'Atualizar', refresh: 'Atualizar',
@ -293,6 +294,7 @@ const message = {
normal: 'Normal', normal: 'Normal',
building: 'Construindo', building: 'Construindo',
upgrading: 'Atualizando', upgrading: 'Atualizando',
pending: 'Aguardando Edição',
rebuilding: 'Reconstruindo', rebuilding: 'Reconstruindo',
deny: 'Negado', deny: 'Negado',
accept: 'Aceito', accept: 'Aceito',
@ -975,6 +977,9 @@ const message = {
cronjob: { cronjob: {
create: 'Criar tarefa cron', create: 'Criar tarefa cron',
edit: 'Editar 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', changeStatus: 'Alterar status',
disableMsg: 'Isso irá parar a execução automática da tarefa agendada. Você deseja continuar?', 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?', enableMsg: 'Isso permitirá que a tarefa agendada seja executada automaticamente. Você deseja continuar?',

View file

@ -48,6 +48,7 @@ const message = {
verify: 'Проверить', verify: 'Проверить',
saveAndEnable: 'Сохранить и включить', saveAndEnable: 'Сохранить и включить',
import: 'Импорт', import: 'Импорт',
export: 'Экспорт',
power: 'Авторизация', power: 'Авторизация',
search: 'Поиск', search: 'Поиск',
refresh: 'Обновить', refresh: 'Обновить',
@ -290,6 +291,7 @@ const message = {
normal: 'Нормально', normal: 'Нормально',
building: 'Сборка', building: 'Сборка',
upgrading: 'Обновление', upgrading: 'Обновление',
pending: 'Ожидает редактирования',
rebuilding: 'Пересборка', rebuilding: 'Пересборка',
deny: 'Отказано', deny: 'Отказано',
accept: 'Принято', accept: 'Принято',
@ -972,6 +974,9 @@ const message = {
cronjob: { cronjob: {
create: 'Создать задачу cron', create: 'Создать задачу cron',
edit: 'Редактировать задачу cron', edit: 'Редактировать задачу cron',
errImport: 'Аномальное содержимое файла:',
importHelper:
'Повторяющиеся запланированные задачи будут автоматически пропущены при импорте. По умолчанию задачи устанавливаются в статус Отключено, а при аномальной ассоциации данных - в статус Ожидает редактирования.',
changeStatus: 'Изменить статус', changeStatus: 'Изменить статус',
disableMsg: 'Это остановит автоматическое выполнение запланированной задачи. Хотите продолжить?', disableMsg: 'Это остановит автоматическое выполнение запланированной задачи. Хотите продолжить?',
enableMsg: 'Это позволит запланированной задаче автоматически выполняться. Хотите продолжить?', enableMsg: 'Это позволит запланированной задаче автоматически выполняться. Хотите продолжить?',

View file

@ -50,6 +50,7 @@ const message = {
verify: 'Doğrula', verify: 'Doğrula',
saveAndEnable: 'Kaydet ve etkinleştir', saveAndEnable: 'Kaydet ve etkinleştir',
import: 'İçe Aktar', import: 'İçe Aktar',
export: 'Dışa Aktar',
power: 'Yetkilendirme', power: 'Yetkilendirme',
search: 'Ara', search: 'Ara',
refresh: 'Yenile', refresh: 'Yenile',
@ -302,6 +303,7 @@ const message = {
normal: 'Normal', normal: 'Normal',
building: 'İnşa Ediliyor', building: 'İnşa Ediliyor',
upgrading: 'Yükseltiliyor', upgrading: 'Yükseltiliyor',
pending: 'Düzenleme Bekliyor',
rebuilding: 'Yeniden İnşa Ediliyor', rebuilding: 'Yeniden İnşa Ediliyor',
deny: 'Reddedildi', deny: 'Reddedildi',
accept: 'Kabul Edildi', accept: 'Kabul Edildi',
@ -1000,6 +1002,9 @@ const message = {
cronjob: { cronjob: {
create: 'Cron görevi oluştur', create: 'Cron görevi oluştur',
edit: 'Cron görevini düzenle', 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', changeStatus: 'Durumu değiştir',
disableMsg: 'Bu, zamanlanmış görevin otomatik olarak yürütülmesini durduracaktır. Devam etmek istiyor musunuz?', disableMsg: 'Bu, zamanlanmış görevin otomatik olarak yürütülmesini durduracaktır. Devam etmek istiyor musunuz?',
enableMsg: enableMsg:

View file

@ -51,6 +51,7 @@ const message = {
saveAndEnable: '保存並啟用', saveAndEnable: '保存並啟用',
import: '導入', import: '導入',
power: '授權', power: '授權',
export: '導出',
search: '搜索', search: '搜索',
refresh: '刷新', refresh: '刷新',
get: '獲取', get: '獲取',
@ -289,6 +290,7 @@ const message = {
normal: '正常', normal: '正常',
building: '製作鏡像中', building: '製作鏡像中',
upgrading: '升級中', upgrading: '升級中',
pending: '待編輯',
rebuilding: '重建中', rebuilding: '重建中',
deny: '已屏蔽', deny: '已屏蔽',
accept: '已放行', accept: '已放行',
@ -942,6 +944,9 @@ const message = {
cronjob: { cronjob: {
create: '創建計劃任務', create: '創建計劃任務',
edit: '編輯計劃任務', edit: '編輯計劃任務',
errImport: '文件內容異常:',
importHelper:
'導入時將自動跳過重名計劃任務任務默認設置為停用狀態數據關聯異常時設置為待編輯狀態',
changeStatus: '狀態修改', changeStatus: '狀態修改',
disableMsg: '停止計劃任務會導致該任務不再自動執行是否繼續', disableMsg: '停止計劃任務會導致該任務不再自動執行是否繼續',
enableMsg: '啟用計劃任務會讓該任務定期自動執行是否繼續', enableMsg: '啟用計劃任務會讓該任務定期自動執行是否繼續',

View file

@ -50,6 +50,7 @@ const message = {
verify: '验证', verify: '验证',
saveAndEnable: '保存并启用', saveAndEnable: '保存并启用',
import: '导入', import: '导入',
export: '导出',
power: '授权', power: '授权',
search: '搜索', search: '搜索',
refresh: '刷新', refresh: '刷新',
@ -287,6 +288,7 @@ const message = {
normal: '正常', normal: '正常',
building: '制作镜像中', building: '制作镜像中',
upgrading: '升级中', upgrading: '升级中',
pending: '待编辑',
rebuilding: '重建中', rebuilding: '重建中',
deny: '已屏蔽', deny: '已屏蔽',
accept: '已放行', accept: '已放行',
@ -940,6 +942,9 @@ const message = {
cronjob: { cronjob: {
create: '创建计划任务', create: '创建计划任务',
edit: '编辑计划任务', edit: '编辑计划任务',
errImport: '文件内容异常',
importHelper:
'导入时将自动跳过重名计划任务任务默认设置为停用状态数据关联异常时设置为待编辑状态',
changeStatus: '状态修改', changeStatus: '状态修改',
disableMsg: '停止计划任务会导致该任务不再自动执行是否继续', disableMsg: '停止计划任务会导致该任务不再自动执行是否继续',
enableMsg: '启用计划任务会让该任务定期自动执行是否继续', enableMsg: '启用计划任务会让该任务定期自动执行是否继续',

View file

@ -0,0 +1,149 @@
<template>
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
<div>
<el-alert :closable="false" show-icon type="info" :title="$t('cronjob.importHelper')" />
<el-upload
action="#"
:auto-upload="false"
ref="uploadRef"
class="upload mt-2"
:show-file-list="false"
:limit="1"
:on-change="fileOnChange"
:on-exceed="handleExceed"
v-model:file-list="uploaderFiles"
>
<el-button class="float-left" type="primary">{{ $t('commons.button.upload') }}</el-button>
</el-upload>
<el-button :disabled="selects.length === 0" @click="onImport" class="ml-2 mt-2">
{{ $t('commons.button.import') }}
</el-button>
<el-card class="mt-2 w-full" v-loading="loading">
<el-table :data="data" @selection-change="handleSelectionChange">
<el-table-column type="selection" fix />
<el-table-column
:label="$t('cronjob.taskName')"
:min-width="120"
prop="name"
show-overflow-tooltip
/>
<el-table-column
:label="$t('commons.table.type')"
:min-width="120"
prop="type"
show-overflow-tooltip
>
<template #default="{ row }">
<el-tag>{{ $t('cronjob.' + row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120">
<template #default="{ row }">
<div v-for="(item, index) of row.spec.split(',')" :key="index">
<div v-if="row.expand || (!row.expand && index < 3)">
<span>
{{ row.specCustom ? item : transSpecToStr(item) }}
</span>
</div>
</div>
<div v-if="!row.expand && row.spec.split(',').length > 3">
<el-button type="primary" link @click="row.expand = true">
{{ $t('commons.button.expand') }}...
</el-button>
</div>
<div v-if="row.expand && row.spec.split(',').length > 3">
<el-button type="primary" link @click="row.expand = false">
{{ $t('commons.button.collapse') }}
</el-button>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('cronjob.retainCopies')" :min-width="120" prop="retainCopies" />
</el-table>
</el-card>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">
{{ $t('commons.button.cancel') }}
</el-button>
</span>
</template>
</DialogPro>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { transSpecToStr } from './../helper';
import { genFileId, UploadFile, UploadFiles, UploadProps, UploadRawFile } from 'element-plus';
import { MsgError, MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import { importCronjob } from '@/api/modules/cronjob';
import { Cronjob } from '@/api/interface/cronjob';
const emit = defineEmits<{ (e: 'search'): void }>();
const visible = ref(false);
const loading = ref();
const selects = ref<any>([]);
const data = ref();
const uploadRef = ref();
const uploaderFiles = ref();
const acceptParams = (): void => {
visible.value = true;
data.value = [];
};
const handleSelectionChange = (val: any) => {
selects.value = val;
};
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
loading.value = true;
data.value = [];
uploaderFiles.value = uploadFiles;
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result as string;
data.value = JSON.parse(content) as Cronjob.CronjobTrans;
console.log(data.value);
loading.value = false;
} catch (error) {
MsgError(i18n.global.t('cronjob.errImport') + error.message);
loading.value = false;
}
};
reader.readAsText(_uploadFile.raw);
};
const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
uploadRef.value!.handleStart(file);
};
const onImport = async () => {
await importCronjob(selects.value).then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
visible.value = false;
emit('search');
});
};
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.upload {
width: 60px;
float: left;
}
</style>

View file

@ -16,6 +16,15 @@
{{ $t('commons.button.delete') }} {{ $t('commons.button.delete') }}
</el-button> </el-button>
</el-button-group> </el-button-group>
<el-button-group class="ml-4">
<el-button @click="onImport">
{{ $t('commons.button.import') }}
</el-button>
<el-button :disabled="selects.length === 0" @click="onExport">
{{ $t('commons.button.export') }}
</el-button>
</el-button-group>
</template> </template>
<template #rightToolBar> <template #rightToolBar>
<TableSearch @search="search()" v-model:searchName="searchName" /> <TableSearch @search="search()" v-model:searchName="searchName" />
@ -54,11 +63,12 @@
:operate="true" :operate="true"
/> />
<Status <Status
v-else v-if="row.status === 'Disable'"
@click="onChangeStatus(row.id, 'enable')" @click="onChangeStatus(row.id, 'enable')"
:status="row.status" :status="row.status"
:operate="true" :operate="true"
/> />
<Status v-if="row.status === 'Pending'" :status="row.status" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120"> <el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120">
@ -166,7 +176,9 @@
</el-form> </el-form>
</template> </template>
</OpDialog> </OpDialog>
<OpDialog ref="opExportRef" @search="search" @submit="onSubmitExport()" />
<Records @search="search" ref="dialogRecordRef" /> <Records @search="search" ref="dialogRecordRef" />
<Import @search="search" ref="dialogImportRef" />
<Backups @search="search" ref="dialogBackupRef" /> <Backups @search="search" ref="dialogBackupRef" />
</div> </div>
</template> </template>
@ -174,8 +186,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import Records from '@/views/cronjob/cronjob/record/index.vue'; import Records from '@/views/cronjob/cronjob/record/index.vue';
import Backups from '@/views/cronjob/cronjob/backup/index.vue'; import Backups from '@/views/cronjob/cronjob/backup/index.vue';
import Import from '@/views/cronjob/cronjob/import/index.vue';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
import { deleteCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob'; import { deleteCronjob, exportCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob';
import i18n from '@/lang'; import i18n from '@/lang';
import { Cronjob } from '@/api/interface/cronjob'; import { Cronjob } from '@/api/interface/cronjob';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
@ -183,6 +196,7 @@ import { MsgSuccess } from '@/utils/message';
import { hasBackup, transSpecToStr } from './helper'; import { hasBackup, transSpecToStr } from './helper';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import router from '@/routers'; import router from '@/routers';
import { getCurrentDateFormatted } from '@/utils/util';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const mobile = computed(() => { const mobile = computed(() => {
@ -198,6 +212,8 @@ const opRef = ref();
const showClean = ref(); const showClean = ref();
const cleanData = ref(); const cleanData = ref();
const cleanRemoteData = ref(); const cleanRemoteData = ref();
const opExportRef = ref();
const dialogImportRef = ref();
const data = ref(); const data = ref();
const paginationConfig = reactive({ const paginationConfig = reactive({
@ -286,6 +302,47 @@ const onSubmitDelete = async () => {
}); });
}; };
const onImport = () => {
dialogImportRef.value.acceptParams();
};
const onExport = async () => {
let names = [];
let ids = [];
for (const item of selects.value) {
names.push(item.name);
ids.push(item.id);
}
operateIDs.value = ids;
opExportRef.value.acceptParams({
title: i18n.global.t('commons.button.export'),
names: names,
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('menu.cronjob'),
i18n.global.t('commons.button.export'),
]),
api: null,
params: null,
});
};
const onSubmitExport = async () => {
loading.value = true;
await exportCronjob({ ids: operateIDs.value })
.then((res) => {
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = '1panel-cronjob-' + getCurrentDateFormatted() + '.json';
const event = new MouseEvent('click');
a.dispatchEvent(event);
})
.finally(() => {
loading.value = false;
});
};
const onChangeStatus = async (id: number, status: string) => { const onChangeStatus = async (id: number, status: string) => {
ElMessageBox.confirm(i18n.global.t('cronjob.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), { ElMessageBox.confirm(i18n.global.t('cronjob.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'), confirmButtonText: i18n.global.t('commons.button.confirm'),

View file

@ -866,14 +866,14 @@ const search = async () => {
form.scriptID = res.data.scriptID; form.scriptID = res.data.scriptID;
form.appID = res.data.appID; form.appID = res.data.appID;
form.appIdList = res.data.appID.split(',') || []; form.appIdList = res.data.appID ? res.data.appID.split(',') : [];
form.website = res.data.website; form.website = res.data.website;
form.websiteList = res.data.website.split(',') || []; form.websiteList = res.data.website ? res.data.website.split(',') : [];
form.exclusionRules = res.data.exclusionRules; form.exclusionRules = res.data.exclusionRules;
form.ignoreFiles = res.data.exclusionRules.split(','); form.ignoreFiles = res.data.exclusionRules ? res.data.exclusionRules.split(',') : [];
form.dbType = res.data.dbType; form.dbType = res.data.dbType;
form.dbName = res.data.dbName; form.dbName = res.data.dbName;
form.dbNameList = res.data.dbName.split(',') || []; form.dbNameList = res.data.dbName ? res.data.dbName.split(',') : [];
form.url = res.data.url; form.url = res.data.url;
form.withImage = res.data.snapshotRule.withImage; form.withImage = res.data.snapshotRule.withImage;
form.ignoreAppIDs = res.data.snapshotRule.ignoreAppIDs; form.ignoreAppIDs = res.data.snapshotRule.ignoreAppIDs;