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

View file

@ -33,8 +33,8 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.GET("/download", baseApi.Download) fileRouter.GET("/download", baseApi.Download)
fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles) fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles)
fileRouter.POST("/size", baseApi.Size) fileRouter.POST("/size", baseApi.Size)
fileRouter.GET("/ws", baseApi.Ws) fileRouter.GET("/wget/process", baseApi.WgetProcess)
fileRouter.GET("/keys", baseApi.Keys) fileRouter.GET("/wget/process/keys", baseApi.ProcessKeys)
fileRouter.POST("/read", baseApi.ReadFileByLine) fileRouter.POST("/read", baseApi.ReadFileByLine)
fileRouter.POST("/batch/role", baseApi.BatchChangeModeAndOwner) 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); return http.post<File.DirSizeRes>('files/size', params, TimeoutEnum.T_5M);
}; };
export const fileKeys = () => { export const fileWgetKeys = () => {
return http.get<File.FileKeys>('files/keys'); return http.get<File.FileKeys>('files//wget/process/keys');
}; };
export const getRecycleList = (params: ReqPage) => { export const getRecycleList = (params: ReqPage) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,39 +1,68 @@
<template> <template>
<DialogPro v-model="open" :title="$t('file.downloadProcess')" size="small" @open="onOpen" @close="handleClose"> <DialogPro v-model="open" :title="$t('file.downloadProcess')" size="small" @close="handleClose">
<div v-for="(value, index) in res" :key="index"> <template #content>
<span>{{ value['percent'] === 100 ? $t('file.downloadSuccess') : $t('file.downloading') }}</span> <div class="space-y-4 p-4" :loading="loading">
<MsgInfo :info="value['name']" width="250" /> <div
<el-progress v-if="value['total'] == 0" :percentage="100" :indeterminate="true" :duration="1" /> v-for="(value, index) in res"
<el-progress v-else :text-inside="true" :stroke-width="15" :percentage="value['percent']"></el-progress> :key="index"
<span> class="bg-white rounded-lg p-4 shadow-sm border border-gray-100 transition-all duration-200 hover:shadow-md"
{{ getFileSize(value['written']) }}/ :class="{ completed: value.percent === 100 }"
<span v-if="value['total'] > 0">{{ getFileSize(value['total']) }}</span> >
</span> <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>
<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> </DialogPro>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { fileKeys } from '@/api/modules/files'; import { fileWgetKeys } from '@/api/modules/files';
import { computeSize } from '@/utils/util'; import { computeSize } from '@/utils/util';
import { onBeforeUnmount, ref, toRefs } from 'vue'; import { onBeforeUnmount, ref } from 'vue';
import MsgInfo from '@/components/msg-info/index.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; let processSocket = ref(null) as unknown as WebSocket;
const res = ref([]); const res = ref([]);
const keys = ref(['']); const keys = ref(['']);
const open = ref(false);
const loading = ref(false);
const em = defineEmits(['close']); const em = defineEmits(['close']);
const handleClose = () => { const handleClose = () => {
closeSocket(); closeSocket();
open.value = false;
em('close', open); em('close', open);
}; };
@ -58,7 +87,7 @@ const initProcess = () => {
let href = window.location.href; let href = window.location.href;
let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss'; let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
let ipLocal = href.split('//')[1].split('/')[0]; 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.onopen = onOpenProcess;
processSocket.onmessage = onMessage; processSocket.onmessage = onMessage;
processSocket.onerror = onerror; processSocket.onerror = onerror;
@ -66,15 +95,20 @@ const initProcess = () => {
sendMsg(); sendMsg();
}; };
const getKeys = () => { const getKeys = async () => {
keys.value = []; keys.value = [];
res.value = []; res.value = [];
fileKeys().then((res) => { loading.value = true;
if (res.data.keys.length > 0) { try {
const res = await fileWgetKeys();
if (res.data && res.data.keys.length > 0) {
keys.value = res.data.keys; keys.value = res.data.keys;
initProcess(); initProcess();
} }
}); } catch (error) {
} finally {
loading.value = false;
}
}; };
const sendMsg = () => { const sendMsg = () => {
@ -98,7 +132,38 @@ onBeforeUnmount(() => {
closeSocket(); closeSocket();
}); });
const onOpen = () => { const acceptParams = () => {
open.value = true;
getKeys(); getKeys();
}; };
defineExpose({ acceptParams });
</script> </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>