feat: Add download task button to view current ongoing download tasks (#7995)

This commit is contained in:
zhengkunwang 2025-02-25 16:39:12 +08:00 committed by GitHub
parent f18a37ad47
commit 51bb42c493
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 125 additions and 38 deletions

View file

@ -748,7 +748,7 @@ var wsUpgrade = websocket.Upgrader{
},
}
func (b *BaseApi) Ws(c *gin.Context) {
func (b *BaseApi) WgetProcess(c *gin.Context) {
ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
@ -758,7 +758,7 @@ func (b *BaseApi) Ws(c *gin.Context) {
go wsClient.Write()
}
func (b *BaseApi) Keys(c *gin.Context) {
func (b *BaseApi) ProcessKeys(c *gin.Context) {
res := &response.FileProcessKeys{}
keys := global.CACHE.PrefixScanKey("file-wget-")
res.Keys = keys

View file

@ -33,8 +33,8 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.GET("/download", baseApi.Download)
fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles)
fileRouter.POST("/size", baseApi.Size)
fileRouter.GET("/ws", baseApi.Ws)
fileRouter.GET("/keys", baseApi.Keys)
fileRouter.GET("/wget/process", baseApi.WgetProcess)
fileRouter.GET("/wget/process/keys", baseApi.ProcessKeys)
fileRouter.POST("/read", baseApi.ReadFileByLine)
fileRouter.POST("/batch/role", baseApi.BatchChangeModeAndOwner)

View file

@ -85,8 +85,8 @@ export const computeDirSize = (params: File.DirSizeReq) => {
return http.post<File.DirSizeRes>('files/size', params, TimeoutEnum.T_5M);
};
export const fileKeys = () => {
return http.get<File.FileKeys>('files/keys');
export const fileWgetKeys = () => {
return http.get<File.FileKeys>('files//wget/process/keys');
};
export const getRecycleList = (params: ReqPage) => {

View file

@ -1392,6 +1392,7 @@ const message = {
minimap: 'Code Mini Map',
fileCanNotRead: 'File can not read',
panelInstallDir: '1Panel installation directory cannot be deleted',
wgetTask: 'Download Task',
},
ssh: {
autoStart: 'Auto Start',

View file

@ -1334,6 +1334,7 @@ const message = {
minimap: 'コードミニマップ',
fileCanNotRead: 'ファイルは読み取れません',
panelInstallDir: `1Panelインストールディレクトリは削除できません`,
wgetTask: 'ダウンロードタスク',
},
ssh: {
setting: '設定',

View file

@ -1320,6 +1320,7 @@ const message = {
minimap: '코드 미니맵',
fileCanNotRead: '파일을 읽을 없습니다.',
panelInstallDir: `1Panel 설치 디렉터리는 삭제할 수 없습니다.`,
wgetTask: '다운로드 작업',
},
ssh: {
setting: '설정',

View file

@ -1377,6 +1377,7 @@ const message = {
minimap: 'Peta mini kod',
fileCanNotRead: 'Fail tidak dapat dibaca',
panelInstallDir: 'Direktori pemasangan 1Panel tidak boleh dipadamkan',
wgetTask: 'Tugas Muat Turun',
},
ssh: {
setting: 'tetapan',

View file

@ -1362,6 +1362,7 @@ const message = {
minimap: 'Mini mapa de código',
fileCanNotRead: 'O arquivo não pode ser lido',
panelInstallDir: 'O diretório de instalação do 1Panel não pode ser excluído',
wgetTask: 'Tarefa de Download',
},
ssh: {
setting: 'configuração',

View file

@ -1366,6 +1366,7 @@ const message = {
minimap: 'Мини-карта кода',
fileCanNotRead: 'Файл не может быть прочитан',
panelInstallDir: 'Директорию установки 1Panel нельзя удалить',
wgetTask: 'Задача загрузки',
},
ssh: {
setting: 'настройка',

View file

@ -1344,6 +1344,7 @@ const message = {
minimap: '縮略圖',
fileCanNotRead: '此文件不支持預覽',
panelInstallDir: '1Panel 安裝目錄不能删除',
wgetTask: '下載任務',
},
ssh: {
autoStart: '開機自啟',

View file

@ -1317,6 +1317,7 @@ const message = {
minimap: '缩略图',
fileCanNotRead: '此文件不支持预览',
panelInstallDir: '1Panel 安装目录不能删除',
wgetTask: '下载任务',
},
ssh: {
autoStart: '开机自启',

View file

@ -102,6 +102,12 @@
{{ $t('menu.terminal') }}
</el-button>
<el-badge :value="processCount" class="btn" v-if="processCount > 0">
<el-button class="btn" @click="openProcess">
{{ $t('file.wgetTask') }}
</el-button>
</el-badge>
<el-button-group class="copy-button" v-if="moveOpen">
<el-tooltip class="box-item" effect="dark" :content="$t('file.paste')" placement="bottom">
<el-button plain @click="openPaste">{{ $t('file.paste') }}({{ fileMove.count }})</el-button>
@ -291,7 +297,7 @@
<Wget ref="wgetRef" @close="closeWget" />
<Move ref="moveRef" @close="closeMovePage" />
<Download ref="downloadRef" @close="search" />
<Process :open="processPage.open" @close="closeProcess" />
<Process ref="processRef" @close="getWgetProcess" />
<Owner ref="chownRef" @close="search"></Owner>
<Detail ref="detailRef" />
<DeleteFile ref="deleteRef" @close="search" />
@ -313,6 +319,7 @@ import {
addFavorite,
removeFavorite,
searchFavorite,
fileWgetKeys,
} from '@/api/modules/files';
import { computeSize, copyText, dateFormat, getFileType, getIcon, getRandomStr, downloadFile } from '@/utils/util';
import { File } from '@/api/interface/file';
@ -386,7 +393,6 @@ const fileUpload = reactive({ path: '' });
const fileRename = reactive({ path: '', oldName: '' });
const fileWget = reactive({ path: '' });
const fileMove = reactive({ oldPaths: [''], type: '', path: '', name: '', count: 0 });
const processPage = reactive({ open: false });
const createRef = ref();
const roleRef = ref();
@ -411,6 +417,7 @@ const favorites = ref([]);
const batchRoleRef = ref();
const dialogVscodeOpenRef = ref();
const previewRef = ref();
const processRef = ref();
// editablePath
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
@ -754,11 +761,17 @@ const closeMovePage = (submit: Boolean) => {
};
const openProcess = () => {
processPage.open = true;
processRef.value.acceptParams();
};
const closeProcess = () => {
processPage.open = false;
const processCount = ref(0);
const getWgetProcess = async () => {
try {
const res = await fileWgetKeys();
if (res.data && res.data.keys.length > 0) {
processCount.value = res.data.keys.length;
}
} catch (error) {}
};
const openRename = (item: File.File) => {
@ -965,6 +978,7 @@ onMounted(() => {
nextTick(function () {
handlePath();
});
getWgetProcess();
});
</script>

View file

@ -1,39 +1,68 @@
<template>
<DialogPro v-model="open" :title="$t('file.downloadProcess')" size="small" @open="onOpen" @close="handleClose">
<div v-for="(value, index) in res" :key="index">
<span>{{ value['percent'] === 100 ? $t('file.downloadSuccess') : $t('file.downloading') }}</span>
<MsgInfo :info="value['name']" width="250" />
<el-progress v-if="value['total'] == 0" :percentage="100" :indeterminate="true" :duration="1" />
<el-progress v-else :text-inside="true" :stroke-width="15" :percentage="value['percent']"></el-progress>
<span>
{{ getFileSize(value['written']) }}/
<span v-if="value['total'] > 0">{{ getFileSize(value['total']) }}</span>
</span>
<DialogPro v-model="open" :title="$t('file.downloadProcess')" size="small" @close="handleClose">
<template #content>
<div class="space-y-4 p-4" :loading="loading">
<div
v-for="(value, index) in res"
:key="index"
class="bg-white rounded-lg p-4 shadow-sm border border-gray-100 transition-all duration-200 hover:shadow-md"
:class="{ completed: value.percent === 100 }"
>
<div class="flex items-center gap-3">
<div class="flex-1">
<MsgInfo :info="value.name" class="text-gray-700" />
<div class="text-gray-500">
{{ value.percent === 100 ? $t('file.downloadSuccess') : $t('file.downloading') }}
</div>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-end text-gray-500 mb-1">
<span>{{ getFileSize(value.written) }}</span>
<span v-if="value.total > 0" class="text-gray-400">/{{ getFileSize(value.total) }}</span>
</div>
<div class="w-full">
<el-progress
v-if="value.total === 0"
:percentage="100"
:indeterminate="true"
:duration="1"
class="progress-bar"
:stroke-width="8"
:show-text="false"
/>
<el-progress
v-else
:percentage="value.percent"
:stroke-width="8"
class="progress-bar"
:status="value.percent === 100 ? 'success' : ''"
/>
</div>
</div>
</div>
</div>
</template>
</DialogPro>
</template>
<script lang="ts" setup>
import { fileKeys } from '@/api/modules/files';
import { fileWgetKeys } from '@/api/modules/files';
import { computeSize } from '@/utils/util';
import { onBeforeUnmount, ref, toRefs } from 'vue';
import { onBeforeUnmount, ref } from 'vue';
import MsgInfo from '@/components/msg-info/index.vue';
const props = defineProps({
open: {
type: Boolean,
default: false,
},
});
const { open } = toRefs(props);
let processSocket = ref(null) as unknown as WebSocket;
const res = ref([]);
const keys = ref(['']);
const open = ref(false);
const loading = ref(false);
const em = defineEmits(['close']);
const handleClose = () => {
closeSocket();
open.value = false;
em('close', open);
};
@ -58,7 +87,7 @@ const initProcess = () => {
let href = window.location.href;
let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
let ipLocal = href.split('//')[1].split('/')[0];
processSocket = new WebSocket(`${protocol}://${ipLocal}/api/v2/files/ws`);
processSocket = new WebSocket(`${protocol}://${ipLocal}/api/v2/files/wget/process`);
processSocket.onopen = onOpenProcess;
processSocket.onmessage = onMessage;
processSocket.onerror = onerror;
@ -66,15 +95,20 @@ const initProcess = () => {
sendMsg();
};
const getKeys = () => {
const getKeys = async () => {
keys.value = [];
res.value = [];
fileKeys().then((res) => {
if (res.data.keys.length > 0) {
loading.value = true;
try {
const res = await fileWgetKeys();
if (res.data && res.data.keys.length > 0) {
keys.value = res.data.keys;
initProcess();
}
});
} catch (error) {
} finally {
loading.value = false;
}
};
const sendMsg = () => {
@ -98,7 +132,38 @@ onBeforeUnmount(() => {
closeSocket();
});
const onOpen = () => {
const acceptParams = () => {
open.value = true;
getKeys();
};
defineExpose({ acceptParams });
</script>
<style type="scss" scoped>
.download-item.completed {
@apply bg-green-50/50;
}
.progress-bar {
:deep(.el-progress-bar__outer) {
@apply rounded-full bg-gray-100;
}
:deep(.el-progress-bar__inner) {
@apply rounded-full transition-all duration-300;
}
}
@keyframes bounce {
0%,
100% {
transform: translateY(-10%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: translateY(0);
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
</style>