本项目已收到哔哩哔哩(bilibili)律师函,要求停止对 B 站 API 的调用及相关仿制行为。

为尊重知识产权及相关法律法规,本仓库即日起停止后续维护与更新,不再接受新的 Issue 和 Pull Request。

现有代码仅作学习参考保留,请勿将本项目用于任何商业或违法用途。

感谢所有支持过本项目的朋友。
This commit is contained in:
Developer
2026-05-12 21:29:45 +08:00
parent 53c67079a1
commit 6e0dc2729c
16 changed files with 1304 additions and 453 deletions

68
hooks/useFollow.ts Normal file
View File

@@ -0,0 +1,68 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getRelation, modifyRelation } from "../services/bilibili";
import { useAuthStore } from "../store/authStore";
import { getSecure } from "../utils/secureStorage";
import { toast } from "../utils/toast";
/**
* 关注 / 取消关注 UP 主。
* - 未登录toggle 时 toast 提示,按钮 disabled=false 仍允许点击
* - 已登录但无 bili_jcttoggle 时 toast 提示让用户重登
* - 正常路径:乐观更新 UI请求失败回滚
*/
export function useFollow(mid: number | undefined) {
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
const [following, setFollowing] = useState(false);
const [loading, setLoading] = useState(false);
const inflightRef = useRef(false);
// 首次拉取 / mid 切换时刷新关注状态
useEffect(() => {
if (!mid || !isLoggedIn) {
setFollowing(false);
return;
}
let cancelled = false;
getRelation(mid)
.then((r) => {
if (!cancelled) setFollowing(r.following);
})
.catch(() => {});
return () => {
cancelled = true;
};
}, [mid, isLoggedIn]);
const toggle = useCallback(async () => {
if (!mid) return;
if (!isLoggedIn) {
toast("请先登录后再关注");
return;
}
const biliJct = await getSecure("bili_jct");
if (!biliJct) {
toast("请重新登录后再使用关注功能");
return;
}
if (inflightRef.current) return;
inflightRef.current = true;
setLoading(true);
const next = !following;
setFollowing(next); // 乐观更新
try {
await modifyRelation(mid, next ? 1 : 2);
} catch (e: any) {
setFollowing(!next); // 失败回滚
if (e?.message === "NO_CSRF") {
toast("请重新登录后再使用关注功能");
} else {
toast(`操作失败:${e?.message || "未知错误"}`);
}
} finally {
inflightRef.current = false;
setLoading(false);
}
}, [mid, isLoggedIn, following]);
return { following, loading, toggle };
}

View File

@@ -1,4 +1,4 @@
import { useState, useCallback, useRef } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react';
import { getVideoRelated } from '../services/bilibili';
import type { VideoItem } from '../services/types';
@@ -7,6 +7,11 @@ export function useRelatedVideos(bvid: string) {
const [loading, setLoading] = useState(false);
const loadingRef = useRef(false);
// 切到不同 bvid 时立刻清空,避免新页面短暂显示上一支视频的推荐流
useEffect(() => {
setVideos([]);
}, [bvid]);
const load = useCallback(async () => {
if (loadingRef.current) return;
loadingRef.current = true;

View File

@@ -37,6 +37,12 @@ export function useVideoDetail(bvid: string) {
}
useEffect(() => {
// bvid 切换时立刻清空旧数据,防止上一支视频的播放器/简介/清晰度短暂"残影"造成抖动
setVideo(null);
setPlayData(null);
setQualities([]);
setCurrentQn(0);
cidRef.current = 0;
async function fetchData() {
try {
setLoading(true);