feat: support batch operate website (#10415)

Refs https://github.com/1Panel-dev/1Panel/issues/5142
This commit is contained in:
CityFun 2025-09-19 18:10:01 +08:00 committed by GitHub
parent c216726449
commit 2040f8f3b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 278 additions and 30 deletions

View file

@ -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)
}

View file

@ -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"`

View file

@ -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 {

View file

@ -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"))

View file

@ -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 }}"

View file

@ -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"

View file

@ -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 }} をスキャン"

View file

@ -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 }} 스캔"

View file

@ -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 }}"

View file

@ -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 }}"

View file

@ -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 }}"

View file

@ -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"

View file

@ -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 }}"

View file

@ -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 }}"

View file

@ -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)

View file

@ -667,4 +667,10 @@ export namespace Website {
user: string;
taskID: string;
}
export interface BatchOperate {
ids: number[];
operate: string;
taskID: string;
}
}

View file

@ -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);
};

View file

@ -22,31 +22,38 @@
</template>
</fu-table>
</div>
<div class="table-footer-container">
<div class="footer-left" v-if="slots.footerLeft">
<el-checkbox v-model="leftSelect" @change="toggleSelection"></el-checkbox>
<div class="ml-4">
<slot name="footerLeft"></slot>
</div>
</div>
<div
class="complex-table__pagination flex items-center w-full sm:flex-row flex-col text-xs sm:text-sm"
v-if="props.paginationConfig"
:class="{ '!justify-between': slots.paginationLeft, '!justify-end': !slots.paginationLeft }"
>
<slot name="paginationLeft"></slot>
<slot name="pagination">
<el-pagination
v-model:current-page="paginationConfig.currentPage"
v-model:page-size="paginationConfig.pageSize"
:total="paginationConfig.total"
:page-sizes="[5, 10, 20, 50, 100, 200, 500]"
@size-change="sizeChange"
@current-change="currentChange"
:size="mobile || paginationConfig.small ? 'small' : 'default'"
:layout="
mobile || paginationConfig.small
? 'total, prev, pager, next'
: 'total, sizes, prev, pager, next, jumper'
"
/>
</slot>
<div
class="complex-table__pagination flex items-center w-full sm:flex-row flex-col text-xs sm:text-sm"
v-if="props.paginationConfig"
:class="{ '!justify-between': slots.paginationLeft, '!justify-end': !slots.paginationLeft }"
>
<slot name="paginationLeft"></slot>
<slot name="pagination">
<el-pagination
v-model:current-page="paginationConfig.currentPage"
v-model:page-size="paginationConfig.pageSize"
:total="paginationConfig.total"
:page-sizes="[5, 10, 20, 50, 100, 200, 500]"
@size-change="sizeChange"
@current-change="currentChange"
:size="mobile || paginationConfig.small ? 'small' : 'default'"
:layout="
mobile || paginationConfig.small
? 'total, prev, pager, next'
: 'total, sizes, prev, pager, next, jumper'
"
/>
</slot>
</div>
</div>
<ul
v-if="rightClick.visible"
class="context-menu"
@ -104,6 +111,7 @@ const mobile = computed(() => {
const tableRef = ref();
const tableHeight = ref(0);
const menuRef = ref<HTMLElement | null>(null);
const leftSelect = ref(false);
const rightClick = ref({
visible: false,
@ -153,6 +161,11 @@ function sizeChange() {
function handleSelectionChange(row: any) {
emit('update:selects', row);
if (row.length > 0) {
leftSelect.value = true;
} else {
leftSelect.value = false;
}
}
function sort(prop: string, order: string) {
@ -236,6 +249,10 @@ function calcHeight() {
}
}
const toggleSelection = () => {
tableRef.value.refElTable.toggleAllSelection();
};
onMounted(() => {
calcHeight();
window.addEventListener('resize', calcHeight);
@ -306,4 +323,26 @@ onBeforeUnmount(() => {
.context-menu li.divided {
border-top: 1px solid var(--el-border-color);
}
.table-footer-container {
display: flex;
align-items: center;
justify-content: space-between;
.footer-left {
flex-shrink: 0;
margin-right: 16px;
margin-left: 12px;
display: flex;
.footer-left-button {
margin-left: 17px;
display: flex;
}
}
}
.complex-table__pagination {
flex: 1;
@include flex-row(flex-end);
}
</style>

View file

@ -2583,6 +2583,9 @@ const message = {
execUser: 'Executing User',
execDir: 'Execution Directory',
packagist: 'China Full Mirror',
batchOpreate: 'Batch Operation',
batchOpreateHelper: 'Batch {0} websites, continue operation?',
},
php: {
short_open_tag: 'Short tag support',

View file

@ -2552,6 +2552,9 @@ const message = {
execUser: 'Usuario de ejecución',
execDir: 'Directorio de ejecución',
packagist: 'Mirror China completo',
batchOpreate: 'Operación en Lote',
batchOpreateHelper: 'Lote {0} sitios web, ¿continuar operación?',
},
php: {
short_open_tag: 'Soporte de etiquetas cortas',

View file

@ -2498,6 +2498,9 @@ const message = {
execUser: '実行ユーザー',
execDir: '実行ディレクトリ',
packagist: '中国フルミラー',
batchOpreate: 'バッチ操作',
batchOpreateHelper: 'ウェブサイトをバッチ{0}しますか',
},
php: {
short_open_tag: '短いタグサポート',

View file

@ -2456,6 +2456,9 @@ const message = {
execUser: '실행 사용자',
execDir: '실행 디렉토리',
packagist: '중국 전체 미러',
batchOpreate: '일괄 작업',
batchOpreateHelper: '웹사이트를 일괄 {0}, 계속 작업하시겠습니까?',
},
php: {
short_open_tag: '짧은 태그 지원',

View file

@ -2554,6 +2554,9 @@ const message = {
execUser: 'Pengguna Melaksanakan',
execDir: 'Direktori Pelaksanaan',
packagist: 'Cermin Penuh China',
batchOpreate: 'Operasi Pukal',
batchOpreateHelper: 'Pukal {0} laman web, teruskan operasi?',
},
php: {
short_open_tag: 'Sokongan tag pendek',

View file

@ -2555,6 +2555,9 @@ const message = {
execUser: 'Usuário Executando',
execDir: 'Diretório de Execução',
packagist: 'Espelho Completo da China',
batchOpreate: 'Operação em Lote',
batchOpreateHelper: 'Lote {0} sites, continuar operação?',
},
php: {
short_open_tag: 'Suporte para short tags',

View file

@ -2553,6 +2553,9 @@ const message = {
execUser: 'Пользователь выполнения',
execDir: 'Каталог выполнения',
packagist: 'Полное зеркало Китая',
batchOpreate: 'Пакетная операция',
batchOpreateHelper: 'Пакетное {0} веб-сайтов, продолжить операцию?',
},
php: {
short_open_tag: 'Поддержка коротких тегов',

View file

@ -2612,6 +2612,9 @@ const message = {
execUser: 'Çalıştıran Kullanıcı',
execDir: 'Çalıştırma Dizini',
packagist: 'Çin Tam Aynası',
batchOpreate: 'Toplu İşlem',
batchOpreateHelper: 'Toplu {0} web siteleri, işlemi devam ettir?',
},
php: {
short_open_tag: 'Kısa etiket desteği',

View file

@ -2406,6 +2406,9 @@ const message = {
execUser: '執行使用者',
execDir: '執行目錄',
packagist: '中國全量鏡像',
batchOpreate: '批次操作',
batchOpreateHelper: '批次{0}網站是否繼續操作',
},
php: {
short_open_tag: '短標籤支援',

View file

@ -2397,6 +2397,9 @@ const message = {
execUser: '执行用户',
execDir: '执行目录',
packagist: '中国全量镜像',
batchOpreate: '批量操作',
batchOpreateHelper: '批量{0}网站是否继续操作',
},
php: {
short_open_tag: '短标签支持',

View file

@ -77,7 +77,9 @@
@cell-mouse-enter="showFavorite"
@cell-mouse-leave="hideFavorite"
localKey="websiteColumn"
v-model:selects="selects"
>
<el-table-column type="selection" width="30" />
<el-table-column
:label="$t('commons.table.name')"
fix
@ -264,6 +266,33 @@
:fixed="mobile ? false : 'right'"
fix
/>
<template #footerLeft>
<div class="footer-left-button">
<el-select class="p-w-200" v-model="batchReq.operate">
<el-option
:label="$t('commons.button.start') + $t('menu.website')"
value="start"
></el-option>
<el-option
:label="$t('commons.button.stop') + $t('menu.website')"
value="stop"
></el-option>
<el-option
:label="$t('commons.button.delete') + $t('menu.website')"
value="delete"
></el-option>
</el-select>
<el-button
class="ml-2"
type="primary"
:disabled="selects.length == 0 || batchReq.operate == ''"
@click="batchOp"
>
{{ $t('website.batchOpreate') }}
<span class="ml-1" v-if="selects.length > 0">({{ selects.length }})</span>
</el-button>
</div>
</template>
</ComplexTable>
<el-card width="30%" v-if="disabledConfig && maskShow" class="mask-prompt">
<span v-if="nginxIsExist">
@ -289,6 +318,7 @@
<GroupDialog @search="listGroup" ref="groupRef" />
<NginxConfig v-if="openNginxConfig" v-loading="loading" :containerName="containerName" :status="nginxStatus" />
<DefaultHtml ref="defaultHtmlRef" />
<TaskLog ref="taskLogRef" @close="search" />
</div>
</template>
@ -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<Website.Domain[]>([]);
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();