1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-02-08 09:30:06 +08:00

feat: Support for modifying host information (#7816)

This commit is contained in:
ssongliu 2025-02-07 18:17:21 +08:00 committed by GitHub
parent 88684c7cf0
commit 6cd72f1a56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 149 additions and 169 deletions

View File

@ -266,8 +266,8 @@ func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) {
req.GroupID = group.ID
}
var sameHostID uint
if req.Addr == "127.0.0.1" {
hostSame, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr))
if req.Name == "local" {
hostSame, _ := hostRepo.Get(repo.WithByName("local"))
sameHostID = hostSame.ID
} else {
hostSame, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr), hostRepo.WithByUser(req.User), hostRepo.WithByPort(req.Port))

View File

@ -1,14 +1,16 @@
system:
db_core_file: core.db
base_dir: /opt
base:
install_dir: /opt
mode: dev
repo_url: https://resource.fit2cloud.com/1panel/package
app_repo: https://apps-assets.fit2cloud.com
is_demo: false
is_intl: false
port: 9999
username: admin
password: admin123
version: v2.0.0
remote_url:
repo_url: https://resource.fit2cloud.com/1panel/package
app_repo: https://apps-assets.fit2cloud.com
log:
level: debug

View File

@ -37,7 +37,6 @@ type ApiInterface struct {
type RemoteURL struct {
RepoUrl string `mapstructure:"repo_url"`
ResourceUrl string `mapstructure:"resource_url"`
}
type LogConfig struct {

View File

@ -20,6 +20,7 @@ func Init() {
migrations.AddTaskDB,
migrations.UpdateSettingStatus,
migrations.RemoveLocalBackup,
migrations.AddMFAInterval,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -297,3 +297,13 @@ var RemoveLocalBackup = &gormigrate.Migration{
return nil
},
}
var AddMFAInterval = &gormigrate.Migration{
ID: "20250207-add-mfa-interval",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "MFAInterval", Value: "30"}).Error; err != nil {
return err
}
return nil
},
}

View File

@ -21,6 +21,8 @@
},
"dependencies": {
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/language": "^6.10.2",
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/theme-one-dark": "^6.1.2",

View File

@ -2,7 +2,7 @@
<DrawerPro v-model="open" :header="$t('commons.table.group')" size="large" :back="handleClose">
<template #content>
<ComplexTable :data="data" @search="search()">
<template #leftToolBar>
<template #toolbar>
<el-button type="primary" @click="openCreate">{{ $t('website.createGroup') }}</el-button>
</template>
<el-table-column :label="$t('commons.table.name')" prop="name">

View File

@ -65,7 +65,7 @@
v-model:selects="selects"
:data="data"
>
<template #leftToolBar>
<template #toolbar>
<el-button
class="ml-2.5"
plain

View File

@ -594,10 +594,14 @@ const message = {
operatorHelper: '{0} will be performed on the following container, Do you want to continue?',
operatorAppHelper:
'The {0} operation will be performed on the following containers,\n some of which are from the App Store. This operation may affect the normal use of the service. \nDo you want to continue?',
start: 'Start',
stop: 'Stop',
restart: 'Restart',
kill: 'Kill',
pause: 'Pause',
unpause: 'Unpause',
rename: 'Rename',
remove: 'Remove',
removeAll: 'Remove All',
containerPrune: 'Prune',
containerPruneHelper1: 'Cleaning containers will delete all containers that are in a stopped state.',
@ -1108,6 +1112,7 @@ const message = {
},
ftp: {
ftp: 'FTP Account',
notStart: 'FTP service is currently not running, please start it first!',
noFtp: 'FTP (pure-ftpd) service not detected, please refer to the official documentation for installation!',
operation: 'Perform [{0}] operation on FTP service, continue?',
noPasswdMsg: 'Can not get the current FTP account password, please set the password and try again! ',

View File

@ -580,10 +580,14 @@ const message = {
operatorHelper: '{0}は次のコンテナで実行されます続行しますか',
operatorAppHelper:
'{0}操作は次のコンテナで実行され実行中のサービスに影響を与える可能性があります続けたいですか',
start: '始める',
stop: '停止',
restart: '再起動',
kill: '殺す',
pause: '一時停止',
unpause: '再開する',
rename: '名前を変更します',
remove: '取り除く',
removeAll: 'すべてを削除します',
containerPrune: 'プルーン',
containerPruneHelper1: 'これにより停止状態にあるすべてのコンテナが削除されます',
@ -1049,6 +1053,7 @@ const message = {
},
ftp: {
ftp: 'FTPアカウント|FTPアカウント',
notStart: 'FTP Serviceは現在実行されていません最初に開始してください',
noFtp: `FTPPure-FTPDサービスは検出されません。インストールする公式ドキュメントを参照してください。`,
operation: 'これによりFTPサービスで{0}操作が実行されます続けたいですか',
noPasswdMsg: '現在のFTPアカウントパスワードを取得できませんパスワードを設定して再試行してください',

View File

@ -575,10 +575,14 @@ const message = {
operatorHelper: '{0} 작업이 다음 컨테이너에서 수행됩니다. 계속하시겠습니까?',
operatorAppHelper:
'"{0}" 작업이 다음 컨테이너에서 수행되며, 실행 중인 서비스에 영향을 미칠 있습니다. 계속하시겠습니까?',
start: '시작',
stop: '중지',
restart: '재시작',
kill: '강제 종료',
pause: '일시 정지',
unpause: '재개',
rename: '이름 변경',
remove: '제거',
removeAll: '모두 제거',
containerPrune: '정리',
containerPruneHelper1: ' 작업은 중지된 모든 컨테이너를 삭제합니다.',
@ -1040,6 +1044,7 @@ const message = {
},
ftp: {
ftp: 'FTP 계정 | FTP 계정들',
notStart: 'FTP 서비스가 현재 실행 중이 아닙니다. 먼저 시작하세요!',
noFtp: `FTP (pure-ftpd) 서비스가 감지되지 않았습니다. 공식 문서를 참조하여 설치하세요.`,
operation: 'FTP 서비스에서 "{0}" 작업을 수행합니다. 계속하시겠습니까?',
noPasswdMsg: '현재 FTP 계정의 비밀번호를 가져올 없습니다. 비밀번호를 설정한 다시 시도하세요!',

View File

@ -592,10 +592,14 @@ const message = {
operatorHelper: '{0} akan dilakukan pada kontena berikut. Adakah anda mahu meneruskan?',
operatorAppHelper:
'Operasi "{0}" akan dilakukan pada kontena berikut dan mungkin mempengaruhi perkhidmatan yang sedang berjalan. Adakah anda mahu meneruskan?',
start: 'Mulakan',
stop: 'Hentikan',
restart: 'Mulakan semula',
kill: 'Hentikan paksa',
pause: 'Jeda',
unpause: 'Sambung semula',
rename: 'Tukar nama',
remove: 'Buang',
removeAll: 'Buang semua',
containerPrune: 'Prune',
containerPruneHelper1: 'Ini akan memadam semua kontena yang berada dalam keadaan dihentikan.',
@ -1086,6 +1090,7 @@ const message = {
},
ftp: {
ftp: 'Akaun FTP | Akaun FTP',
notStart: 'Perkhidmatan FTP tidak berjalan pada masa ini, sila mulakan dahulu!',
noFtp: 'Perkhidmatan FTP (pure-ftpd) tidak dikesan. Rujuk dokumen rasmi untuk memasang.',
operation: 'Ini akan melaksanakan operasi "{0}" pada perkhidmatan FTP. Adakah anda mahu meneruskan?',
noPasswdMsg:

View File

@ -590,10 +590,14 @@ const message = {
operatorHelper: '{0} será realizado no seguinte contêiner. Deseja continuar?',
operatorAppHelper:
'A operação "{0}" será realizada no(s) seguinte(s) contêiner(es) e pode afetar os serviços em execução. Deseja continuar?',
start: 'Iniciar',
stop: 'Parar',
restart: 'Reiniciar',
kill: 'Finalizar',
pause: 'Pausar',
unpause: 'Retomar',
rename: 'Renomear',
remove: 'Remover',
removeAll: 'Remover todos',
containerPrune: 'Limpar',
containerPruneHelper1: 'Isso excluirá todos os contêineres que estão no estado parado.',
@ -1074,6 +1078,7 @@ const message = {
},
ftp: {
ftp: 'Conta FTP | Contas FTP',
notStart: 'O serviço FTP não está em execução, por favor, inicie-o primeiro!',
noFtp: `O serviço FTP (pure-ftpd) não foi detectado. Consulte a documentação oficial para instalá-lo.`,
operation: 'Isso realizará a operação "{0}" no serviço FTP. Deseja continuar?',
noPasswdMsg: 'Não foi possível obter a senha atual da conta FTP, por favor, defina a senha e tente novamente!',

View File

@ -588,10 +588,14 @@ const message = {
operatorHelper: 'Действие {0} будет выполнено для следующего контейнера. Хотите продолжить?',
operatorAppHelper:
'Операция "{0}" будет выполнена для следующего(-их) контейнера(-ов) и может повлиять на работающие сервисы. Хотите продолжить?',
start: 'Запустить',
stop: 'Остановить',
restart: 'Перезапустить',
kill: 'Завершить',
pause: 'Приостановить',
unpause: 'Возобновить',
rename: 'Переименовать',
remove: 'Удалить',
removeAll: 'Удалить все',
containerPrune: 'Очистить',
containerPruneHelper1: 'Это удалит все контейнеры, которые находятся в остановленном состоянии.',
@ -1078,6 +1082,7 @@ const message = {
},
ftp: {
ftp: 'FTP аккаунт | FTP аккаунты',
notStart: 'Служба FTP в настоящее время не запущена, пожалуйста, сначала запустите её!',
noFtp: 'Служба FTP (pure-ftpd) не обнаружена. Обратитесь к официальной документации для установки.',
operation: 'Это выполнит операцию "{0}" над службой FTP. Хотите продолжить?',
noPasswdMsg:

View File

@ -572,10 +572,14 @@ const message = {
operatorHelper: '將對以下容器進行 {0} 操作是否繼續',
operatorAppHelper:
'將對以下容器進行 {0} 操作\n其中部分來源於應用商店該操作可能會影響到該服務的正常使用\n是否確認',
start: '啟動',
stop: '停止',
restart: '重啟',
kill: '強製停止',
pause: '暫停',
unpause: '恢復',
rename: '重命名',
remove: '刪除',
removeAll: '删除所有',
containerPrune: '清理容器',
containerPruneHelper1: '清理容器 將刪除所有處於停止狀態的容器',
@ -1047,6 +1051,7 @@ const message = {
},
ftp: {
ftp: 'FTP 帳戶',
notStart: '當前未 FTP 服務請先開啟',
noFtp: '未檢測到 FTP (pure-ftpd) 服務請參考官方文檔進行安裝',
operation: ' FTP 服務進行 [{0}] 操作是否繼續',
noPasswdMsg: '無法獲取當前 FTP 賬號密碼請先設置密碼後重試',

View File

@ -570,10 +570,14 @@ const message = {
operatorHelper: '将对以下容器进行 {0} 操作是否继续',
operatorAppHelper:
'将对以下容器进行 {0} 操作\n其中部分来源于应用商店该操作可能会影响到该服务的正常使用\n是否继续',
start: '启动',
stop: '停止',
restart: '重启',
kill: '强制停止',
pause: '暂停',
unpause: '恢复',
rename: '重命名',
remove: '删除',
removeAll: '删除所有',
containerPrune: '清理容器',
containerPruneHelper1: '清理容器 将删除所有处于停止状态的容器',
@ -1044,6 +1048,7 @@ const message = {
},
ftp: {
ftp: 'FTP 账户',
notStart: '当前未开启 FTP 服务请先开启',
noFtp: '未检测到 FTP (pure-ftpd) 服务请参考官方文档进行安装',
operation: ' FTP 服务进行 [{0}] 操作是否继续',
noPasswdMsg: '无法获取当前 FTP 账号密码请先设置密码后重试',

View File

@ -80,7 +80,7 @@
</el-row>
<el-card style="margin-top: 20px">
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" @search="search" :data="data">
<template #leftToolBar>
<template #toolbar>
<el-button type="primary" @click="onBackup">{{ $t('commons.button.backup') }}</el-button>
<el-button type="primary" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
{{ $t('commons.button.delete') }}

View File

@ -19,8 +19,13 @@
<el-select v-model="group" @change="search()" clearable class="p-w-200 mr-2.5">
<template #prefix>{{ $t('commons.table.group') }}</template>
<el-option :label="$t('commons.table.all')" value=""></el-option>
<div v-for="item in groupList" :key="item.name">
<el-option :value="item.id" :label="item.name" />
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
</el-select>
<TableSearch @search="search()" v-model:searchName="info" />
@ -56,7 +61,23 @@
min-width="100"
prop="groupBelong"
fix
>
<template #default="{ row }">
<fu-select-rw-switch v-model="row.groupID" @change="updateGroup(row)">
<template #read>
{{ row.groupBelong === 'default' ? $t('commons.table.default') : row.groupBelong }}
</template>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
</fu-select-rw-switch>
</template>
</el-table-column>
<fu-table-operations width="200px" :buttons="buttons" :label="$t('commons.table.operate')" fix />
</ComplexTable>
</template>
@ -81,7 +102,12 @@
<el-form-item :label="$t('commons.table.group')" prop="name">
<el-select filterable v-model="commandInfo.groupID" clearable style="width: 100%">
<div v-for="item in groupList" :key="item.id">
<el-option :label="item.name" :value="item.id" />
<el-option
v-if="item.name === 'default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
</el-select>
</el-form-item>
@ -101,14 +127,12 @@
<OpDialog ref="opRef" @search="search" />
<GroupDialog @search="loadGroups" ref="dialogGroupRef" />
<GroupChangeDialog @search="search" @change="onChangeGroup" ref="dialogGroupChangeRef" />
</div>
</template>
<script setup lang="ts">
import { Command } from '@/api/interface/command';
import GroupDialog from '@/components/group/index.vue';
import GroupChangeDialog from '@/components/group/change.vue';
import { addCommand, editCommand, deleteCommand, getCommandPage } from '@/api/modules/command';
import { reactive, ref } from 'vue';
import type { ElForm } from 'element-plus';
@ -131,7 +155,6 @@ const paginationConfig = reactive({
});
const info = ref();
const group = ref<string>('');
const dialogGroupChangeRef = ref();
const opRef = ref();
@ -203,9 +226,8 @@ const submitAddCommand = (formEl: FormInstance | undefined) => {
});
};
const onChangeGroup = async (groupID: number) => {
commandInfo.groupID = groupID;
await editCommand(commandInfo);
const updateGroup = async (row: any) => {
await editCommand(row);
search();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
};
@ -235,16 +257,6 @@ const batchDelete = async (row: Command.CommandInfo | null) => {
};
const buttons = [
{
label: i18n.global.t('terminal.groupChange'),
click: (row: any) => {
commandInfo = row;
dialogGroupChangeRef.value!.acceptParams({
group: row.groupBelong,
groupType: 'command',
});
},
},
{
label: i18n.global.t('commons.button.edit'),
icon: 'Edit',

View File

@ -1,100 +0,0 @@
<template>
<DrawerPro v-model="drawerVisible" :header="$t('terminal.groupChange')" :back="handleClose" size="small">
<el-form
@submit.prevent
ref="hostInfoRef"
label-position="top"
:model="dialogData"
:rules="rules"
v-loading="loading"
>
<el-form-item :label="$t('commons.table.group')" prop="group">
<el-select filterable v-model="dialogData.groupID" clearable class="w-full">
<div v-for="item in groupList" :key="item.id">
<el-option :label="item.name" :value="item.id" />
</div>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(hostInfoRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import type { ElForm } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { editHostGroup } from '@/api/modules/terminal';
import { getGroupList } from '@/api/modules/group';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const loading = ref();
interface DialogProps {
id: number;
group: string;
}
const drawerVisible = ref(false);
const dialogData = ref({
id: 0,
groupID: 0,
});
const groupList = ref();
const acceptParams = (params: DialogProps): void => {
dialogData.value.id = params.id;
loadGroups(params.group);
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const handleClose = () => {
drawerVisible.value = false;
};
type FormInstance = InstanceType<typeof ElForm>;
const hostInfoRef = ref<FormInstance>();
const rules = reactive({
groupID: [Rules.requiredSelect],
});
const loadGroups = async (groupName: string) => {
const res = await getGroupList('host');
groupList.value = res.data;
for (const group of groupList.value) {
if (group.name === groupName) {
dialogData.value.groupID = group.id;
break;
}
}
};
const onSubmit = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
await editHostGroup(dialogData.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
drawerVisible.value = false;
emit('search');
})
.catch(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -16,8 +16,13 @@
<el-select v-model="group" @change="search()" clearable class="p-w-200 mr-5">
<template #prefix>{{ $t('commons.table.group') }}</template>
<el-option :label="$t('commons.table.all')" value=""></el-option>
<div v-for="item in groupList" :key="item.name">
<el-option :value="item.id" :label="item.name" />
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
</el-select>
<TableSearch @search="search()" v-model:searchName="info" />
@ -33,10 +38,26 @@
<el-table-column :label="$t('terminal.ip')" prop="addr" fix />
<el-table-column :label="$t('commons.login.username')" show-overflow-tooltip prop="user" />
<el-table-column :label="$t('commons.table.port')" prop="port" />
<el-table-column :label="$t('commons.table.group')" show-overflow-tooltip prop="groupBelong">
<el-table-column
:label="$t('commons.table.group')"
prop="description"
min-width="80"
show-overflow-tooltip
>
<template #default="{ row }">
<span v-if="row.groupBelong === 'default'">{{ $t('commons.table.default') }}</span>
<span v-else>{{ row.groupBelong }}</span>
<fu-select-rw-switch v-model="row.groupID" @change="updateGroup(row)">
<template #read>
{{ row.groupBelong === 'default' ? $t('commons.table.default') : row.groupBelong }}
</template>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
</fu-select-rw-switch>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.title')" show-overflow-tooltip prop="name" />
@ -53,13 +74,11 @@
<OpDialog ref="opRef" @search="search" />
<OperateDialog @search="search" ref="dialogRef" />
<GroupDialog @search="search" ref="dialogGroupRef" />
<GroupChangeDialog @search="search" @change="onChangeGroup" ref="dialogGroupChangeRef" />
</div>
</template>
<script setup lang="ts">
import GroupDialog from '@/components/group/index.vue';
import GroupChangeDialog from '@/components/group/change.vue';
import OperateDialog from '@/views/terminal/host/operate/index.vue';
import { deleteHost, editHostGroup, searchHosts } from '@/api/modules/terminal';
import { getGroupList } from '@/api/modules/group';
@ -80,8 +99,6 @@ const paginationConfig = reactive({
});
const info = ref();
const group = ref<string>('');
const dialogGroupChangeRef = ref();
const currentID = ref();
const opRef = ref();
@ -142,24 +159,13 @@ const loadGroups = async () => {
groupList.value = res.data;
};
const onChangeGroup = async (groupID: number) => {
let param = {
id: currentID.value,
groupID: groupID,
};
await editHostGroup(param);
const updateGroup = async (row: any) => {
await editHostGroup({ id: row.id, groupID: row.groupID });
search();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
};
const buttons = [
{
label: i18n.global.t('terminal.groupChange'),
click: (row: any) => {
currentID.value = row.id;
dialogGroupChangeRef.value!.acceptParams({ group: row.groupBelong, groupType: 'host' });
},
},
{
label: i18n.global.t('commons.button.edit'),
click: (row: any) => {

View File

@ -61,7 +61,8 @@
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input :disabled="itemName === 'local'" clearable v-model="dialogData.rowData!.name" />
<el-tag v-if="itemName === 'local'">local</el-tag>
<el-input v-else clearable v-model="dialogData.rowData!.name" />
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input clearable type="textarea" v-model="dialogData.rowData!.description" />
@ -107,6 +108,7 @@ const itemName = ref();
const groupList = ref();
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
itemName.value = params.rowData.name;
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
drawerVisible.value = true;
loadGroups();

View File

@ -119,7 +119,7 @@ interface DialogProps {
}
const acceptParams = (props: DialogProps) => {
hostInfo.addr = '';
hostInfo.name = '';
hostInfo.name = 'local';
hostInfo.groupID = 0;
hostInfo.addr = '';
hostInfo.port = 22;

View File

@ -215,7 +215,7 @@ const acceptParams = async () => {
continue;
}
for (let i = 0; i < hostTree.value[gIndex].children.length; i++) {
if (hostTree.value[gIndex].children[i].label.indexOf('@127.0.0.1:') !== -1) {
if (hostTree.value[gIndex].children[i].label.startsWith('local - ')) {
localHostID.value = hostTree.value[gIndex].children[i].id;
hostTree.value[gIndex].children.splice(i, 1);
if (hostTree.value[gIndex].children.length === 0) {

View File

@ -8,7 +8,7 @@
</template>
<template #main>
<el-row class="mt-5 mb-5">
<el-col :xs="24" :sm="20" :md="20" :lg="10" :xl="10">
<el-col :xs="22" :sm="20" :md="20" :lg="10" :xl="10" :offset="1">
<div v-if="scanStatus !== 'scanned'">
<div v-if="scanStatus === 'beforeScan'">
<div v-if="form.lastCleanTime">

View File

@ -23,15 +23,15 @@
</el-card>
</div>
<div v-if="form.isExist">
<LayoutContent v-loading="loading" title="FTP">
<LayoutContent v-loading="loading" title="FTP" :class="{ mask: !form.isActive }">
<template #leftToolBar>
<el-button type="primary" :disabled="!form.isActive" @click="onOpenDialog('add')">
<el-button type="primary" @click="onOpenDialog('add')">
{{ $t('commons.button.add') }} FTP
</el-button>
<el-button @click="onSync()" :disabled="!form.isActive">
<el-button @click="onSync()">
{{ $t('commons.button.sync') }}
</el-button>
<el-button plain :disabled="selects.length === 0 || !form.isActive" @click="onDelete(null)">
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
@ -124,6 +124,10 @@
</ComplexTable>
</template>
</LayoutContent>
<el-card v-if="form.isExist && !form.isActive" class="mask-prompt">
<span>{{ $t('toolbox.ftp.notStart') }}</span>
</el-card>
</div>
<div v-else>
<LayoutContent title="FTP" :divider="true">

View File

@ -41,12 +41,14 @@
>
<template #prefix>{{ $t('commons.table.group') }}</template>
<el-option :label="$t('commons.table.all')" :value="0"></el-option>
<div v-for="item in groups" :key="item.id">
<el-option
v-for="(group, index) in groups"
:key="index"
:label="group.name"
:value="group.id"
></el-option>
v-if="item.name === 'default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
</el-select>
<TableSearch @search="search()" v-model:searchName="req.name" />
<div class="!ml-2.5">