From 0ac6570da9c6ce3b6af2e870290eaace88f68376 Mon Sep 17 00:00:00 2001 From: yw1573 Date: Thu, 9 Apr 2026 17:14:12 +0800 Subject: [PATCH] feat: add sticky progress bar for batch operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 批量下载、批量取消和批量删除操作添加固定在顶部的进度条。 使用 CSS sticky-top 确保进度条始终可见,无论滚动位置如何。 Co-Authored-By: Claude Opus 4.6 --- web/src/task/index.ts | 63 +++++++++++++++++++++++++++++---- web/src/work/view/parseModal.ts | 16 +++++++-- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/web/src/task/index.ts b/web/src/task/index.ts index 38c33a4..b0fdaa6 100644 --- a/web/src/task/index.ts +++ b/web/src/task/index.ts @@ -55,10 +55,34 @@ export class TaskRoute implements VanComponent { 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() { 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() { const _that = this return Route({ @@ -67,6 +91,8 @@ export class TaskRoute implements VanComponent { return div( () => _that.loading.val ? LoadingBox() : '', () => div({ class: 'vstack gap-3', hidden: _that.loading.val }, + // 固定在顶部的批量操作进度条 + _that.BatchProgressBar(), // 批量操作工具栏 div({ class: 'hstack gap-2', hidden: () => _that.taskList.val.length == 0 }, input({ @@ -84,29 +110,54 @@ export class TaskRoute implements VanComponent { button({ class: 'btn btn-sm btn-outline-danger', hidden: () => _that.selectedCancellable.val.length == 0, + disabled: () => _that.batchInProgress.val, onclick() { if (!confirm(`确定要取消选中的 ${_that.selectedCancellable.val.length} 个任务吗?`)) return - const ids = _that.selectedCancellable.val.map(t => t.id) - cancelTasks(ids).then(() => { - _that.selectedCancellable.val.forEach(t => { + const tasks = [..._that.selectedCancellable.val] + _that.batchOperation.val = 'cancel' + _that.batchTotal.val = tasks.length + _that.batchProgress.val = 0 + + let completed = 0 + tasks.forEach(t => { + cancelTask(t.id).then(() => { t.statusState.val = 'error' 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})`), button({ class: 'btn btn-sm btn-outline-secondary', hidden: () => _that.selectedDeletable.val.length == 0, + disabled: () => _that.batchInProgress.val, onclick() { 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(() => { + _that.batchProgress.val = tasks.length _that.taskList.val = _that.taskList.val.filter(taskInDB => !ids.includes(taskInDB.id)) }).catch(error => { alert(error.message) + }).finally(() => { + setTimeout(() => { + _that.batchTotal.val = 0 + _that.batchProgress.val = 0 + }, 500) }) } }, () => `批量删除 (${_that.selectedDeletable.val.length})`), diff --git a/web/src/work/view/parseModal.ts b/web/src/work/view/parseModal.ts index 0f20155..608d799 100644 --- a/web/src/work/view/parseModal.ts +++ b/web/src/work/view/parseModal.ts @@ -77,9 +77,9 @@ export class ParseModalComp implements VanComponent { div({ class: `h5 modal-title` }, () => this.isDownloading.val ? '正在创建下载任务...' : (allFinish.val ? '批量下载' : '批量解析')), button({ class: `btn-close`, 'data-bs-dismiss': `modal` }) ), + // 进度条固定在头部下方 + this.StickyProgress(), 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: 'text-danger' }, () => `以下 ${this.errorList.val.length} 个视频解析失败`), () => 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() { return div({ class: 'vstack gap-3', hidden: () => !this.isDownloading.val }, div({ class: 'text-center fs-5' }, () => `正在创建下载任务 (${this.downloadProgress.val}/${this.downloadTotal.val})`),