diff --git a/agent/app/api/v2/website.go b/agent/app/api/v2/website.go index 9c9e6b05a..402083e63 100644 --- a/agent/app/api/v2/website.go +++ b/agent/app/api/v2/website.go @@ -1141,3 +1141,23 @@ func (b *BaseApi) ExecComposer(c *gin.Context) { } helper.Success(c) } + +// @Tags Website +// @Summary Batch operate websites +// @Accept json +// @Param request body request.BatchOpWebsite true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/batch/operate [post] +func (b *BaseApi) BatchOpWebsites(c *gin.Context) { + var req request.BatchWebsiteOp + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.BatchOpWebsite(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} diff --git a/agent/app/dto/request/website.go b/agent/app/dto/request/website.go index 5c1fa320b..54a388a96 100644 --- a/agent/app/dto/request/website.go +++ b/agent/app/dto/request/website.go @@ -102,6 +102,12 @@ type WebsiteOp struct { Operate string `json:"operate"` } +type BatchWebsiteOp struct { + IDs []uint `json:"ids" validate:"required"` + Operate string `json:"operate" validate:"required"` + TaskID string `json:"taskID" validate:"required"` +} + type WebsiteRedirectUpdate struct { WebsiteID uint `json:"websiteId" validate:"required"` Key string `json:"key" validate:"required"` diff --git a/agent/app/service/website.go b/agent/app/service/website.go index 047304c15..4c703d5d6 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -60,6 +60,7 @@ type IWebsiteService interface { UpdateWebsite(req request.WebsiteUpdate) error DeleteWebsite(req request.WebsiteDelete) error GetWebsite(id uint) (response.WebsiteDTO, error) + BatchOpWebsite(req request.BatchWebsiteOp) error CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) GetWebsiteDomain(websiteId uint) ([]model.WebsiteDomain, error) @@ -517,6 +518,43 @@ func (w WebsiteService) OpWebsite(req request.WebsiteOp) error { return websiteRepo.Save(context.Background(), &website) } +func (w WebsiteService) BatchOpWebsite(req request.BatchWebsiteOp) error { + websites, _ := websiteRepo.List(repo.WithByIDs(req.IDs)) + opTask, err := task.NewTaskWithOps(i18n.GetMsgByKey("Status"), task.TaskBatch, task.TaskScopeWebsite, req.TaskID, 0) + if err != nil { + return err + } + opWebsiteTask := func(t *task.Task) error { + for _, web := range websites { + msg := fmt.Sprintf("%s %s", i18n.GetMsgByKey(req.Operate), web.PrimaryDomain) + switch req.Operate { + case constant.StopWeb, constant.StartWeb: + if err := opWebsite(&web, req.Operate); err != nil { + t.LogFailedWithErr(msg, err) + continue + } + _ = websiteRepo.Save(context.Background(), &web) + case "delete": + if err := w.DeleteWebsite(request.WebsiteDelete{ + ID: web.ID, + }); err != nil { + t.LogFailedWithErr(msg, err) + continue + } + } + + t.LogSuccess(msg) + } + return nil + } + opTask.AddSubTask("", opWebsiteTask, nil) + + go func() { + _ = opTask.Execute() + }() + return nil +} + func (w WebsiteService) GetWebsiteOptions(req request.WebsiteOptionReq) ([]response.WebsiteOption, error) { var options []repo.DBOption if len(req.Types) > 0 { diff --git a/agent/app/task/task.go b/agent/app/task/task.go index 1fbf90ffd..223697f08 100644 --- a/agent/app/task/task.go +++ b/agent/app/task/task.go @@ -69,6 +69,7 @@ const ( TaskHandle = "TaskHandle" TaskScan = "TaskScan" TaskExec = "TaskExec" + TaskBatch = "TaskBatch" ) const ( @@ -317,7 +318,7 @@ func (t *Task) LogFailedWithErr(msg string, err error) { } func (t *Task) LogSuccess(msg string) { - t.Logger.Println(msg + i18n.GetMsgByKey("Success")) + t.Logger.Println(msg + " " + i18n.GetMsgByKey("Success")) } func (t *Task) LogSuccessF(format string, v ...any) { t.Logger.Println(fmt.Sprintf(format, v...) + i18n.GetMsgByKey("Success")) diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 0e04d8489..ee5e919b3 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -140,6 +140,10 @@ ErrSSLValid: 'Certificate file is abnormal, please check the certificate status! ErrWebsiteDir: "Please select a directory within the website directory." ErrComposerFileNotFound: "composer.json file does not exist" ErrRuntimeNoPort: "The runtime environment is not set with a port, please edit the runtime environment first." +Status: 'Status' +start: 'Start' +stop: 'Stop' +delete: 'Delete' #ssl ErrSSLCannotDelete: 'The {{ .name }} certificate is being used by a website and cannot be deleted' @@ -353,6 +357,7 @@ RuntimeExtension: 'Runtime Environment Extension' TaskIsExecuting: 'Task is running' CustomAppstore: 'Custom application warehouse' TaskExec: 'Execute' +TaskBatch: "Batch Operation" # task - clam Clamscan: "Scan {{ .name }}" diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 0450ba0e6..0f126075b 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -138,6 +138,10 @@ ErrSSLValid: 'Archivo de certificado anómalo, ¡revise el estado del certificad ErrWebsiteDir: "Por favor seleccione un directorio dentro del directorio del sitio web" ErrComposerFileNotFound: "El archivo composer.json no existe" ErrRuntimeNoPort: "El entorno de ejecución no tiene configurado un puerto, edítelo primero" +Status: 'Estado' +start: 'Iniciar' +stop: 'Detener' +delete: 'Eliminar' #ssl ErrSSLCannotDelete: 'El certificado {{ .name }} está siendo utilizado por un sitio web y no puede eliminarse' @@ -158,7 +162,6 @@ ApplySSLFailed: 'Solicitud de certificado para [{{ .domain }}] fallida, {{ .deta ApplySSLSuccess: '¡Solicitud de certificado para [{{ .domain }}] exitosa!' DNSAccountName: 'Cuenta DNS [{{ .name }}] proveedor [{{ .type }}]' PushDirLog: 'Certificado empujado al directorio [{{ .path }}] {{ .status }}' -ErrDeleteCAWithSSL: 'La organización actual tiene un certificado emitido y no puede eliminarse.' ErrDeleteWithPanelSSL: 'La configuración SSL del panel utiliza este certificado y no puede eliminarse' ErrDefaultCA: 'La autoridad por defecto no puede eliminarse' ApplyWebSiteSSLLog: 'Iniciando renovación del certificado del sitio web {{ .name }}' @@ -352,6 +355,7 @@ TaskIsExecuting: 'Tarea en ejecución' CustomAppstore: 'Almacén de aplicaciones personalizado' TaskClean: "Limpieza" TaskExec: "Ejecutar" +TaskBatch: "Operación por Lotes" # task - ai OllamaModelPull: 'Descargar modelo Ollama {{ .name }}' @@ -428,12 +432,9 @@ ImageBuild: 'Construcción de imagen' ImageBuildStdoutCheck: 'Analizar salida de la imagen' ImageBuildRes: 'Salida de construcción de imagen: {{ .name }}' ImagePull: 'Descargar imagen' -ImageRepoAuthFromDB: 'Obtener autenticación de repositorio desde base de datos' -ImaegPullRes: 'Salida de descarga de imagen: {{ .name }}' ImagePush: 'Subir imagen' ImageRenameTag: 'Modificar etiqueta de imagen' ImageNewTag: 'Nueva etiqueta de imagen {{ .name }}' -ImaegPushRes: 'Salida de subida de imagen: {{ .name }}' ComposeCreate: 'Crear orquestación' ComposeCreateRes: 'Salida de creación de orquestación: {{ .name }}' ImageRepoAuthFromDB: "Obtener información de autenticación del repositorio desde la base de datos" diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 34f8f5ccd..11fa96207 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -140,6 +140,10 @@ ErrSSLValid: '証明書ファイルが異常です、証明書の状態を確認 ErrWebsiteDir: "ウェブサイトディレクトリ内のディレクトリを選択してください。" ErrComposerFileNotFound: "composer.json ファイルが存在しません" ErrRuntimeNoPort: "ランタイム環境にポートが設定されていません。先にランタイム環境を編集してください。" +Status: 'ステータス' +start: '開始' +stop: '停止' +delete: '削除' #ssl ErrSSLCannotDelete: '{{ .name }} 証明書は Web サイトで使用されているため、削除できません' @@ -353,6 +357,7 @@ RuntimeExtension: 'ランタイム環境拡張' TaskIsExecuting: 'タスクは実行中です' CustomAppstore: 'カスタム アプリケーション ウェアハウス' TaskExec: '実行' +TaskBatch: "一括操作" # task - clam Clamscan: "{{ .name }} をスキャン" diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 772a18f21..4e6f44f65 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -140,6 +140,10 @@ ErrSSLValid: '인증서 파일에 문제가 있습니다. 인증서 상태를 ErrWebsiteDir: "웹사이트 디렉토리 내의 디렉토리를 선택하세요." ErrComposerFileNotFound: "composer.json 파일이 존재하지 않습니다" ErrRuntimeNoPort: "런타임 환경에 포트가 설정되지 않았습니다. 먼저 런타임 환경을 편집하세요." +Status: '상태' +start: '시작' +stop: '중지' +delete: '삭제' #SSL인증 ErrSSLCannotDelete: '{{ .name }} 인증서는 웹사이트에서 사용 중이므로 삭제할 수 없습니다.' @@ -353,6 +357,7 @@ RuntimeExtension: '런타임 환경 확장' TaskIsExecuting: '작업이 실행 중입니다' CustomAppstore: '사용자 정의 애플리케이션 웨어하우스' TaskExec: '실행' +TaskBatch: "일괄 작업" # task - clam Clamscan: "{{ .name }} 스캔" diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 042d5c522..e23ab9fd4 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -143,6 +143,10 @@ ErrSSLValid: 'Fail sijil bermasalah, sila periksa status sijil!' ErrWebsiteDir: "Sila pilih direktori dalam direktori laman web." ErrComposerFileNotFound: "Fail composer.json tidak wujud" ErrRuntimeNoPort: "Persekitaran runtime tidak diset dengan port, sila edit persekitaran runtime terlebih dahulu." +Status: 'Status' +start: 'Mulakan' +stop: 'Berhenti' +delete: 'Padam' #ssl ErrSSLCannotDelete: 'Sijil {{ .name }} sedang digunakan oleh tapak web dan tidak boleh dipadamkan' @@ -353,6 +357,7 @@ RuntimeExtension: 'Sambungan Persekitaran Runtime' TaskIsExecuting: 'Tugas sedang berjalan' CustomAppstore: 'Gudang aplikasi tersuai' TaskExec: 'Laksanakan' +TaskBatch: "Operasi Batch" # task - clam Clamscan: "Imbas {{ .name }}" diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index c73a042e0..5daeada0f 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -143,6 +143,10 @@ ErrSSLValid: 'O arquivo do certificado está anormal, verifique o status do cert ErrWebsiteDir: "Por favor, selecione um diretório dentro do diretório do site." ErrComposerFileNotFound: "O arquivo composer.json não existe" ErrRuntimeNoPort: "O ambiente de tempo de execução não está configurado com uma porta, edite o ambiente de tempo de execução primeiro." +Status: 'Status' +start: 'Iniciar' +stop: 'Parar' +delete: 'Excluir' #ssl ErrSSLCannotDelete: 'O certificado {{ .name }} está sendo usado por um site e não pode ser excluído' @@ -353,6 +357,7 @@ RuntimeExtension: 'Extensão do ambiente de tempo de execução' TaskIsExecuting: 'A tarefa está em execução' CustomAppstore: 'Armazém de aplicativos personalizados' TaskExec: 'Executar' +TaskBatch: "Operação em Lote" # task - clam Clamscan: "Escanear {{ .name }}" diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index a8e9397ac..3c5f86f15 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -143,6 +143,10 @@ ErrSSLValid: 'Файл сертификата аномален, проверьт ErrWebsiteDir: "Пожалуйста, выберите директорию внутри директории сайта." ErrComposerFileNotFound: "Файл composer.json не существует" ErrRuntimeNoPort: "Среда выполнения не настроена с портом, сначала отредактируйте среду выполнения." +Status: 'Статус' +start: 'Запустить' +stop: 'Остановить' +delete: 'Удалить' #ssl ErrSSLCannotDelete: 'Сертификат {{ .name }} используется веб-сайтом и не может быть удален' @@ -353,6 +357,7 @@ RuntimeExtension: 'Расширение среды выполнения' TaskIsExecuting: 'Задача выполняется' CustomAppstore: 'Хранилище пользовательских приложений' TaskExec: 'Выполнить' +TaskBatch: "Пакетная операция" # task - clam Clamscan: "Сканировать {{ .name }}" diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 7f5aa38a8..c5385a33d 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -144,6 +144,10 @@ ErrSSLValid: 'Sertifika dosyası anormal, lütfen sertifika durumunu kontrol edi ErrWebsiteDir: "Lütfen web sitesi dizini içindeki bir dizin seçin." ErrComposerFileNotFound: "composer.json dosyası mevcut değil" ErrRuntimeNoPort: "Çalışma zamanı ortamı bir porta sahip değil, lütfen önce çalışma zamanı ortamını düzenleyin." +Status: 'Durum' +start: 'Başlat' +stop: 'Durdur' +delete: 'Sil' #ssl ErrSSLCannotDelete: '{{ .name }} sertifikası bir web sitesi tarafından kullanılıyor ve silinemez' @@ -354,6 +358,7 @@ RuntimeExtension: 'Çalışma Ortamı Uzantısı' TaskIsExecuting: 'Görev çalışıyor' CustomAppstore: 'Özel uygulama deposu' TaskExec: 'Çalıştır' +TaskBatch: "Toplu İşlem" # task - clam Clamscan: "{{ .name }} Tara" diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 8e6d94009..54c37bc57 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -139,6 +139,10 @@ ErrSSLValid: '證書文件異常,請檢查證書狀態!' ErrWebsiteDir: "請選擇網站目錄下的目錄" ErrComposerFileNotFound: "composer.json 文件不存在" ErrRuntimeNoPort: "執行環境未設定埠,請先編輯執行環境" +Status: '狀態' +start: '開啟' +stop: '關閉' +delete: '刪除' #ssl ErrSSLCannotDelete: '{{ .name }} 憑證正在被網站使用,無法刪除' @@ -352,6 +356,7 @@ RuntimeExtension: '執行環境擴充' TaskIsExecuting: '任務正在運作' CustomAppstore: '自訂應用程式倉庫' TaskExec: '執行' +TaskBatch: "批量操作" # task - clam Clamscan: "掃描 {{ .name }}" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 93b17bca5..15c1e3563 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -139,6 +139,10 @@ ErrSSLValid: '证书文件异常,请检查证书状态!' ErrWebsiteDir: '请选择网站目录下的目录' ErrComposerFileNotFound: 'composer.json 文件不存在' ErrRuntimeNoPort: '运行环境未设置端口,请先编辑运行环境' +Status: '状态' +start: '开启' +stop: '关闭' +delete: '删除' #ssl ErrSSLCannotDelete: "{{ .name }} 证书正在被网站使用,无法删除" @@ -353,6 +357,7 @@ RuntimeExtension: "运行环境扩展" TaskIsExecuting: "任务正在运行" CustomAppstore: "自定义应用仓库" TaskExec: "执行" +TaskBatch: "批量操作" # task - clam Clamscan: "扫描 {{ .name }}" diff --git a/agent/router/ro_website.go b/agent/router/ro_website.go index a7b7db914..16bdb9dcb 100644 --- a/agent/router/ro_website.go +++ b/agent/router/ro_website.go @@ -25,6 +25,7 @@ func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) { websiteRouter.POST("/del", baseApi.DeleteWebsite) websiteRouter.POST("/default/server", baseApi.ChangeDefaultServer) websiteRouter.POST("/group/change", baseApi.ChangeWebsiteGroup) + websiteRouter.POST("/batch/operate", baseApi.BatchOpWebsites) websiteRouter.GET("/domains/:websiteId", baseApi.GetWebDomains) websiteRouter.POST("/domains/del", baseApi.DeleteWebDomain) diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index 7adb030fa..0bb6b7535 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -667,4 +667,10 @@ export namespace Website { user: string; taskID: string; } + + export interface BatchOperate { + ids: number[]; + operate: string; + taskID: string; + } } diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index ebdb7ccc3..b7d0b236a 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -351,3 +351,7 @@ export const operateCrossSiteAccess = (req: Website.CrossSiteAccessOp) => { export const execComposer = (req: Website.ExecComposer) => { return http.post(`/websites/exec/composer`, req); }; + +export const batchOpreate = (req: Website.BatchOperate) => { + return http.post(`/websites/batch/operate`, req); +}; diff --git a/frontend/src/components/complex-table/index.vue b/frontend/src/components/complex-table/index.vue index 631390c75..11c2b95e7 100644 --- a/frontend/src/components/complex-table/index.vue +++ b/frontend/src/components/complex-table/index.vue @@ -22,31 +22,38 @@ + @@ -302,13 +332,14 @@ import DeleteWebsite from '@/views/website/website/delete/index.vue'; import NginxConfig from '@/views/website/website/nginx/index.vue'; import GroupDialog from '@/components/agent-group/index.vue'; import AppStatus from '@/components/app-status/index.vue'; +import TaskLog from '@/components/log/task/index.vue'; import i18n from '@/lang'; import { onMounted, reactive, ref, computed } from 'vue'; -import { listDomains, opWebsite, searchWebsites, updateWebsite } from '@/api/modules/website'; +import { batchOpreate, listDomains, opWebsite, searchWebsites, updateWebsite } from '@/api/modules/website'; import { Website } from '@/api/interface/website'; import { App } from '@/api/interface/app'; import { ElMessageBox } from 'element-plus'; -import { dateFormatSimple } from '@/utils/util'; +import { dateFormatSimple, newUUID } from '@/utils/util'; import { MsgError, MsgSuccess } from '@/utils/message'; import { useI18n } from 'vue-i18n'; import { getAgentGroupList } from '@/api/modules/group'; @@ -356,6 +387,13 @@ const domains = ref([]); const columns = ref([]); const hoveredRowIndex = ref(-1); const websiteDir = ref(); +const selects = ref([]); +const batchReq = reactive({ + operate: '', + ids: [] as number[], + taskID: '', +}); +const taskLogRef = ref(); const paginationConfig = reactive({ cacheSizeKey: 'website-page-size', @@ -640,6 +678,25 @@ const updateRemark = (row: Website.Website, bulr: Function) => { updateWebsitConfig(row); }; +const batchOp = () => { + ElMessageBox.confirm( + i18n.global.t('website.batchOpreateHelper', [i18n.global.t('commons.button.' + batchReq.operate)]), + i18n.global.t('website.batchOpreate'), + { + confirmButtonText: i18n.global.t('commons.button.confirm'), + cancelButtonText: i18n.global.t('commons.button.cancel'), + }, + ).then(async () => { + batchReq.ids = selects.value.map((item) => item.id); + const taskID = newUUID(); + batchReq.taskID = taskID; + await batchOpreate(batchReq); + taskLogRef.value.openWithTaskID(taskID); + selects.value = []; + batchReq.operate = ''; + }); +}; + onMounted(() => { search(); listGroup();