feat: add sticky progress bar for batch operations
批量下载、批量取消和批量删除操作添加固定在顶部的进度条。 使用 CSS sticky-top 确保进度条始终可见,无论滚动位置如何。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+57
-6
@@ -55,10 +55,34 @@ export class TaskRoute implements VanComponent {
|
|||||||
t.selected.val && !t.deleting.val && (t.statusState.val == 'waiting' || t.statusState.val == 'running')
|
t.selected.val && !t.deleting.val && (t.statusState.val == 'waiting' || t.statusState.val == 'running')
|
||||||
))
|
))
|
||||||
|
|
||||||
|
/** 批量操作进度 */
|
||||||
|
batchProgress = van.state(0)
|
||||||
|
batchTotal = van.state(0)
|
||||||
|
batchOperation = van.state<'cancel' | 'delete' | ''>('')
|
||||||
|
batchInProgress = van.derive(() => this.batchTotal.val > 0 && this.batchProgress.val < this.batchTotal.val)
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.element = this.Root()
|
this.element = this.Root()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 固定在顶部的进度条 */
|
||||||
|
BatchProgressBar() {
|
||||||
|
return div({
|
||||||
|
class: 'sticky-top bg-body p-3 border-bottom',
|
||||||
|
hidden: () => !this.batchInProgress.val,
|
||||||
|
style: 'z-index: 10;'
|
||||||
|
},
|
||||||
|
div({ class: 'text-center fs-5' }, () => {
|
||||||
|
const op = this.batchOperation.val == 'cancel' ? '取消' : '删除'
|
||||||
|
return `正在批量${op}任务 (${this.batchProgress.val}/${this.batchTotal.val})`
|
||||||
|
}),
|
||||||
|
div({ class: 'progress' }, div({
|
||||||
|
class: 'progress-bar progress-bar-striped progress-bar-animated bg-primary',
|
||||||
|
style: () => `width: ${this.batchProgress.val / this.batchTotal.val * 100}%`
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Root() {
|
Root() {
|
||||||
const _that = this
|
const _that = this
|
||||||
return Route({
|
return Route({
|
||||||
@@ -67,6 +91,8 @@ export class TaskRoute implements VanComponent {
|
|||||||
return div(
|
return div(
|
||||||
() => _that.loading.val ? LoadingBox() : '',
|
() => _that.loading.val ? LoadingBox() : '',
|
||||||
() => div({ class: 'vstack gap-3', hidden: _that.loading.val },
|
() => div({ class: 'vstack gap-3', hidden: _that.loading.val },
|
||||||
|
// 固定在顶部的批量操作进度条
|
||||||
|
_that.BatchProgressBar(),
|
||||||
// 批量操作工具栏
|
// 批量操作工具栏
|
||||||
div({ class: 'hstack gap-2', hidden: () => _that.taskList.val.length == 0 },
|
div({ class: 'hstack gap-2', hidden: () => _that.taskList.val.length == 0 },
|
||||||
input({
|
input({
|
||||||
@@ -84,29 +110,54 @@ export class TaskRoute implements VanComponent {
|
|||||||
button({
|
button({
|
||||||
class: 'btn btn-sm btn-outline-danger',
|
class: 'btn btn-sm btn-outline-danger',
|
||||||
hidden: () => _that.selectedCancellable.val.length == 0,
|
hidden: () => _that.selectedCancellable.val.length == 0,
|
||||||
|
disabled: () => _that.batchInProgress.val,
|
||||||
onclick() {
|
onclick() {
|
||||||
if (!confirm(`确定要取消选中的 ${_that.selectedCancellable.val.length} 个任务吗?`)) return
|
if (!confirm(`确定要取消选中的 ${_that.selectedCancellable.val.length} 个任务吗?`)) return
|
||||||
const ids = _that.selectedCancellable.val.map(t => t.id)
|
const tasks = [..._that.selectedCancellable.val]
|
||||||
cancelTasks(ids).then(() => {
|
_that.batchOperation.val = 'cancel'
|
||||||
_that.selectedCancellable.val.forEach(t => {
|
_that.batchTotal.val = tasks.length
|
||||||
|
_that.batchProgress.val = 0
|
||||||
|
|
||||||
|
let completed = 0
|
||||||
|
tasks.forEach(t => {
|
||||||
|
cancelTask(t.id).then(() => {
|
||||||
t.statusState.val = 'error'
|
t.statusState.val = 'error'
|
||||||
t.selected.val = false
|
t.selected.val = false
|
||||||
|
}).finally(() => {
|
||||||
|
completed++
|
||||||
|
_that.batchProgress.val = completed
|
||||||
|
if (completed >= tasks.length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
_that.batchTotal.val = 0
|
||||||
|
_that.batchProgress.val = 0
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}).catch(error => {
|
|
||||||
alert(error.message)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, () => `批量取消 (${_that.selectedCancellable.val.length})`),
|
}, () => `批量取消 (${_that.selectedCancellable.val.length})`),
|
||||||
button({
|
button({
|
||||||
class: 'btn btn-sm btn-outline-secondary',
|
class: 'btn btn-sm btn-outline-secondary',
|
||||||
hidden: () => _that.selectedDeletable.val.length == 0,
|
hidden: () => _that.selectedDeletable.val.length == 0,
|
||||||
|
disabled: () => _that.batchInProgress.val,
|
||||||
onclick() {
|
onclick() {
|
||||||
if (!confirm(`确定要删除选中的 ${_that.selectedDeletable.val.length} 个任务吗?`)) return
|
if (!confirm(`确定要删除选中的 ${_that.selectedDeletable.val.length} 个任务吗?`)) return
|
||||||
const ids = _that.selectedDeletable.val.map(t => t.id)
|
const tasks = [..._that.selectedDeletable.val]
|
||||||
|
_that.batchOperation.val = 'delete'
|
||||||
|
_that.batchTotal.val = tasks.length
|
||||||
|
_that.batchProgress.val = 0
|
||||||
|
|
||||||
|
const ids = tasks.map(t => t.id)
|
||||||
deleteTasks(ids).then(() => {
|
deleteTasks(ids).then(() => {
|
||||||
|
_that.batchProgress.val = tasks.length
|
||||||
_that.taskList.val = _that.taskList.val.filter(taskInDB => !ids.includes(taskInDB.id))
|
_that.taskList.val = _that.taskList.val.filter(taskInDB => !ids.includes(taskInDB.id))
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
alert(error.message)
|
alert(error.message)
|
||||||
|
}).finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
_that.batchTotal.val = 0
|
||||||
|
_that.batchProgress.val = 0
|
||||||
|
}, 500)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, () => `批量删除 (${_that.selectedDeletable.val.length})`),
|
}, () => `批量删除 (${_that.selectedDeletable.val.length})`),
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ export class ParseModalComp implements VanComponent {
|
|||||||
div({ class: `h5 modal-title` }, () => this.isDownloading.val ? '正在创建下载任务...' : (allFinish.val ? '批量下载' : '批量解析')),
|
div({ class: `h5 modal-title` }, () => this.isDownloading.val ? '正在创建下载任务...' : (allFinish.val ? '批量下载' : '批量解析')),
|
||||||
button({ class: `btn-close`, 'data-bs-dismiss': `modal` })
|
button({ class: `btn-close`, 'data-bs-dismiss': `modal` })
|
||||||
),
|
),
|
||||||
|
// 进度条固定在头部下方
|
||||||
|
this.StickyProgress(),
|
||||||
div({ class: `modal-body vstack gap-3`, tabIndex: -1, style: 'outline: none;' },
|
div({ class: `modal-body vstack gap-3`, tabIndex: -1, style: 'outline: none;' },
|
||||||
this.ParseProgress(),
|
|
||||||
this.DownloadProgress(),
|
|
||||||
div({ class: 'vstack gap-2', hidden: () => this.errorList.val.length == 0 || !allFinish.val || this.isDownloading.val },
|
div({ class: 'vstack gap-2', hidden: () => this.errorList.val.length == 0 || !allFinish.val || this.isDownloading.val },
|
||||||
div({ class: 'text-danger' }, () => `以下 ${this.errorList.val.length} 个视频解析失败`),
|
div({ class: 'text-danger' }, () => `以下 ${this.errorList.val.length} 个视频解析失败`),
|
||||||
() => div({ class: 'list-group' },
|
() => div({ class: 'list-group' },
|
||||||
@@ -218,6 +218,18 @@ export class ParseModalComp implements VanComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 固定在顶部的进度条 */
|
||||||
|
StickyProgress() {
|
||||||
|
return div({
|
||||||
|
class: 'sticky-top bg-body p-3 border-bottom',
|
||||||
|
hidden: () => this.totalCount.val == this.finishCount.val && !this.isDownloading.val,
|
||||||
|
style: 'z-index: 10;'
|
||||||
|
},
|
||||||
|
this.ParseProgress(),
|
||||||
|
this.DownloadProgress(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
DownloadProgress() {
|
DownloadProgress() {
|
||||||
return div({ class: 'vstack gap-3', hidden: () => !this.isDownloading.val },
|
return div({ class: 'vstack gap-3', hidden: () => !this.isDownloading.val },
|
||||||
div({ class: 'text-center fs-5' }, () => `正在创建下载任务 (${this.downloadProgress.val}/${this.downloadTotal.val})`),
|
div({ class: 'text-center fs-5' }, () => `正在创建下载任务 (${this.downloadProgress.val}/${this.downloadTotal.val})`),
|
||||||
|
|||||||
Reference in New Issue
Block a user