mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-05 06:04:35 +08:00
feat: Add local file selection for import/restore operations
This commit is contained in:
parent
dcb004e983
commit
a3c2dbe308
17 changed files with 246 additions and 124 deletions
|
@ -133,6 +133,28 @@ func (b *BaseApi) UpdateBackup(c *gin.Context) {
|
|||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Backup Account
|
||||
// @Summary Upload file for recover
|
||||
// @Accept json
|
||||
// @Param request body dto.UploadForRecover true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /backups/upload [post]
|
||||
// @x-panel-log {"bodyKeys":["filePath"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传备份文件 [filePath]","formatEN":"upload backup file [filePath]"}
|
||||
func (b *BaseApi) UploadForRecover(c *gin.Context) {
|
||||
var req dto.UploadForRecover
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := backupService.UploadForRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Backup Account
|
||||
// @Summary Load backup account options
|
||||
// @Accept json
|
||||
|
|
|
@ -53,6 +53,11 @@ type BackupOption struct {
|
|||
IsPublic bool `json:"isPublic"`
|
||||
}
|
||||
|
||||
type UploadForRecover struct {
|
||||
FilePath string `json:"filePath"`
|
||||
TargetDir string `json:"targetDir"`
|
||||
}
|
||||
|
||||
type CommonBackup struct {
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster"`
|
||||
Name string `json:"name"`
|
||||
|
|
|
@ -39,6 +39,7 @@ type IBackupService interface {
|
|||
Delete(id uint) error
|
||||
RefreshToken(req dto.OperateByID) error
|
||||
GetLocalDir() (string, error)
|
||||
UploadForRecover(req dto.UploadForRecover) error
|
||||
|
||||
MysqlBackup(db dto.CommonBackup) error
|
||||
PostgresqlBackup(db dto.CommonBackup) error
|
||||
|
@ -309,6 +310,16 @@ func (u *BackupService) RefreshToken(req dto.OperateByID) error {
|
|||
return backupRepo.Save(&backup)
|
||||
}
|
||||
|
||||
func (u *BackupService) UploadForRecover(req dto.UploadForRecover) error {
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(req.TargetDir) {
|
||||
if err := fileOp.CreateDir(req.TargetDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fileOp.Copy(req.FilePath, req.TargetDir)
|
||||
}
|
||||
|
||||
func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, error) {
|
||||
client, err := newClient(backup, false)
|
||||
if err != nil {
|
||||
|
|
|
@ -21,6 +21,7 @@ func (s *BackupRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
backupRouter.POST("", baseApi.CreateBackup)
|
||||
backupRouter.POST("/del", baseApi.DeleteBackup)
|
||||
backupRouter.POST("/update", baseApi.UpdateBackup)
|
||||
backupRouter.POST("/upload", baseApi.UploadForRecover)
|
||||
|
||||
backupRouter.POST("/backup", baseApi.Backup)
|
||||
backupRouter.POST("/recover", baseApi.Recover)
|
||||
|
|
|
@ -35,6 +35,9 @@ export const deleteBackupRecord = (params: { ids: number[] }) => {
|
|||
export const updateRecordDescription = (id: Number, description: String) => {
|
||||
return http.post(`/backups/record/description/update`, { id: id, description: description });
|
||||
};
|
||||
export const uploadByRecover = (filePath: string, targetDir: String) => {
|
||||
return http.post(`/backups/upload`, { filePath: filePath, targetDir: targetDir });
|
||||
};
|
||||
export const searchBackupRecords = (params: Backup.SearchBackupRecord) => {
|
||||
return http.post<ResPage<Backup.RecordInfo>>(`/backups/record/search`, params, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
|
|
@ -46,37 +46,54 @@
|
|||
</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="data" highlight-current-row height="40vh">
|
||||
<el-table-column show-overflow-tooltip fix>
|
||||
<el-table :data="data" highlight-current-row height="40vh" @row-click="openDir" class="cursor-pointer">
|
||||
<el-table-column prop="name" show-overflow-tooltip fix>
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<svg-icon
|
||||
:class="'table-icon'"
|
||||
:iconName="row.isDir ? 'p-file-folder' : 'p-file-normal'"
|
||||
></svg-icon>
|
||||
<svg-icon
|
||||
:class="'table-icon'"
|
||||
:iconName="row.isDir ? 'p-file-folder' : 'p-file-normal'"
|
||||
></svg-icon>
|
||||
<template v-if="!row.isCreate">
|
||||
{{ row.name }}
|
||||
</template>
|
||||
|
||||
<template v-if="!row.isCreate">
|
||||
<el-link underline="never" @click="openDir(row)">
|
||||
{{ row.name }}
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-input
|
||||
ref="rowRefs"
|
||||
v-model="newFolder"
|
||||
class="p-w-200"
|
||||
placeholder="new folder"
|
||||
@input="handleChange(newFolder, row)"
|
||||
></el-input>
|
||||
<el-button link @click="createFolder(row)" type="primary" size="small" class="ml-2">
|
||||
{{ $t('commons.button.save') }}
|
||||
</el-button>
|
||||
<el-button link @click="cancelFolder(row)" type="primary" size="small" class="!ml-2">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-input
|
||||
ref="rowRefs"
|
||||
v-model="newFolder"
|
||||
class="p-w-200"
|
||||
placeholder="new folder"
|
||||
@input="handleChange(newFolder, row)"
|
||||
></el-input>
|
||||
<el-button link @click="createFolder(row)" type="primary" size="small" class="ml-2">
|
||||
{{ $t('commons.button.save') }}
|
||||
</el-button>
|
||||
<el-button link @click="cancelFolder(row)" type="primary" size="small" class="!ml-2">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="size" width="160px" fix>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
small
|
||||
v-if="!row.isCreate"
|
||||
:loading="row.btnLoading"
|
||||
@click="row.isDir ? getDirSize(row.path) : getFileSize(row.path)"
|
||||
>
|
||||
<span v-if="row.isDir">
|
||||
<span v-if="row.dirSize === undefined">
|
||||
{{ $t('file.calculate') }}
|
||||
</span>
|
||||
<span v-else>{{ computeSize(row.dirSize) }}</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ computeSize(row.size) }}
|
||||
</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
@ -107,11 +124,12 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { File } from '@/api/interface/file';
|
||||
import { createFile, getFilesList } from '@/api/modules/files';
|
||||
import { computeDirSize, createFile, getFileContent, getFilesList } from '@/api/modules/files';
|
||||
import { onUpdated, reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import { useSearchableForSelect } from '@/views/host/file-management/hooks/searchable';
|
||||
import { computeSize } from '@/utils/util';
|
||||
|
||||
const data = ref([]);
|
||||
const loading = ref(false);
|
||||
|
@ -170,7 +188,10 @@ const openPage = () => {
|
|||
selectRow.value.path = form.dir ? form.path || '/' : '';
|
||||
};
|
||||
|
||||
const openDir = async (row: File.File) => {
|
||||
const openDir = async (row: File.File, column: any, event: any) => {
|
||||
if (event?.target?.tagName === 'BUTTON' || event?.target?.tagName === 'SPAN') {
|
||||
return;
|
||||
}
|
||||
if (row.isDir) {
|
||||
const name = row.name;
|
||||
paths.value.push(name);
|
||||
|
@ -220,6 +241,40 @@ const jumpPath = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const getFileSize = async (path: string) => {
|
||||
let params = {
|
||||
path: path,
|
||||
expand: true,
|
||||
isDetail: true,
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
};
|
||||
updateByPath(path, { btnLoading: true });
|
||||
try {
|
||||
const res = await getFileContent(params);
|
||||
updateByPath(path, { dirSize: res.data.size });
|
||||
} finally {
|
||||
updateByPath(path, { btnLoading: false });
|
||||
}
|
||||
};
|
||||
|
||||
const getDirSize = async (path: string) => {
|
||||
const req = {
|
||||
path: path,
|
||||
};
|
||||
updateByPath(path, { btnLoading: true });
|
||||
try {
|
||||
const res = await computeDirSize(req);
|
||||
updateByPath(path, { dirSize: res.data.size });
|
||||
} finally {
|
||||
updateByPath(path, { btnLoading: false });
|
||||
}
|
||||
};
|
||||
|
||||
const updateByPath = (path: string, patch: Partial<(typeof data.value)[0]>) => {
|
||||
data.value = data.value.map((item) => (item.path === path ? { ...item, ...patch } : item));
|
||||
};
|
||||
|
||||
const getPaths = (reqPath: string) => {
|
||||
const pathArray = reqPath.split('/');
|
||||
paths.value = [];
|
||||
|
|
|
@ -9,71 +9,57 @@
|
|||
>
|
||||
<template #content>
|
||||
<div v-loading="loading">
|
||||
<div class="mb-4" v-if="type === 'mysql' || type === 'mariadb'">
|
||||
<el-alert type="error" :title="$t('database.formatHelper', [remark])" />
|
||||
<div>
|
||||
<el-alert :closable="false" type="warning">
|
||||
<template #default>
|
||||
<ul>
|
||||
<li v-if="type === 'mysql' || type === 'mariadb'">
|
||||
{{ $t('database.formatHelper', [remark]) }}
|
||||
</li>
|
||||
<li v-if="type === 'website'">{{ $t('website.websiteBackupWarn') }}</li>
|
||||
<span v-if="isDb()">
|
||||
<li>{{ $t('database.supportUpType') }}</li>
|
||||
<li>{{ $t('database.zipFormat') }}</li>
|
||||
</span>
|
||||
<span v-else>
|
||||
<li>{{ $t('website.supportUpType') }}</li>
|
||||
<li>{{ $t('website.zipFormat', [type + '.json']) }}</li>
|
||||
</span>
|
||||
</ul>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
<div class="mb-4" v-if="type === 'website'">
|
||||
<el-alert :closable="false" type="warning" :title="$t('website.websiteBackupWarn')"></el-alert>
|
||||
</div>
|
||||
<el-upload
|
||||
:limit="1"
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:on-exceed="handleExceed"
|
||||
:on-change="fileOnChange"
|
||||
:on-remove="fileOnRemove"
|
||||
class="upload-demo"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
{{ $t('database.dropHelper') }}
|
||||
<em>{{ $t('database.clickHelper') }}</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<el-progress
|
||||
v-if="isUpload"
|
||||
text-inside
|
||||
:stroke-width="12"
|
||||
:percentage="uploadPercent"
|
||||
></el-progress>
|
||||
<div
|
||||
v-if="type === 'mysql' || type === 'mariadb' || type === 'postgresql'"
|
||||
class="w-4/5 el-upload__tip"
|
||||
>
|
||||
<span class="input-help">{{ $t('database.supportUpType') }}</span>
|
||||
<span class="input-help">
|
||||
{{ $t('database.zipFormat') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="w-4/5 el-upload__tip">
|
||||
<span class="input-help">{{ $t('website.supportUpType') }}</span>
|
||||
<span class="input-help">
|
||||
{{ $t('website.zipFormat', [type + '.json']) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button :disabled="isUpload || uploaderFiles.length !== 1" icon="Upload" @click="onSubmit">
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
|
||||
<el-divider />
|
||||
<ComplexTable
|
||||
:pagination-config="paginationConfig"
|
||||
class="mt-5"
|
||||
@search="search"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-button
|
||||
class="ml-2.5"
|
||||
plain
|
||||
:disabled="selects.length === 0"
|
||||
@click="onBatchDelete(null)"
|
||||
<el-upload
|
||||
:limit="1"
|
||||
class="float-left"
|
||||
ref="uploadRef"
|
||||
accept=".tar.gz,.sql,.sql.gz"
|
||||
:show-file-list="false"
|
||||
:on-exceed="handleExceed"
|
||||
:on-change="fileOnChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<el-button class="float-left">
|
||||
{{ $t('database.localUpload') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button class="float-left ml-3" @click="fileRef.acceptParams({ dir: false })">
|
||||
{{ $t('database.hostSelect') }}
|
||||
</el-button>
|
||||
<el-button :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
|
||||
<el-progress v-if="isUpload" text-inside :stroke-width="12" :percentage="uploadPercent" />
|
||||
</template>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" />
|
||||
|
@ -131,6 +117,7 @@
|
|||
</DialogPro>
|
||||
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<FileList ref="fileRef" @choose="loadFile" />
|
||||
<TaskLog ref="taskLogRef" @close="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -144,7 +131,7 @@ import { File } from '@/api/interface/file';
|
|||
import { batchDeleteFile, checkFile, chunkUploadFileData, getUploadList } from '@/api/modules/files';
|
||||
import { loadBaseDir } from '@/api/modules/setting';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { handleRecoverByUpload } from '@/api/modules/backup';
|
||||
import { handleRecoverByUpload, uploadByRecover } from '@/api/modules/backup';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
|
||||
interface DialogProps {
|
||||
|
@ -154,6 +141,7 @@ interface DialogProps {
|
|||
remark: string;
|
||||
}
|
||||
const loading = ref();
|
||||
const fileRef = ref();
|
||||
const isUpload = ref();
|
||||
const uploadPercent = ref<number>(0);
|
||||
const selects = ref<any>([]);
|
||||
|
@ -220,6 +208,32 @@ const search = async () => {
|
|||
paginationConfig.total = res.data.total;
|
||||
};
|
||||
|
||||
const loadFile = async (path: string) => {
|
||||
let filaName = path.split('/').pop();
|
||||
if (!filaName) {
|
||||
MsgError(i18n.global.t('commons.msg.fileNameErr'));
|
||||
return;
|
||||
}
|
||||
let reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-z:A-Z0-9_.\u4e00-\u9fa5-]{0,256}$/;
|
||||
if (!reg.test(filaName)) {
|
||||
MsgError(i18n.global.t('commons.msg.fileNameErr'));
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm(i18n.global.t('database.selectHelper', [path]), i18n.global.t('database.loadBackup'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
uploadByRecover(path, baseDir.value)
|
||||
.then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const openTaskLog = (taskID: string) => {
|
||||
taskLogRef.value.openWithTaskID(taskID);
|
||||
};
|
||||
|
@ -267,30 +281,32 @@ const onRecover = async (row: File.File) => {
|
|||
recoverDialog.value = true;
|
||||
};
|
||||
|
||||
const isDb = () => {
|
||||
return type.value === 'mysql' || type.value === 'mariadb' || type.value === 'postgresql';
|
||||
};
|
||||
const uploaderFiles = ref<UploadFiles>([]);
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
|
||||
const beforeAvatarUpload = (rawFile) => {
|
||||
if (type.value === 'app' || type.value === 'website') {
|
||||
if (!rawFile.name.endsWith('.tar.gz')) {
|
||||
MsgError(i18n.global.t('commons.msg.unSupportType'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!rawFile.name.endsWith('.sql') && !rawFile.name.endsWith('.tar.gz') && !rawFile.name.endsWith('.sql.gz')) {
|
||||
MsgError(i18n.global.t('commons.msg.unSupportType'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||
uploaderFiles.value = uploadFiles;
|
||||
};
|
||||
|
||||
const fileOnRemove = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||
uploaderFiles.value = uploadFiles;
|
||||
if (uploaderFiles.value.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const file = uploaderFiles.value[0];
|
||||
if (!file.raw.name) {
|
||||
MsgError(i18n.global.t('commons.msg.fileNameErr'));
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('database.selectHelper', [file.raw.name]),
|
||||
i18n.global.t('database.loadBackup'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
},
|
||||
).then(async () => {
|
||||
onSubmit();
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadClose = () => {
|
||||
|
@ -311,14 +327,7 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
|
|||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (uploaderFiles.value.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const file = uploaderFiles.value[0];
|
||||
if (!file.raw.name) {
|
||||
MsgError(i18n.global.t('commons.msg.fileNameErr'));
|
||||
return;
|
||||
}
|
||||
let reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-z:A-Z0-9_.\u4e00-\u9fa5-]{0,256}$/;
|
||||
if (!reg.test(file.raw.name)) {
|
||||
MsgError(i18n.global.t('commons.msg.fileNameErr'));
|
||||
|
@ -329,10 +338,6 @@ const onSubmit = async () => {
|
|||
MsgError(i18n.global.t('commons.msg.fileExist'));
|
||||
return;
|
||||
}
|
||||
let isOk = beforeAvatarUpload(file.raw);
|
||||
if (!isOk) {
|
||||
return;
|
||||
}
|
||||
submitUpload(file);
|
||||
};
|
||||
|
||||
|
|
|
@ -467,6 +467,9 @@ const message = {
|
|||
serviceNameHelper: 'Access between containers in the same network.',
|
||||
backupList: 'Backup',
|
||||
loadBackup: 'Import',
|
||||
localUpload: 'Local Upload',
|
||||
hostSelect: 'Server Selection',
|
||||
selectHelper: 'Are you sure you want to import backup file {0}?',
|
||||
remoteAccess: 'Remote access',
|
||||
remoteHelper: 'Multiple IP comma-delimited, example: 172.16.10.111, 172.16.10.112',
|
||||
remoteConnHelper:
|
||||
|
|
|
@ -454,6 +454,9 @@ const message = {
|
|||
serviceNameHelper: '同じネットワーク内のコンテナ間のアクセス。',
|
||||
backupList: 'バックアップ',
|
||||
loadBackup: '輸入',
|
||||
localUpload: 'ローカルアップロード',
|
||||
hostSelect: 'サーバー選択',
|
||||
selectHelper: 'バックアップファイル {0} をインポートしてもよろしいですか?',
|
||||
remoteAccess: 'リモートアクセス',
|
||||
remoteHelper: '複数のIP Comma delimited、例:172.16.10.111、172.16.10.112',
|
||||
remoteConnHelper:
|
||||
|
|
|
@ -457,6 +457,9 @@ const message = {
|
|||
serviceNameHelper: '같은 네트워크 내 컨테이너 간의 접근.',
|
||||
backupList: '백업',
|
||||
loadBackup: '불러오기',
|
||||
localUpload: '로컬 업로드',
|
||||
hostSelect: '서버 선택',
|
||||
selectHelper: '백업 파일 {0}을(를) 가져오시겠습니까?',
|
||||
remoteAccess: '원격 접근',
|
||||
remoteHelper: '여러 IP 를 쉼표로 구분하여 입력, 예: 172.16.10.111, 172.16.10.112',
|
||||
remoteConnHelper:
|
||||
|
|
|
@ -463,6 +463,9 @@ const message = {
|
|||
serviceNameHelper: 'Akses antara kontena dalam rangkaian yang sama.',
|
||||
backupList: 'Sandaran',
|
||||
loadBackup: 'Import',
|
||||
localUpload: 'Muat Naik Tempatan',
|
||||
hostSelect: 'Pemilihan Pelayan',
|
||||
selectHelper: 'Adakah anda pasti ingin mengimport fail sandaran {0}?',
|
||||
remoteAccess: 'Akses jauh',
|
||||
remoteHelper: 'Berbilang IP dipisahkan dengan koma, contoh: 172.16.10.111, 172.16.10.112',
|
||||
remoteConnHelper:
|
||||
|
|
|
@ -461,6 +461,9 @@ const message = {
|
|||
serviceNameHelper: 'Acesso entre containers na mesma rede.',
|
||||
backupList: 'Backup',
|
||||
loadBackup: 'Importar',
|
||||
localUpload: 'Upload Local',
|
||||
hostSelect: 'Seleção de Servidor',
|
||||
selectHelper: 'Tem certeza de que deseja importar o arquivo de backup {0}?',
|
||||
remoteAccess: 'Acesso remoto',
|
||||
remoteHelper: 'Vários IPs separados por vírgula, exemplo: 172.16.10.111, 172.16.10.112',
|
||||
remoteConnHelper:
|
||||
|
|
|
@ -456,6 +456,9 @@ const message = {
|
|||
serviceNameHelper: 'Доступ между контейнерами в одной сети.',
|
||||
backupList: 'Резервное копирование',
|
||||
loadBackup: 'Импорт',
|
||||
localUpload: 'Локальная загрузка',
|
||||
hostSelect: 'Выбор сервера',
|
||||
selectHelper: 'Вы уверены, что хотите импортировать файл резервной копии {0}?',
|
||||
remoteAccess: 'Удаленный доступ',
|
||||
remoteHelper: 'Несколько IP через запятую, например: 172.16.10.111, 172.16.10.112',
|
||||
remoteConnHelper:
|
||||
|
|
|
@ -472,6 +472,9 @@ const message = {
|
|||
serviceNameHelper: 'Aynı ağdaki konteynerler arası erişim.',
|
||||
backupList: 'Yedekleme',
|
||||
loadBackup: 'İçe Aktar',
|
||||
localUpload: 'Yerel Yükleme',
|
||||
hostSelect: 'Sunucu Seçimi',
|
||||
selectHelper: '{0} yedek dosyasını içe aktarmak istediğinizden emin misiniz?',
|
||||
remoteAccess: 'Uzaktan erişim',
|
||||
remoteHelper: 'Birden fazla IP virgülle ayrılır, örnek: 172.16.10.111, 172.16.10.112',
|
||||
remoteConnHelper:
|
||||
|
|
|
@ -456,6 +456,9 @@ const message = {
|
|||
serviceNameHelper: '用於同一 network 下的容器間訪問',
|
||||
backupList: '備份列表',
|
||||
loadBackup: '導入備份',
|
||||
localUpload: '本地上傳',
|
||||
hostSelect: '伺服器選擇',
|
||||
selectHelper: '是否確認導入備份文件 {0}?',
|
||||
remoteAccess: '遠程訪問',
|
||||
remoteHelper: '多個 ip 以逗號分隔,例:172.16.10.111,172.16.10.112',
|
||||
remoteConnHelper: 'root 帳號遠程連接 MySQL 有安全風險,開啟需謹慎!',
|
||||
|
|
|
@ -454,6 +454,9 @@ const message = {
|
|||
serviceNameHelper: '用于同一 network 下的容器间访问',
|
||||
backupList: '备份列表',
|
||||
loadBackup: '导入备份',
|
||||
localUpload: '本地上传',
|
||||
hostSelect: '服务器选择',
|
||||
selectHelper: '是否确认导入备份文件 {0}?',
|
||||
remoteAccess: '远程访问',
|
||||
remoteHelper: '多个 ip 以逗号分隔,例:172.16.10.111,172.16.10.112',
|
||||
remoteConnHelper: 'root 帐号远程连接 MySQL 有安全风险,开启需谨慎!',
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadRef"
|
||||
class="upload mt-2"
|
||||
class="float-left mt-2"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
:accept="'.json'"
|
||||
accept=".json"
|
||||
:on-change="fileOnChange"
|
||||
:on-exceed="handleExceed"
|
||||
v-model:file-list="uploaderFiles"
|
||||
|
@ -176,10 +176,3 @@ defineExpose({
|
|||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload {
|
||||
width: 60px;
|
||||
float: left;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Add table
Reference in a new issue