mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 03:56:38 +08:00
Revert "feat(cloud): 115/夸克网盘 providers with cookie + QR login and 302 playback"
This reverts commit c6455103e6.
This commit is contained in:
@@ -1,26 +1,6 @@
|
||||
import { api } from './client'
|
||||
|
||||
export type StorageType = 'alist' | 's3' | 'webdav' | 'cloud115' | 'quark'
|
||||
|
||||
export interface CloudEntry {
|
||||
id: string
|
||||
name: string
|
||||
is_dir: boolean
|
||||
size: number
|
||||
pick_code?: string
|
||||
}
|
||||
|
||||
export interface QRSession {
|
||||
uid: string
|
||||
time: number
|
||||
sign: string
|
||||
qr_image_url: string
|
||||
}
|
||||
|
||||
export interface QRStatus {
|
||||
state: 'waiting' | 'scanned' | 'confirmed' | 'expired'
|
||||
cookie?: string
|
||||
}
|
||||
export type StorageType = 'alist' | 's3' | 'webdav'
|
||||
|
||||
export interface StorageConfig {
|
||||
id: string
|
||||
@@ -54,24 +34,3 @@ export const storageAPI = {
|
||||
})
|
||||
.then((r) => r.data),
|
||||
}
|
||||
|
||||
// cloudAPI drives 网盘 browsing, QR login and 302 import.
|
||||
export const cloudAPI = {
|
||||
list: (type: StorageType, dir = '') =>
|
||||
api
|
||||
.get<{ items: CloudEntry[]; error?: string }>(`/admin/cloud/${type}/list`, {
|
||||
params: { dir },
|
||||
})
|
||||
.then((r) => r.data),
|
||||
|
||||
import: (type: StorageType, ref: string, name: string, size: number) =>
|
||||
api
|
||||
.post(`/admin/cloud/${type}/import`, { ref, name, size })
|
||||
.then((r) => r.data),
|
||||
|
||||
qrStart: (type: StorageType) =>
|
||||
api.post<QRSession>(`/admin/cloud/${type}/qr/start`).then((r) => r.data),
|
||||
|
||||
qrPoll: (type: StorageType, sess: QRSession) =>
|
||||
api.post<QRStatus>(`/admin/cloud/${type}/qr/poll`, sess).then((r) => r.data),
|
||||
}
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react'
|
||||
import { Cloud, FileVideo, Folder, Loader2, QrCode, Save, Send } from 'lucide-react'
|
||||
import { Cloud, Loader2, Save, Send } from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import {
|
||||
cloudAPI,
|
||||
storageAPI,
|
||||
type CloudEntry,
|
||||
type QRSession,
|
||||
type StorageType,
|
||||
} from '../api/storage_config'
|
||||
|
||||
const CLOUD_TYPES: StorageType[] = ['cloud115', 'quark']
|
||||
const isCloud = (t: StorageType) => CLOUD_TYPES.includes(t)
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
alist: 'ALIST',
|
||||
webdav: 'WEBDAV',
|
||||
s3: 'S3',
|
||||
cloud115: '115网盘',
|
||||
quark: '夸克网盘',
|
||||
}
|
||||
import { storageAPI, type StorageType } from '../api/storage_config'
|
||||
|
||||
// StorageConfigPage manages the Alist / S3 / WebDAV adapters used by
|
||||
// the import / playback / STRM subsystems. Mirrors the Vue UI's
|
||||
@@ -34,24 +18,24 @@ export function StorageConfigPage() {
|
||||
<div>
|
||||
<h1 className="font-display text-3xl font-bold text-ink-600">外部存储</h1>
|
||||
<p className="text-sm text-ink-50">
|
||||
配置 Alist / S3 / WebDAV / 网盘(115 / 夸克)后端,Cookie 加密存储 + 在线测试,网盘资源通过 302 直链播放
|
||||
配置 Alist / S3 / WebDAV 后端,支持密码加密存储 + 在线测试
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 border-b border-gray-200">
|
||||
{(['alist', 'webdav', 's3', 'cloud115', 'quark'] as StorageType[]).map((t) => (
|
||||
{(['alist', 'webdav', 's3'] as StorageType[]).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setActive(t)}
|
||||
className={
|
||||
'border-b-2 px-4 py-2 text-sm ' +
|
||||
'border-b-2 px-4 py-2 text-sm uppercase ' +
|
||||
(active === t
|
||||
? 'border-primary-400 text-brand-500'
|
||||
: 'border-transparent text-ink-50 hover:text-white')
|
||||
}
|
||||
>
|
||||
{TYPE_LABEL[t] ?? t}
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -79,14 +63,6 @@ const FIELD_DEFS: Record<StorageType, { key: string; label: string; secret?: boo
|
||||
{ key: 'secret_key', label: 'Secret Key', secret: true },
|
||||
{ key: 'force_path_style', label: 'force_path_style (true/false)' },
|
||||
],
|
||||
cloud115: [
|
||||
{ key: 'cookie', label: 'Cookie(UID / CID / SEID,或扫码登录自动填充)', secret: true, placeholder: 'UID=...; CID=...; SEID=...' },
|
||||
{ key: 'force_proxy', label: '强制反代(true/false,默认 302 直链)' },
|
||||
],
|
||||
quark: [
|
||||
{ key: 'cookie', label: 'Cookie(从 pan.quark.cn 复制整段)', secret: true, placeholder: '__pus=...; __kp=...; kps=...' },
|
||||
{ key: 'force_302', label: '强制 302 直链(true/false,默认反代)' },
|
||||
],
|
||||
}
|
||||
|
||||
function StorageForm({ type }: { type: StorageType }) {
|
||||
@@ -182,12 +158,6 @@ function StorageForm({ type }: { type: StorageType }) {
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
{type === 'cloud115' && (
|
||||
<QRLoginPanel
|
||||
type={type}
|
||||
onCookie={(c) => setConfig((cfg) => ({ ...cfg, cookie: c }))}
|
||||
/>
|
||||
)}
|
||||
<label className="flex items-center gap-2 text-sm text-ink-100">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -212,169 +182,6 @@ function StorageForm({ type }: { type: StorageType }) {
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
{isCloud(type) && <CloudBrowser type={type} />}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// QRLoginPanel drives the 115 QR-code login: start → render image → poll →
|
||||
// fill the cookie field on confirmation.
|
||||
function QRLoginPanel({ type, onCookie }: { type: StorageType; onCookie: (c: string) => void }) {
|
||||
const [sess, setSess] = useState<QRSession | null>(null)
|
||||
const [state, setState] = useState<string>('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!sess) return
|
||||
let alive = true
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const st = await cloudAPI.qrPoll(type, sess)
|
||||
if (!alive) return
|
||||
setState(st.state)
|
||||
if (st.state === 'confirmed' && st.cookie) {
|
||||
onCookie(st.cookie)
|
||||
toast.success('扫码登录成功,Cookie 已填入,请点击保存')
|
||||
setSess(null)
|
||||
} else if (st.state === 'expired') {
|
||||
toast.error('二维码已过期,请重新获取')
|
||||
setSess(null)
|
||||
}
|
||||
} catch {
|
||||
/* keep polling */
|
||||
}
|
||||
}, 2000)
|
||||
return () => {
|
||||
alive = false
|
||||
clearInterval(timer)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sess])
|
||||
|
||||
const start = async () => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const s = await cloudAPI.qrStart(type)
|
||||
setSess(s)
|
||||
setState('waiting')
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '获取二维码失败'
|
||||
toast.error(msg)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 p-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={start}
|
||||
disabled={busy}
|
||||
className="flex items-center gap-2 rounded-lg border border-gray-200 px-3 py-2 text-sm text-ink-100 hover:bg-gray-50"
|
||||
>
|
||||
{busy ? <Loader2 size={14} className="animate-spin" /> : <QrCode size={14} />}
|
||||
使用 115 App 扫码登录
|
||||
</button>
|
||||
{sess && (
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<img src={sess.qr_image_url} alt="115 QR" className="h-40 w-40 rounded bg-white p-1" />
|
||||
<span className="text-sm text-ink-50">
|
||||
{state === 'scanned' ? '已扫描,请在手机上确认登录…' : '请使用 115 手机 App 扫描二维码…'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// CloudBrowser lists 网盘 directories and imports a file as a 302-backed media.
|
||||
function CloudBrowser({ type }: { type: StorageType }) {
|
||||
const [stack, setStack] = useState<{ id: string; name: string }[]>([{ id: '', name: '根目录' }])
|
||||
const [items, setItems] = useState<CloudEntry[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const cur = stack[stack.length - 1]
|
||||
const load = async (dir: string) => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const r = await cloudAPI.list(type, dir)
|
||||
setItems(r.items ?? [])
|
||||
if (r.error) setError(r.error)
|
||||
} catch (err: unknown) {
|
||||
setError((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '加载失败')
|
||||
setItems([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load(cur.id).catch(() => undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stack.length, type])
|
||||
|
||||
const enter = (e: CloudEntry) => setStack((s) => [...s, { id: e.id, name: e.name }])
|
||||
const goTo = (i: number) => setStack((s) => s.slice(0, i + 1))
|
||||
|
||||
const doImport = async (e: CloudEntry) => {
|
||||
const ref = type === 'cloud115' ? e.pick_code || e.id : e.id
|
||||
try {
|
||||
await cloudAPI.import(type, ref, e.name, e.size)
|
||||
toast.success(`已导入「${e.name}」,可在媒体库中 302 播放`)
|
||||
} catch (err: unknown) {
|
||||
toast.error((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '导入失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2 rounded-lg border border-gray-200 p-3" onClick={(e) => e.preventDefault()}>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-1 text-xs text-ink-50">
|
||||
<span className="text-ink-100">网盘资源:</span>
|
||||
{stack.map((s, i) => (
|
||||
<span key={i}>
|
||||
<button type="button" className="hover:text-brand-500" onClick={() => goTo(i)}>
|
||||
{s.name}
|
||||
</button>
|
||||
{i < stack.length - 1 && <span className="mx-1">/</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-4 text-ink-50">
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
</div>
|
||||
) : error ? (
|
||||
<p className="py-2 text-sm text-red-400">{error}(请先填写有效 Cookie 并保存)</p>
|
||||
) : items.length === 0 ? (
|
||||
<p className="py-2 text-sm text-ink-50">该目录为空</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-gray-100">
|
||||
{items.map((e) => (
|
||||
<li key={e.id} className="flex items-center gap-2 py-1.5 text-sm">
|
||||
{e.is_dir ? <Folder size={15} className="text-amber-400" /> : <FileVideo size={15} className="text-blue-300" />}
|
||||
{e.is_dir ? (
|
||||
<button type="button" className="flex-1 text-left text-ink-100 hover:text-brand-500" onClick={() => enter(e)}>
|
||||
{e.name}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1 truncate text-ink-100">{e.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-gray-200 px-2 py-0.5 text-xs text-ink-100 hover:bg-gray-50"
|
||||
onClick={() => doImport(e)}
|
||||
>
|
||||
导入
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user