From 9b9f07dea53579545a1444cc8305a19aa7603933 Mon Sep 17 00:00:00 2001 From: owen Date: Mon, 21 Sep 2026 23:15:49 +0800 Subject: [PATCH] w --- .../DmsFileService.auto.cs | 24 ++-- .../FolderProfile3.pubxml.user | 2 +- .../CloudBuilder.Web.Entry/appsettings.json | 5 +- WebVue/src/md/fileUpload.md | 110 +++++++++++++++--- WebVue/src/views/master/DmsFileEnquiry.vue | 60 +++++++++- 5 files changed, 167 insertions(+), 34 deletions(-) diff --git a/CloudBuilder/CloudBuilder.Master.Service/DmsFileService.auto.cs b/CloudBuilder/CloudBuilder.Master.Service/DmsFileService.auto.cs index a0623b0..d7a738c 100644 --- a/CloudBuilder/CloudBuilder.Master.Service/DmsFileService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master.Service/DmsFileService.auto.cs @@ -1360,7 +1360,8 @@ namespace CloudBuilder.Master.Service if (File.Exists(fileName)) File.Delete(fileName); } string m3u8FilePath = fullName.Replace(fileExtension, FileTypePolicy.JPG); - //生成m3u8(暂不生成,只生成截图) + //生成m3u8(生成10秒视频,只生成截图) + dmsFile.TranscodeIndc = YesNoPolicy.YES; TsToM3u8(dmsFile.OrderKey, fullName, m3u8FilePath); } } @@ -1430,22 +1431,19 @@ namespace CloudBuilder.Master.Service Directory.CreateDirectory(targetDir); } - //return; para = string.Format(service.Configuration["Ffmpeg:Arguments"], mp4Url, fullName); - //不直接转码视频文件,影响服务器性能,改为在闭时自动生成 - //Task.Run(() => - //{ - // Thread.Sleep(200); - // RunMyProcess(service.Configuration["Ffmpeg:Path"], para); - //}); + IRepository logRepository = repository.GetRepository>(); SecurityScheduleTaskQueueEntity task = new SecurityScheduleTaskQueueEntity(); task.DocumentId = documentId; task.TaskId = service.Configuration["Ffmpeg:Path"]; task.TaskParam = para; - + logRepository.InsertNow(task); + + //直接转码视频文件10秒,影响服务器性能,改为在闭时自动生成全部 + RunMyProcess(task.TaskId, task.TaskParam); } public void ImageToScare(string fullName, DmsFileEntity dmsFile) @@ -1517,16 +1515,16 @@ namespace CloudBuilder.Master.Service // 必须等 ffmpeg 真正退出:视频封面(jpg)要在接口返回前落盘, // 否则前端刷新列表时文件还没生成, 请求 404 且不会自动重试, // 表现为"上传完看不到封面,手动刷新才有"。 - // 最多等 15 秒,超时就交给前端的封面重试机制兜底,不再死等拖住上传请求。 + // 最多等 25 秒,超时就交给前端的封面重试机制兜底,不再死等拖住上传请求。 // CliWrap 的 ExecuteAsync 返回 CommandTask,取它的 Task 才能等待 var task = Cli.Wrap(exePath) .WithArguments(Parameters) .ExecuteAsync() .Task; - if (!task.Wait(TimeSpan.FromSeconds(15))) + if (!task.Wait(TimeSpan.FromSeconds(25))) { - log.WriteToFile("ffmpeg 执行超时(15s):" + Parameters); + log.WriteToFile("ffmpeg 执行超时(25s):" + Parameters); return; } @@ -1576,7 +1574,7 @@ namespace CloudBuilder.Master.Service entity.IsDirectoryIndc = YesNoPolicy.NO; entity.FilePath = folderNameParent; - + entity.TranscodeIndc = dmsFile.TranscodeIndc; entity.OwerId = service.UserId; entity.IsLinkIndc = YesNoPolicy.NO; diff --git a/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user b/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user index e7eedb3..eab1af1 100644 --- a/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user +++ b/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user @@ -3,7 +3,7 @@ <_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2 - True|2026-09-21T13:01:22.8888913Z||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||; + True|2026-09-21T14:40:07.1232044Z||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||; \ No newline at end of file diff --git a/CloudBuilder/CloudBuilder.Web.Entry/appsettings.json b/CloudBuilder/CloudBuilder.Web.Entry/appsettings.json index 35aee63..131c224 100644 --- a/CloudBuilder/CloudBuilder.Web.Entry/appsettings.json +++ b/CloudBuilder/CloudBuilder.Web.Entry/appsettings.json @@ -61,7 +61,8 @@ "ScaleFileDirectory": "D:\\Net8\\FileServer\\Scale", "RequestPath": "/FileServer", //如果是IIS反向代理来的,那么要在本地给这个文件添加IUSR,IIS_IUSRS权限 "DmsFileDirectory": "DmsFile", - "ExcelOutputDirectory": "D:\\Net8\\FileServer\\Output" + "ExcelOutputDirectory": "D:\\Net8\\FileServer\\Output", + "MaxFileSize": 1000//M }, "ScheduleTask": { "Ffmpeg": "RunTask" @@ -69,7 +70,7 @@ "Ffmpeg": { "Path": "D:\\NET8\\Exe\\ffmpeg\\bin\\ffmpeg.exe", //视频转码 "Pictrue": " -i {0} -ss 00:00:2.000 -frames:v 1 -vf \"scale=1280:-2\" -q:v 4 {1}", //" -i {0} -ss 00:00:2.000 -frames:v 1 {1}", - "Arguments": "-i {0} -c:v libx264 -preset veryfast -crf 26 -profile:v main -level 3.1 -vf \"scale='min(1280,iw)':-2\" -maxrate 1500k -bufsize 2400k -c:a aac -b:a 96k -ac 2 -ar 44100 -start_number 0 -hls_time 3 -hls_list_size 0 -hls_flags independent_segments -f hls {1}" //0-源路径,1-目标路径 + "Arguments": "-i {0} -t 10 -c:v libx264 -preset veryfast -crf 26 -profile:v main -level 3.1 -vf \"scale='min(1280,iw)':-2\" -maxrate 1500k -bufsize 2400k -c:a aac -b:a 96k -ac 2 -ar 44100 -start_number 0 -hls_time 3 -hls_list_size 0 -hls_flags independent_segments -f hls {1}" //0-源路径,1-目标路径 }, "CloudBuilder.Topshelf": { "Path": "D:\\MAFAI\\CloudBuilder.Topshelf\\bin\\Debug\\net8.0\\CloudBuilder.Topshelf.exe" //图片压缩 diff --git a/WebVue/src/md/fileUpload.md b/WebVue/src/md/fileUpload.md index f665e74..0cf7d13 100644 --- a/WebVue/src/md/fileUpload.md +++ b/WebVue/src/md/fileUpload.md @@ -5,6 +5,7 @@ - 组件位置:`src/components/basic/FileUpload.vue` - 适用场景:DMS 文件上传、头像上传、任意需要分片上传的场景 - 上传方式:基于 `axios`,把文件按 `chunkSize` 切成 `Blob` 分片后**顺序**(串行)上传 +- 容错能力:单片失败自动重试(1x → 2x → 4x 退避);重试耗尽保留断点,可由外部决定**续传**或放弃 --- @@ -25,12 +26,14 @@ import FileUpload from "@/components/basic/FileUpload.vue"; | `resolveFilePath` | `() => string \| Promise` | - | 动态解析文件夹路径,**优先级高于 `filePath`**,在每次开始上传时调用(适合路径会随界面变化的情况) | | `headers` | `Record` | `{}` | 额外请求头。注意:组件内部会强制覆盖 `Authorization`、`Content-Type`、`FilePath` 三个键 | | `accept` | `string` | `"*"` | 原生 input 的 accept,例如 `image/*`、`.png,.jpg` | -| `multiple` | `boolean` | `false` | 是否支持多选(多选时每个文件会独立触发一轮上传流程) | +| `multiple` | `boolean` | `false` | 是否支持多选。多选的文件会**攒成一批**进同一个上传会话,逐个串行上传,整批结束只触发一次 `complete` | | `disabled` | `boolean` | `false` | 是否禁用。为 `true` 时 `trigger()` 不会打开选择框 | | `showButton` | `boolean` | `false` | 是否渲染内置的 `a-button` 作为触发元素 | | `buttonText` | `string` | `"上传文件"` | 内置按钮的文案 | | `chunkSize` | `number` | `1024 * 1024`(1MB) | 分片大小(字节)。小于等于 0 时按 1MB 处理 | | `timeout` | `number` | `60000` | 单个分片请求的超时时间(毫秒) | +| `retryCount` | `number` | `3` | 单片请求失败后的自动重试次数,`0` 表示不重试 | +| `retryDelay` | `number` | `1000` | 重试的初始等待毫秒数,之后按 1x → 2x → 4x 递增 | --- @@ -39,14 +42,18 @@ import FileUpload from "@/components/basic/FileUpload.vue"; | 事件 | 参数 | 触发时机 | | --- | --- | --- | | `start` | `file: File` | 开始上传(已通过校验、解析完路径之前) | -| `progress` | `{ percent, current, total, text }` | **每个分片上传成功后**触发一次。`percent` 为 0-100 整数,`current` 从 1 开始 | +| `progress` | `{ percent, current, total, text }` | **每个分片上传成功后**触发一次;网络重试期间也会触发一次(进度保持不变,不回退)。`percent` 为 0-100 整数,`current` 从 1 开始 | | `success` | `{ file, path, total, fileName, response }` | 全部分片上传成功。`fileName` 为**最后一个分片**的文件名(`uid-total-total.扩展名`),`response` 为**最后一个分片**的响应体数据 | -| `error` | `error: unknown` | 任意分片请求失败、或文件无法读取时触发 | +| `complete` | `{ count: number }` | **一批文件全部传完**(多选时只触发一次)。用它统一刷新列表,避免每个文件都刷一遍 | +| `interrupted` | `{ fileName, uploaded, total }` | 某片重试耗尽仍失败:断点已保留在内存,由外部决定续传还是放弃 | +| `error` | `error: unknown` | 任意分片请求失败、或文件无法读取时触发(与 `interrupted` 同时触发) | -进度数据示例: +进度数据示例(`text` 一律用百分比,不再显示「第几片/共几片」): ```ts -{ percent: 66, current: 2, total: 3, text: "上传中(2/3)..." } +{ percent: 66, current: 2, total: 3, text: "上传中 66%" } +// 多文件:上传中(第 2/3 个) 66% +// 重试中:网络异常,正在重试(1/3)... 66% ``` 成功数据示例: @@ -67,14 +74,25 @@ fileUploadRef.value?.trigger(); // 直接把一个 File 对象交给组件上传(例如拖拽、粘贴得到的 File) await fileUploadRef.value?.upload(file); + +// 中断后从断点继续(配合 interrupted 事件使用) +await fileUploadRef.value?.resume(); + +// 放弃这次上传,清掉断点 +fileUploadRef.value?.discard(); ``` | 方法 | 说明 | | --- | --- | | `trigger()` | 触发原生文件选择。`disabled === true` 或正在上传时会被忽略 | | `upload(file: File)` | 直接对指定 `File` 执行完整上传流程(不校验 `disabled`) | +| `resume()` | 从断点继续上传(沿用原来的分片与 `uid`,保证和服务器上的分片对得上) | +| `discard()` | 放弃本次上传,清空断点状态 | +| `hasPendingUpload` | `computed`,是否存在待续传的任务 | > 说明:`trigger()` 内部通过 `uploadRef.value.$el.querySelector("input").click()` 实现,因此即使组件整体不可见也能触发。 +> +> 断点只存在内存里:只要不刷新页面,文件、分片和 `uid` 都还在,续传能接着发;**刷新页面就丢了**(浏览器限制,`File` 无法持久化)。 --- @@ -108,6 +126,8 @@ await fileUploadRef.value?.upload(file); 4. **上传顺序**:`index` 从 1 递归到 `total`,**上一片成功后才发下一片**。 5. **并发控制**:组件内部有 `uploading` 标记,同一时间只允许一个上传流程;上传中再次选择文件会被忽略。 6. **文件列表**:使用 `v-model:file-list` 内部维护,不展示列表(`show-upload-list=false`),每次上传结束后自动清空。 +7. **进度口径**:`percent` 按**字节**算的整体进度:`(已完成文件累计字节 + 当前文件已传字节) / 本次所有文件总字节`。多文件时进度一路向前,不会每个文件退回 0;比按分片数算更准(最后一片通常不足 1MB)。`text` 统一输出百分比,单文件 `上传中 66%`、多文件 `上传中(第 2/3 个) 66%`。 +8. **失败重试**:单片失败自动重试 `retryCount` 次,等待 `retryDelay × 1/2/4`;重试期间进度保持在上一次的值,不回退。全部重试都失败才 `interrupted`。 --- @@ -126,9 +146,12 @@ await fileUploadRef.value?.upload(file); :action="uploadUrl" :headers="headers" :resolve-file-path="resolveUploadFilePath" + multiple @start="onUploadStart" @progress="onUploadProgress" @success="onUploadSuccess" + @complete="onUploadComplete" + @interrupted="onUploadInterrupted" @error="onUploadError" /> @@ -136,6 +159,7 @@ await fileUploadRef.value?.upload(file); @@ -255,5 +334,8 @@ await fileUploadRef.value?.upload(file); 2. **`FilePath` 为空**:表示上传到 DMS 根目录。如果后端要求必须有目录,请通过 `file-path` 或 `resolve-file-path` 显式指定。 3. **`headers` 覆盖规则**:`Authorization`、`Content-Type`、`FilePath` 会被组件强制覆盖,不要通过 `headers` 传这三个键。 4. **`disabled` 的作用范围**:只拦截用户交互(`trigger()`),不拦截 `upload(file)`;如需完全禁止,请在调用前自行判断。 -5. **分片合并延时**:DMS 在收到最后一个分片后需要时间合并,建议 `success` 后延迟刷新列表(见示例 1)。 -6. **多选**:`multiple` 为 `true` 时,所选文件会逐个进入上传流程,但受 `uploading` 标记限制,实际仍是一个接一个串行执行。 +5. **分片合并延时**:DMS 在收到最后一个分片后需要时间合并,用 `complete` 而不是 `success` 统一延迟刷新列表;文件越多等得越久(见示例 1)。 +6. **多选**:`multiple` 为 `true` 时,所选文件会逐个进入上传流程,但受 `uploading` 标记限制,实际仍是一个接一个串行执行;`complete` 只在整批结束时触发一次。 +7. **视频上传会卡在 99%/100%**:这是正常的——服务端在最后一个分片里同步执行 ffmpeg 生成封面(上限 25 秒)才返回。此时应显示"视频转码中 x%"之类的提示,不要让用户以为卡死(见示例 1)。 +8. **视频封面可能晚于列表出现**:ffmpeg 是异步落盘的,首刷时 jpg 可能还没生成,`` 会 404 且不会自动重试。处理办法:① 上传后补刷列表(每次查询服务端会重新拼 `?v=时间戳`,`src` 变了才会重新请求);② 列表缩略图挂 `@error="retryImage($event)"`(`src/utils/imageRetry.ts`,按去掉查询参数的地址累计,**最多重试 3 次**,用完就隐藏图片露出占位图标,避免反复闪)。 +9. **超时时间要留够**:视频场景最后一个分片请求里包含 ffmpeg 执行时间,`timeout` 默认 60s 够用;如果调小,会出现"分片超时 → 触发中断弹窗",但服务端其实已经处理完了。 diff --git a/WebVue/src/views/master/DmsFileEnquiry.vue b/WebVue/src/views/master/DmsFileEnquiry.vue index 0628753..437e413 100644 --- a/WebVue/src/views/master/DmsFileEnquiry.vue +++ b/WebVue/src/views/master/DmsFileEnquiry.vue @@ -201,6 +201,7 @@ const toUploadView = (r: Record): DmsFileUploadViewDisplayData => onUnmounted(() => { sound.value?.stop(); clearRefreshTimers(); + stopTranscodeTip(); }); const play = (src: string) => { @@ -298,16 +299,58 @@ const resolveUploadFilePath = () => { return bindOption?.Value || currFilePath.value; }; -const onUploadStart = () => { - loading.value = true; +/** 本次上传的是否是视频:只有视频才需要转码/截图 */ +const uploadingVideo = ref(false); + +/** + * "视频转码中 x%" 提示。 + * + * 最后一片发出去后,服务端要先 ffmpeg 截封面(要解码一段视频,最多 25 秒)才返回, + * 这段时间前端进度停在 99%/100% 一动不动,看起来像卡死。 + * 这里用一个按时间推进的估算进度(与后端 25 秒上限对齐,最高 99%)告诉用户还在处理。 + */ +const TRANSCODE_MAX_MS = 25000; +let transcodeTimer = 0; + +const startTranscodeTip = () => { + if (transcodeTimer) return; + const startAt = Date.now(); + spinTip.value = "预览10秒视频转码中 0%"; + transcodeTimer = window.setInterval(() => { + const ratio = Math.min(0.99, (Date.now() - startAt) / TRANSCODE_MAX_MS); + spinTip.value = `预览10秒视频转码中 ${Math.round(ratio * 100)}%`; + }, 300); }; -const onUploadProgress = (payload: { text?: string }) => { +const stopTranscodeTip = () => { + if (!transcodeTimer) return; + clearInterval(transcodeTimer); + transcodeTimer = 0; +}; + +const onUploadStart = (file: File) => { + loading.value = true; + uploadingVideo.value = !!file && fileTypePolicy.isVideo(file?.name || ""); +}; + +const onUploadProgress = (payload: { text?: string; percent?: number }) => { + // 99%/100% 之后就是服务端转码阶段,换成转码提示,别让用户以为卡住了 + if (uploadingVideo.value && (payload?.percent ?? 0) >= 99) { + startTranscodeTip(); + return; + } + // 进度回到低处说明开始传下一个文件了,转码提示到此为止 + stopTranscodeTip(); spinTip.value = payload?.text || "上传中..."; }; /** 单个文件传完:这里不刷新,等一批都传完再统一刷,避免选了多个文件反复刷列表 */ const onUploadSuccess = () => { + // 字节已传完,服务端还在合并分片 + ffmpeg 截图/转码 + if (uploadingVideo.value) { + startTranscodeTip(); + return; + } spinTip.value = "上传完成,等待服务端处理..."; }; @@ -326,11 +369,15 @@ const clearRefreshTimers = () => { const onUploadComplete = (payload: { count?: number }) => { const count = payload?.count ?? 1; const sec = 2500 + (count - 1) * 1000; - spinTip.value = "加载中..."; + // 视频场景由转码提示接管文案,等首刷完成后再切回"加载中..." + if (!uploadingVideo.value) spinTip.value = "加载中..."; clearRefreshTimers(); refreshTimers.push( window.setTimeout(() => { + // 列表已经刷出来了,转码提示到此为止(后续补刷沿用默认的"加载中...") + stopTranscodeTip(); + uploadingVideo.value = false; onReSearch(); loading.value = false; }, sec), @@ -359,6 +406,7 @@ const onUploadInterrupted = (payload: { uploaded: payload?.uploaded ?? 0, total: payload?.total ?? 0, }; + stopTranscodeTip(); loading.value = false; spinTip.value = "加载中..."; showResumeModal.value = true; @@ -376,10 +424,14 @@ const onResumeUpload = () => { const onDiscardUpload = () => { showResumeModal.value = false; operatorStationRef.value?.discardUpload(); + stopTranscodeTip(); + uploadingVideo.value = false; loading.value = false; }; const onUploadError = () => { + stopTranscodeTip(); + uploadingVideo.value = false; loading.value = false; spinTip.value = "加载中..."; };