mirror of
https://github.com/nianzhibai/91.git
synced 2026-06-15 08:45:41 +08:00
refactor: polish admin mobile management UI
This commit is contained in:
@@ -23,8 +23,10 @@ tools/
|
||||
|
||||
# 编译产物
|
||||
backend/server
|
||||
backend/server.*
|
||||
release/
|
||||
tsconfig.tsbuildinfo
|
||||
tmp/
|
||||
|
||||
# 91 爬虫脚本独立运行时的默认输出文件(backend 跑时会显式 --output 到 backend/data/spider91/,所以不会落在这里)
|
||||
91porn_videos.json
|
||||
|
||||
@@ -608,7 +608,10 @@ export function DrivesPage() {
|
||||
{storage && <StorageSummary storage={storage} />}
|
||||
|
||||
{loading ? (
|
||||
<div className="admin-empty">加载中...</div>
|
||||
<div className="admin-loading-state">
|
||||
<RefreshCw size={20} className="admin-spin" />
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
) : loadError ? (
|
||||
<div className="admin-error-state">
|
||||
<strong>网盘数据加载失败</strong>
|
||||
@@ -681,6 +684,7 @@ export function DrivesPage() {
|
||||
uploadTargets={uploadTargets}
|
||||
nameError={nameError}
|
||||
onNameBlur={() => setNameTouched(true)}
|
||||
onBack={() => setNameTouched(false)}
|
||||
/>
|
||||
</Modal>
|
||||
<DeleteDriveModal
|
||||
|
||||
@@ -8,11 +8,7 @@ export function RequireAuth({ children }: { children: ReactNode }) {
|
||||
const location = useLocation();
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="admin-loading-screen">
|
||||
加载中...
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status === "guest") {
|
||||
|
||||
@@ -360,7 +360,10 @@ export function TagsPage() {
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="admin-empty">加载中...</div>
|
||||
<div className="admin-loading-state">
|
||||
<RefreshCw size={20} className="admin-spin" />
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
) : loadError ? (
|
||||
<div className="admin-error-state">
|
||||
<strong>标签加载失败</strong>
|
||||
|
||||
@@ -53,6 +53,7 @@ export function VideosPage() {
|
||||
}, [driveId, page, searchKeyword]);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyword === searchKeyword) return;
|
||||
const timer = window.setTimeout(() => {
|
||||
setSearchKeyword(keyword);
|
||||
setPage(1);
|
||||
@@ -208,44 +209,10 @@ export function VideosPage() {
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<table className="admin-table is-selectable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="is-checkbox" style={{ width: '40px' }}><Square size={16} color="var(--border-default)" /></th>
|
||||
<th>标题</th>
|
||||
<th>作者</th>
|
||||
<th>标签</th>
|
||||
<th>时长</th>
|
||||
<th>Teaser</th>
|
||||
<th>来源</th>
|
||||
<th className="is-actions">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...Array(10)].map((_, i) => (
|
||||
<tr key={i}>
|
||||
<td className="is-checkbox"><Square size={16} color="var(--border-subtle)" /></td>
|
||||
<td>
|
||||
<div className="admin-skeleton-pulse" style={{ width: '60%', height: '14px', marginBottom: '6px', borderRadius: '4px' }}></div>
|
||||
<div className="admin-skeleton-pulse" style={{ width: '40%', height: '12px', borderRadius: '4px' }}></div>
|
||||
</td>
|
||||
<td><div className="admin-skeleton-pulse" style={{ width: '80%', height: '14px', borderRadius: '4px' }}></div></td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<div className="admin-skeleton-pulse" style={{ width: '40px', height: '22px', borderRadius: '12px' }}></div>
|
||||
<div className="admin-skeleton-pulse" style={{ width: '30px', height: '22px', borderRadius: '12px' }}></div>
|
||||
</div>
|
||||
</td>
|
||||
<td><div className="admin-skeleton-pulse" style={{ width: '40px', height: '14px', borderRadius: '4px' }}></div></td>
|
||||
<td><div className="admin-skeleton-pulse" style={{ width: '50px', height: '22px', borderRadius: '4px' }}></div></td>
|
||||
<td><div className="admin-skeleton-pulse" style={{ width: '60px', height: '14px', borderRadius: '4px' }}></div></td>
|
||||
<td className="is-actions">
|
||||
<div className="admin-skeleton-pulse" style={{ width: '60px', height: '28px', borderRadius: '4px', display: 'inline-block' }}></div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="admin-loading-state">
|
||||
<RefreshCw size={20} className="admin-spin" />
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
) : loadError ? (
|
||||
<div className="admin-error-state">
|
||||
<strong>视频加载失败</strong>
|
||||
|
||||
+122
-59
@@ -1,9 +1,35 @@
|
||||
import { useId, useMemo } from "react";
|
||||
import { useId, useMemo, useState } from "react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { P123QRCodeLogin } from "./P123QRCodeLogin";
|
||||
import { Spider91UploadTargetField } from "./Spider91UploadTargetField";
|
||||
import { FormState, Kind, credentialFields, credentialHelp, usesRootDirectoryID, rootIdPlaceholder } from "./constants";
|
||||
import {
|
||||
FormState,
|
||||
Kind,
|
||||
credentialFields,
|
||||
credentialHelp,
|
||||
usesRootDirectoryID,
|
||||
rootIdPlaceholder,
|
||||
} from "./constants";
|
||||
import * as api from "../api";
|
||||
|
||||
type DriveOption = {
|
||||
kind: Kind;
|
||||
label: string;
|
||||
abbr: string;
|
||||
};
|
||||
|
||||
const DRIVE_OPTIONS: DriveOption[] = [
|
||||
{ kind: "p115", label: "115 网盘", abbr: "115" },
|
||||
{ kind: "p123", label: "123 云盘", abbr: "123" },
|
||||
{ kind: "pikpak", label: "PikPak", abbr: "Pk" },
|
||||
{ kind: "onedrive", label: "OneDrive", abbr: "OD" },
|
||||
{ kind: "googledrive", label: "Google Drive", abbr: "GD" },
|
||||
{ kind: "localstorage", label: "本地存储", abbr: "Lo" },
|
||||
{ kind: "spider91", label: "91 爬虫", abbr: "91" },
|
||||
{ kind: "quark", label: "夸克网盘", abbr: "Qk" },
|
||||
{ kind: "wopan", label: "联通沃盘", abbr: "Wo" },
|
||||
];
|
||||
|
||||
export function DriveForm({
|
||||
form,
|
||||
onChange,
|
||||
@@ -11,6 +37,7 @@ export function DriveForm({
|
||||
uploadTargets,
|
||||
nameError,
|
||||
onNameBlur,
|
||||
onBack,
|
||||
}: {
|
||||
form: FormState;
|
||||
onChange: (f: FormState) => void;
|
||||
@@ -18,12 +45,13 @@ export function DriveForm({
|
||||
uploadTargets: api.AdminDrive[];
|
||||
nameError?: string;
|
||||
onNameBlur?: () => void;
|
||||
onBack?: () => void;
|
||||
}) {
|
||||
const idPrefix = useId();
|
||||
const fields = useMemo(() => credentialFields(form.kind), [form.kind]);
|
||||
const help = credentialHelp(form.kind, isEdit);
|
||||
const [step, setStep] = useState<"type" | "form">(isEdit ? "form" : "type");
|
||||
const nameId = `${idPrefix}-drive-name`;
|
||||
const kindId = `${idPrefix}-drive-kind`;
|
||||
const rootId = `${idPrefix}-drive-root`;
|
||||
|
||||
function set<K extends keyof FormState>(k: K, v: FormState[K]) {
|
||||
@@ -40,64 +68,98 @@ export function DriveForm({
|
||||
creds: {},
|
||||
});
|
||||
}
|
||||
function selectType(kind: Kind) {
|
||||
setKind(kind);
|
||||
setStep("form");
|
||||
}
|
||||
function goBack() {
|
||||
setStep("type");
|
||||
onChange({
|
||||
...form,
|
||||
name: "",
|
||||
rootId: "",
|
||||
creds: {},
|
||||
});
|
||||
onBack?.();
|
||||
}
|
||||
|
||||
const selectedOption = DRIVE_OPTIONS.find((o) => o.kind === form.kind);
|
||||
|
||||
if (step === "type" && !isEdit) {
|
||||
return (
|
||||
<div className="admin-drive-type-grid">
|
||||
{DRIVE_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.kind}
|
||||
type="button"
|
||||
className="admin-drive-type-card"
|
||||
onClick={() => selectType(opt.kind)}
|
||||
>
|
||||
<span className="admin-drive-type-card__icon">
|
||||
{opt.abbr}
|
||||
</span>
|
||||
<span className="admin-drive-type-card__label">{opt.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-form">
|
||||
<div className="admin-form__row">
|
||||
<label htmlFor={nameId}>名称 *</label>
|
||||
<input
|
||||
id={nameId}
|
||||
value={form.name}
|
||||
onChange={(e) => set("name", e.target.value)}
|
||||
onBlur={onNameBlur}
|
||||
placeholder="给这个盘起个名字"
|
||||
className={nameError ? "is-invalid" : undefined}
|
||||
aria-invalid={nameError ? "true" : undefined}
|
||||
aria-describedby={nameError ? `${nameId}-error` : undefined}
|
||||
/>
|
||||
{nameError && (
|
||||
<div className="admin-form__error" id={`${nameId}-error`}>
|
||||
{nameError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="admin-form__row">
|
||||
<label htmlFor={kindId}>类型</label>
|
||||
<select
|
||||
id={kindId}
|
||||
value={form.kind}
|
||||
onChange={(e) => setKind(e.target.value as Kind)}
|
||||
disabled={isEdit}
|
||||
>
|
||||
<option value="p115">115 网盘</option>
|
||||
<option value="p123">123 云盘</option>
|
||||
<option value="pikpak">PikPak</option>
|
||||
<option value="onedrive">OneDrive</option>
|
||||
<option value="googledrive">Google Drive</option>
|
||||
<option value="localstorage">本地存储</option>
|
||||
<option value="spider91">91 Spider</option>
|
||||
<option value="quark">夸克网盘</option>
|
||||
<option value="wopan">联通沃盘</option>
|
||||
</select>
|
||||
</div>
|
||||
{usesRootDirectoryID(form.kind) && (
|
||||
<div className="admin-form__row">
|
||||
<label htmlFor={rootId}>根目录 ID</label>
|
||||
<input
|
||||
id={rootId}
|
||||
value={form.rootId}
|
||||
onChange={(e) => set("rootId", e.target.value)}
|
||||
placeholder={rootIdPlaceholder(form.kind)}
|
||||
/>
|
||||
<div className="admin-form__help">
|
||||
留空时使用该网盘类型的默认根目录,具体目录ID获取方式请参考OpenList文档
|
||||
</div>
|
||||
{!isEdit && (
|
||||
<div className="admin-drive-step-header">
|
||||
<button type="button" className="admin-drive-step-back" onClick={goBack}>
|
||||
<ArrowLeft size={14} /> 重选类型
|
||||
</button>
|
||||
{selectedOption && (
|
||||
<span className="admin-drive-step-badge">
|
||||
<span className="admin-drive-step-badge__abbr">{selectedOption.abbr}</span>
|
||||
<span className="admin-drive-step-badge__label">{selectedOption.label}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="admin-form__section">
|
||||
<div className="admin-form__row">
|
||||
<label htmlFor={nameId}>名称 *</label>
|
||||
<input
|
||||
id={nameId}
|
||||
value={form.name}
|
||||
onChange={(e) => set("name", e.target.value)}
|
||||
onBlur={onNameBlur}
|
||||
placeholder="给这个盘起个名字"
|
||||
className={nameError ? "is-invalid" : undefined}
|
||||
aria-invalid={nameError ? "true" : undefined}
|
||||
aria-describedby={nameError ? `${nameId}-error` : undefined}
|
||||
/>
|
||||
{nameError && (
|
||||
<div className="admin-form__error" id={`${nameId}-error`}>
|
||||
{nameError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{usesRootDirectoryID(form.kind) && (
|
||||
<div className="admin-form__row">
|
||||
<label htmlFor={rootId}>根目录 ID</label>
|
||||
<input
|
||||
id={rootId}
|
||||
value={form.rootId}
|
||||
onChange={(e) => set("rootId", e.target.value)}
|
||||
placeholder={rootIdPlaceholder(form.kind)}
|
||||
/>
|
||||
<div className="admin-form__help">
|
||||
留空时使用该网盘类型的默认根目录
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(help || fields.length > 0) && (
|
||||
<>
|
||||
<hr className="admin-form__divider" />
|
||||
<div className="admin-form__section">
|
||||
<h3 className="admin-form__section-label">凭证配置</h3>
|
||||
|
||||
{help && (
|
||||
<div className="admin-form__help admin-form__help--lead">
|
||||
@@ -114,7 +176,8 @@ export function DriveForm({
|
||||
{fields.map((f) => (
|
||||
<div key={f.key} className="admin-form__row">
|
||||
<label htmlFor={`${idPrefix}-credential-${f.key}`}>
|
||||
{f.label}{f.required && " *"}
|
||||
{f.label}
|
||||
{f.required && " *"}
|
||||
</label>
|
||||
{f.multiline ? (
|
||||
<textarea
|
||||
@@ -135,18 +198,18 @@ export function DriveForm({
|
||||
{f.help && <div className="admin-form__help">{f.help}</div>}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{form.kind === "spider91" && (
|
||||
<>
|
||||
<hr className="admin-form__divider" />
|
||||
<div className="admin-form__section">
|
||||
<h3 className="admin-form__section-label">上传设置</h3>
|
||||
<Spider91UploadTargetField
|
||||
value={form.spider91UploadDriveId}
|
||||
onChange={(v) => set("spider91UploadDriveId", v)}
|
||||
uploadTargets={uploadTargets}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1143,6 +1143,9 @@
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.admin-table.admin-drives-table tr:hover td {
|
||||
@@ -1158,6 +1161,7 @@
|
||||
border-bottom: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.admin-table.admin-drives-table td::before {
|
||||
@@ -1398,6 +1402,14 @@
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: border-color var(--transition-fast), transform var(--transition-fast);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.admin-table:not(.admin-drives-table) tr.is-selected {
|
||||
background: var(--accent-soft);
|
||||
border-color: var(--border-accent);
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
@@ -1419,6 +1431,7 @@
|
||||
border-bottom: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.admin-table:not(.admin-drives-table) td::before {
|
||||
@@ -1538,6 +1551,17 @@
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.admin-loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-8);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-md);
|
||||
}
|
||||
|
||||
.admin-videos-filter {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -1649,6 +1673,150 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Drive Type Picker (新建网盘 - 类型选择卡片)
|
||||
* ========================================================= */
|
||||
.admin-drive-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.admin-drive-type-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: var(--space-4) var(--space-3);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-default);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: border-color var(--transition-fast),
|
||||
background var(--transition-fast),
|
||||
box-shadow var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.admin-drive-type-card:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--bg-elevated);
|
||||
box-shadow: 0 4px 16px var(--accent-glow);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.admin-drive-type-card:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.admin-drive-type-card:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.admin-drive-type-card__icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: 14px;
|
||||
font-weight: var(--weight-bold);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.admin-drive-type-card__label {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--weight-semibold);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.admin-drive-type-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.admin-drive-step-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--weight-medium);
|
||||
transition: color var(--transition-fast), background var(--transition-fast);
|
||||
}
|
||||
|
||||
.admin-drive-step-back:hover {
|
||||
color: var(--text-strong);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
:root[data-theme="pink"] .admin-drive-step-back:hover {
|
||||
background: rgba(120, 50, 80, 0.06);
|
||||
}
|
||||
|
||||
.admin-drive-step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.admin-drive-step-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid var(--border-accent);
|
||||
}
|
||||
|
||||
.admin-drive-step-badge__abbr {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent);
|
||||
color: var(--text-on-accent);
|
||||
font-size: 11px;
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
|
||||
.admin-drive-step-badge__label {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--weight-semibold);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.admin-form__section {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.admin-form__section + .admin-form__section {
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.admin-form__section-label {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--weight-semibold);
|
||||
color: var(--text-strong);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Theme Page (外观)
|
||||
*
|
||||
@@ -2071,6 +2239,9 @@
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
position: relative;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.admin-drive-card:hover,
|
||||
@@ -2497,6 +2668,9 @@
|
||||
gap: var(--space-3);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
box-shadow: var(--shadow-sm);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.admin-tag-card:hover {
|
||||
|
||||
@@ -20,6 +20,24 @@ const combinedSource = drivesPageSource + "\n" + driveFormSource + "\n" + consta
|
||||
"utf8"
|
||||
);
|
||||
|
||||
function driveTypeOptions() {
|
||||
const match = /const DRIVE_OPTIONS:\s*DriveOption\[]\s*=\s*\[([\s\S]*?)\];/.exec(
|
||||
driveFormSource
|
||||
);
|
||||
assert.ok(match, "drive option card list should be present");
|
||||
return Array.from(
|
||||
match[1].matchAll(/\{\s*kind:\s*"([^"]+)",\s*label:\s*"([^"]+)"/g),
|
||||
(option) => ({ value: option[1], label: option[2] })
|
||||
);
|
||||
}
|
||||
|
||||
function assertDriveTypeOption(value: string, label: string) {
|
||||
assert.ok(
|
||||
driveTypeOptions().some((option) => option.value === value && option.label === label),
|
||||
`${value} drive type option should be present`
|
||||
);
|
||||
}
|
||||
|
||||
test("spider91 drive form does not expose advanced crawler credentials", () => {
|
||||
assert.match(combinedSource, /key: "proxy"/);
|
||||
assert.match(combinedSource, /label: "代理地址(可选)"/);
|
||||
@@ -70,7 +88,7 @@ test("onedrive drive form only exposes required default-app fields", () => {
|
||||
});
|
||||
|
||||
test("googledrive drive form only exposes refresh token", () => {
|
||||
assert.match(combinedSource, /<option value="googledrive">Google Drive<\/option>/);
|
||||
assertDriveTypeOption("googledrive", "Google Drive");
|
||||
|
||||
const match =
|
||||
/case "googledrive":\s*return \[([\s\S]*?)\];\s*case "localstorage":/.exec(
|
||||
@@ -104,7 +122,7 @@ test("pikpak drive form only exposes account login fields", () => {
|
||||
});
|
||||
|
||||
test("localstorage drive form asks for a server directory path", () => {
|
||||
assert.match(combinedSource, /<option value="localstorage">本地存储<\/option>/);
|
||||
assertDriveTypeOption("localstorage", "本地存储");
|
||||
|
||||
const match =
|
||||
/case "localstorage":\s*return \[([\s\S]*?)\];\s*case "spider91":/.exec(
|
||||
@@ -120,20 +138,14 @@ test("localstorage drive form asks for a server directory path", () => {
|
||||
});
|
||||
|
||||
test("drive type selector keeps primary source order", () => {
|
||||
const options = Array.from(
|
||||
combinedSource.matchAll(/<option value="([^"]+)">([^<]+)<\/option>/g),
|
||||
(match) => ({ value: match[1], label: match[2] })
|
||||
);
|
||||
const driveOptions = options.slice(0, 9);
|
||||
|
||||
assert.deepEqual(driveOptions, [
|
||||
assert.deepEqual(driveTypeOptions(), [
|
||||
{ value: "p115", label: "115 网盘" },
|
||||
{ value: "p123", label: "123 云盘" },
|
||||
{ value: "pikpak", label: "PikPak" },
|
||||
{ value: "onedrive", label: "OneDrive" },
|
||||
{ value: "googledrive", label: "Google Drive" },
|
||||
{ value: "localstorage", label: "本地存储" },
|
||||
{ value: "spider91", label: "91 Spider" },
|
||||
{ value: "spider91", label: "91 爬虫" },
|
||||
{ value: "quark", label: "夸克网盘" },
|
||||
{ value: "wopan", label: "联通沃盘" },
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user