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*\)/
+ );
+});