diff --git a/src/admin/VideosPage.tsx b/src/admin/VideosPage.tsx index 1b0db7a..2dac7d4 100644 --- a/src/admin/VideosPage.tsx +++ b/src/admin/VideosPage.tsx @@ -152,15 +152,16 @@ export function VideosPage() { if (ids.length === 0) return; setBatchDeleting(true); try { - const results = await Promise.allSettled( - ids.map((id) => api.deleteVideo(id)) - ); let success = 0; let deletedSources = 0; - for (const r of results) { - if (r.status !== "fulfilled") continue; - success++; - if (r.value.deletedSource) deletedSources++; + for (const id of ids) { + try { + const result = await api.deleteVideo(id); + success++; + if (result.deletedSource) deletedSources++; + } catch { + // Keep deleting the rest of the selected videos; report aggregate failure below. + } } const failed = ids.length - success; if (failed === 0) { diff --git a/src/admin/drive/Spider91UploadTargetField.tsx b/src/admin/drive/Spider91UploadTargetField.tsx index 6eba5c1..3557dc3 100644 --- a/src/admin/drive/Spider91UploadTargetField.tsx +++ b/src/admin/drive/Spider91UploadTargetField.tsx @@ -1,4 +1,5 @@ import { useId } from "react"; +import { ChevronDown } from "lucide-react"; import { kindLabel } from "./constants"; import * as api from "../api"; @@ -16,16 +17,21 @@ export function Spider91UploadTargetField({ return (
- -
- 选择本地保存时,爬取视频只保存在服务器本地;选择 115 网盘、123 云盘、PikPak 或 OneDrive 后,较早的视频会上传到该云盘根目录下的 91 Spider 文件夹。该设置全局生效。 +
+ +
); diff --git a/src/admin/drive/constants.ts b/src/admin/drive/constants.ts index 4050361..d648f4a 100644 --- a/src/admin/drive/constants.ts +++ b/src/admin/drive/constants.ts @@ -264,7 +264,7 @@ export function credentialFields(kind: Kind): Array<{ key: "proxy", label: "代理地址(可选)", placeholder: "http://127.0.0.1:7890", - help: "仅用于 91Spider 的列表/详情请求和视频、封面下载;留空则使用服务器环境变量 HTTP_PROXY / HTTPS_PROXY 或直连。支持 http://、https://、socks5:// 或 socks5h://。", + help: "支持 http://、https://、socks5://、socks5h://代理", }, ]; } diff --git a/src/styles/admin.css b/src/styles/admin.css index 0fa4e92..b5ecebf 100644 --- a/src/styles/admin.css +++ b/src/styles/admin.css @@ -404,6 +404,39 @@ color: var(--text-strong); } +.admin-form-select-wrap { + position: relative; + display: block; + width: 100%; +} + +.admin-form__row .admin-form-select { + appearance: none; + -webkit-appearance: none; + width: 100%; + min-height: 40px; + padding-right: 36px; + line-height: 1.2; + cursor: pointer; +} + +.admin-form__row .admin-form-select::-ms-expand { + display: none; +} + +.admin-form-select__icon { + position: absolute; + top: 50%; + right: 12px; + transform: translateY(-50%); + color: var(--text-faint); + pointer-events: none; +} + +.admin-form-select:focus + .admin-form-select__icon { + color: var(--accent); +} + .admin-form__row--inline { display: flex; gap: var(--space-2); diff --git a/tests/adminDriveForm.test.ts b/tests/adminDriveForm.test.ts index 5b1d78b..8952032 100644 --- a/tests/adminDriveForm.test.ts +++ b/tests/adminDriveForm.test.ts @@ -10,10 +10,18 @@ const driveComponentsSource = readFileSync( new URL("../src/admin/drive/DriveComponents.tsx", import.meta.url), "utf8" ); +const spider91UploadTargetSource = readFileSync( + new URL("../src/admin/drive/Spider91UploadTargetField.tsx", import.meta.url), + "utf8" +); const driveFormSource = readFileSync( new URL("../src/admin/drive/DriveForm.tsx", import.meta.url), "utf8" ); +const adminCss = readFileSync( + new URL("../src/styles/admin.css", import.meta.url), + "utf8" +); const apiSource = readFileSync( new URL("../src/admin/api.ts", import.meta.url), "utf8" @@ -23,10 +31,7 @@ const constantsSource = readFileSync( "utf8" ); -const combinedSource = drivesPageSource + "\n" + driveFormSource + "\n" + constantsSource + "\n" + readFileSync( - new URL("../src/admin/drive/Spider91UploadTargetField.tsx", import.meta.url), - "utf8" -); +const combinedSource = drivesPageSource + "\n" + driveFormSource + "\n" + constantsSource + "\n" + spider91UploadTargetSource; function driveTypeOptions() { const match = /const DRIVE_OPTIONS:\s*DriveOption\[]\s*=\s*\[([\s\S]*?)\];/.exec( @@ -49,7 +54,7 @@ function assertDriveTypeOption(value: string, label: string) { test("spider91 drive form does not expose advanced crawler credentials", () => { assert.match(combinedSource, /key: "proxy"/); assert.match(combinedSource, /label: "代理地址(可选)"/); - assert.match(combinedSource, /支持 http:\/\/、https:\/\/、socks5:\/\/ 或 socks5h:\/\//); + assert.match(combinedSource, /支持 http:\/\/、https:\/\/、socks5:\/\/、socks5h:\/\/代理/); assert.doesNotMatch(combinedSource, /target_new/); assert.doesNotMatch(combinedSource, /crawl_hour/); assert.doesNotMatch(combinedSource, /python_path/); @@ -64,6 +69,18 @@ test("spider91 upload target uses explicit local-save option instead of auto tar ); assert.doesNotMatch(combinedSource, /自动:唯一/); assert.doesNotMatch(combinedSource, /自动模式/); + assert.doesNotMatch(combinedSource, /较早的视频会上传到该云盘根目录下的 91 Spider 文件夹/); +}); + +test("spider91 upload target select uses an aligned custom arrow", () => { + assert.match(spider91UploadTargetSource, /className="admin-form-select-wrap"/); + assert.match(spider91UploadTargetSource, /className="admin-form-select"/); + assert.match(spider91UploadTargetSource, /className="admin-form-select__icon"/); + assert.match(adminCss, /\.admin-form__row \.admin-form-select\s*\{[^}]*appearance\s*:\s*none/s); + assert.match( + adminCss, + /\.admin-form-select__icon\s*\{[^}]*top\s*:\s*50%[^}]*right\s*:\s*12px[^}]*transform\s*:\s*translateY\(-50%\)/s + ); }); test("drive form hides root directory id for localstorage and spider91", () => { diff --git a/tests/adminVideosPagination.test.ts b/tests/adminVideosPagination.test.ts index 2cc0add..23bfdc5 100644 --- a/tests/adminVideosPagination.test.ts +++ b/tests/adminVideosPagination.test.ts @@ -11,3 +11,12 @@ test("admin videos page uses responsive page size", () => { assert.match(videosPageSource, /window\.matchMedia\(VIDEOS_MOBILE_QUERY\)/); assert.match(videosPageSource, /api\.listVideos\(\{ driveId, page, size: pageSize, keyword: searchKeyword \}\)/); }); + +test("admin videos batch delete runs deletions sequentially", () => { + assert.match(videosPageSource, /for \(const id of ids\) \{/); + assert.match(videosPageSource, /const result = await api\.deleteVideo\(id\);/); + assert.doesNotMatch( + videosPageSource, + /Promise\.allSettled\(\s*ids\.map\(\(id\) => api\.deleteVideo\(id\)\)\s*\)/ + ); +});