1- <script setup lang="ts"></script >
2-
31<template >
4- <div class =" d-flex flex-column h-screen" >
5- <div class =" d-flex ga-2 pa-2 overflow-x-auto" >
6- <v-dialog max-width =" 500" persistent >
7- <template #activator =" { props: activatorProps } " >
8- <v-btn prepend-icon =" mdi-plus" stacked v-bind =" activatorProps" >
9- 新建任务
10- </v-btn >
11- </template >
12- <template #default =" { isActive } " >
13- <v-card title =" 新建任务" >
14- <v-card-text >
15- <v-form v-model =" valid" @submit =" createTask(isActive)" >
16- <v-container >
17- <v-row >
18- <v-textarea
19- v-model =" rawUrls"
20- label =" URL (一行一个)"
21- :rules =" urlRules"
22- />
23- </v-row >
24- <v-row >
25- <v-text-field
26- v-model =" store.saveDir"
27- label =" 保存文件夹"
28- :rules =" dirRules"
29- >
30- <template #append >
31- <IconBtn
32- icon =" mdi-folder"
33- text =" 选择文件夹"
34- @click =" selectDir"
35- />
36- </template >
37- </v-text-field >
38- </v-row >
39- <v-row >
40- <v-number-input
41- v-model =" store.threads"
42- label =" 线程数量"
43- :min =" 1"
44- />
45- </v-row >
46- </v-container >
47- </v-form >
48- </v-card-text >
49- <v-card-actions >
50- <v-spacer />
51- <v-btn @click =" isActive.value = false" > 取消 </v-btn >
52- <v-btn @click =" createTask(isActive)" > 开始 </v-btn >
53- </v-card-actions >
54- </v-card >
55- </template >
56- </v-dialog >
57- <v-btn prepend-icon =" mdi-play" stacked > 全部开始 </v-btn >
58- <v-btn prepend-icon =" mdi-pause" stacked > 全部暂停 </v-btn >
59- <v-btn prepend-icon =" mdi-delete" stacked > 全部删除 </v-btn >
60- <v-btn prepend-icon =" mdi-cog" stacked > 设置 </v-btn >
61- </div >
62- <div ref =" scrollRef" class =" overflow-hidden" style =" flex : 1 " >
63- <v-virtual-scroll :height =" height" :items =" store.list" >
64- <template #default =" { item } " >
65- <v-card class =" download-item ma-2" >
66- <v-card-item >
67- <v-card-title > {{ item.fileName }} </v-card-title >
68- <v-card-subtitle > {{ item.filePath }} </v-card-subtitle >
69- </v-card-item >
70- <v-card-text >
71- <v-container class =" pa-0" >
72- <v-row >
73- <v-col >
74- <div >速度</div >
75- <div >{{ formatSize(item.speed) }}/s</div >
76- </v-col >
77- <v-col >
78- <div >用时</div >
79- <div >{{ formatTime(item.elapsedMs / 1000) }}</div >
80- </v-col >
81- <v-col >
82- <div >速度</div >
83- <div >10MB/s</div >
84- </v-col >
85- </v-row >
86- <v-row >
87- <v-col class =" py-0" >
88- <v-progress-linear
89- :max =" item.fileSize"
90- :model-value =" 1 * 1024 * 1024"
91- />
92- </v-col >
93- </v-row >
94- </v-container >
95- </v-card-text >
96- <v-card-actions >
97- <IconBtn
98- v-if =" ['downloading', 'pending'].includes(item.status!)"
99- icon =" mdi-pause"
100- text =" 暂停"
101- />
102- <IconBtn v-else icon =" mdi-play" text =" 开始" />
103- <IconBtn icon =" mdi-delete" text =" 删除" />
104- <IconBtn icon =" mdi-open-in-new" text =" 打开" />
105- <IconBtn icon =" mdi-folder" text =" 打开文件夹" />
106- </v-card-actions >
107- </v-card >
108- </template >
109- </v-virtual-scroll >
110- </div >
111- </div >
1122</template >
1133
114- <script lang="ts" setup>
115- import { invoke } from ' @tauri-apps/api/core'
116- import { open } from ' @tauri-apps/plugin-dialog'
117- import { useElementSize } from ' @vueuse/core'
118- import { useAppStore } from ' @/stores/app'
119- import { formatSize } from ' @/utils/format-size'
120- import { formatTime } from ' @/utils/format-time'
121-
122- const store = useAppStore ()
123- for (const e of store .list ) {
124- e .status = ' paused'
125- e .readProgress = []
126- e .readProgress .concat (e .writeProgress )
127- const downloaded = e .writeProgress .reduce ((a , b ) => a + b [1 ] - b [0 ], 0 )
128- e .speed = (downloaded / e .elapsedMs ) * 1000
129- }
130-
131- const scrollRef = useTemplateRef (' scrollRef' )
132- const { height } = useElementSize (scrollRef )
133-
134- const valid = ref (false )
135- const rawUrls = ref (' ' )
136- const urlRules = [
137- (value ? : string ) => {
138- if (! value ?.trim ()) return ' 请输入 URL'
139- const urls = value .split (' \n ' ).map (e => e .trim ())
140- for (const [i, item] of urls .entries ()) {
141- if (! item ) continue
142- try {
143- const url = new URL (item )
144- if (! [' http:' , ' https:' ].includes (url .protocol )) {
145- return ` 第 ${i + 1 } 行 URL 协议不正确 `
146- }
147- } catch (error ) {
148- console .error (error )
149- return ` 第 ${i + 1 } 行 URL 格式不正确 `
150- }
151- }
152- return true
153- },
154- ]
155- const dirRules = [
156- async (value ? : string ) => {
157- if (! value ?.trim ()) return ' 请选择一个保存目录'
158- try {
159- const res: string | null = await invoke (' format_dir' , { dir: value })
160- if (! res ) return ' 目录不存在'
161- console .log (res )
162- return true
163- } catch (error ) {
164- console .error (error )
165- return ' 目录格式不正确'
166- }
167- },
168- ]
169- async function selectDir() {
170- const dir = await open ({
171- directory: true ,
172- title: ' 选择保存文件夹' ,
173- })
174- if (dir ) store .saveDir = dir
175- }
176-
177- function createTask(isActive : Ref <boolean >) {
178- if (! valid .value ) {
179- return
180- }
181- isActive .value = false
182- const urls = rawUrls .value .split (' \n ' ).map (e => e .trim ())
183- rawUrls .value = ' '
184- const headers: Record <string , string > = {}
185- for (const [k, v] of store .headers
186- .split (' \n ' )
187- .map (e => e .trim ())
188- .filter (Boolean )
189- .map (e => e .split (' :' ).map (e => e .trim ()))) {
190- headers [k ] = v
191- }
192- for (const url of urls ) {
193- store .addEntry ({
194- url ,
195- headers ,
196- threads: store .threads ,
197- saveDir: store .saveDir ,
198- proxy: store .proxy ,
199- })
200- }
201- }
202- </script >
203-
204- <style >
205- .download-item :first-child {
206- margin-top : 0 !important ;
207- }
208- </style >
4+ <script >
5+ </script >
0 commit comments