feat(web): 站点按类型分组、列表模式修样式、下掉自定义 RSS 源与索引器入口;智能体会话分对话 / 自动巡检两栏
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8da8679a04
commit
8eb8a68295
11 files changed
+364
-312
No files matched your search
@@ -12,8 +12,6 @@ import { ArrowLeft, Check, Globe, LoaderCircle, Rss, Search, Server, ShieldCheck
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useFetchFeedNow } from "~/api/feeds.ts";
|
||||
import { apiParsed } from "~/api/parse.ts";
|
||||
import {
|
||||
useCreateSite,
|
||||
useHealthCheck,
|
||||
@@ -49,20 +47,12 @@ import { ADAPTER_LABEL, hostOf, type LoginKind, supportedKinds } from "./site-fi
|
||||
|
||||
type Step = 0 | 1 | 2 | 3;
|
||||
const PT_STEPS = ["选择类型", "选择站点", "填写凭证", "测试"] as const;
|
||||
const FEED_STEPS = ["选择类型", "填写地址"] as const;
|
||||
const FEED_STEPS = ["选择类型", "选择站点"] as const;
|
||||
const GROUP_ORDER = ["nexusphp", "mteam_api", "api", "generic_rss"] as const;
|
||||
const FeedPage = cursorPage(FeedSource);
|
||||
const KIND_ORDER: SiteKind[] = ["pt", "public_bt", "rss_feed", "torznab"];
|
||||
/** 自定义 RSS 源与索引器入口暂时不开放(已有的 RSS 源站点照常显示) */
|
||||
const KIND_ORDER: SiteKind[] = ["pt", "public_bt"];
|
||||
const KIND_ICON = { pt: ShieldCheck, public_bt: Globe, rss_feed: Rss, torznab: Server } as const;
|
||||
|
||||
function isHttpUrl(v: string): boolean {
|
||||
try {
|
||||
return /^https?:$/.test(new URL(v.trim()).protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 馒头默认 API Key,其余默认 Cookie(验证码站点账密大概率失败) */
|
||||
function defaultKind(kinds: LoginKind[]): LoginKind {
|
||||
if (kinds.includes("api_key") && !kinds.includes("cookie")) return "api_key";
|
||||
@@ -72,10 +62,9 @@ function defaultKind(kinds: LoginKind[]): LoginKind {
|
||||
export function SiteAddDialog({ onClose, initialKind }: { onClose: () => void; initialKind?: SiteKind | null }) {
|
||||
const defs = useSiteDefinitions();
|
||||
const navigate = useNavigate();
|
||||
const startKind = initialKind && SITE_KINDS[initialKind]?.implemented ? initialKind : null;
|
||||
const startKind = initialKind && KIND_ORDER.includes(initialKind) ? initialKind : null;
|
||||
const [kind, setKind] = useState<SiteKind | null>(startKind);
|
||||
const [step, setStep] = useState<Step>(startKind ? 1 : 0);
|
||||
const [rssUrl, setRssUrl] = useState("");
|
||||
const [q, setQ] = useState("");
|
||||
const [defKey, setDefKey] = useState<string | null>(null);
|
||||
const [form, setForm] = useState({ name: "", baseUrl: "", key: "" });
|
||||
@@ -91,7 +80,6 @@ export function SiteAddDialog({ onClose, initialKind }: { onClose: () => void; i
|
||||
const put = usePutCredentials();
|
||||
const refresh = useRefreshLogin();
|
||||
const check = useHealthCheck();
|
||||
const fetchFeed = useFetchFeedNow();
|
||||
const feedKind = kind !== null && kind !== "pt";
|
||||
const stepLabels: readonly string[] = feedKind ? FEED_STEPS : PT_STEPS;
|
||||
|
||||
@@ -102,41 +90,6 @@ export function SiteAddDialog({ onClose, initialKind }: { onClose: () => void; i
|
||||
setStep(1);
|
||||
};
|
||||
|
||||
/** 公开 BT 站 / 自定义 RSS 源:建站点 + RSS(服务端一个事务),立即抓一次,进 RSS Tab */
|
||||
const saveFeedSite = async () => {
|
||||
const fe: Record<string, string> = {};
|
||||
const url = rssUrl.trim();
|
||||
const base = form.baseUrl.trim();
|
||||
if (!form.name.trim()) fe.name = "填写名称";
|
||||
if (kind === "rss_feed" && !isHttpUrl(url)) fe.rssUrl = "填写完整的 RSS 地址(http / https)";
|
||||
if (kind === "public_bt" && url && !isHttpUrl(url)) fe.rssUrl = "填写完整的 RSS 地址(http / https)";
|
||||
if (kind === "public_bt" && !isHttpUrl(base)) fe.baseUrl = "填完整地址,如 https://example.org";
|
||||
if (kind === "rss_feed" && base && !isHttpUrl(base)) fe.baseUrl = "填完整地址,如 https://example.org";
|
||||
setErrors(fe);
|
||||
if (Object.keys(fe).length || !kind) return;
|
||||
try {
|
||||
setPhase("保存中…");
|
||||
const site = await create.mutateAsync({
|
||||
kind,
|
||||
adapter: "generic_rss",
|
||||
name: form.name.trim(),
|
||||
baseUrl: base || new URL(url).origin,
|
||||
...(url ? { rssUrl: url } : {}),
|
||||
});
|
||||
if (url) {
|
||||
const feeds = await apiParsed(FeedPage, "/api/v1/feed-sources", { params: { siteId: site.id } });
|
||||
for (const f of feeds.items) fetchFeed.mutate(f.id);
|
||||
}
|
||||
toast.success(`已添加「${site.name}」`);
|
||||
onClose();
|
||||
navigate(`${consolePath("sites", site.id)}?tab=rss`);
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "保存失败"));
|
||||
} finally {
|
||||
setPhase(null);
|
||||
}
|
||||
};
|
||||
|
||||
const items = defs.data?.items ?? [];
|
||||
const def = items.find((d) => d.key === defKey);
|
||||
const kinds: LoginKind[] = supportedKinds(def);
|
||||
@@ -212,90 +165,33 @@ export function SiteAddDialog({ onClose, initialKind }: { onClose: () => void; i
|
||||
</ul>
|
||||
);
|
||||
const feedBody = (
|
||||
<form
|
||||
id="site-add-feed"
|
||||
noValidate
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void saveFeedSite();
|
||||
}}
|
||||
className="min-w-0"
|
||||
>
|
||||
{kind === "public_bt" && publicDefs.length ? (
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-caption font-medium text-foreground">内置站点</p>
|
||||
<ul className="grid gap-2 sm:grid-cols-2">
|
||||
{publicDefs.map((d) => (
|
||||
<li key={d.key}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy || !!d.siteId}
|
||||
onClick={() => void addPublic(d)}
|
||||
className="flex w-full min-w-0 items-center justify-between gap-2 rounded-lg border border-border/70 px-3 py-2 text-left transition-colors hover:border-foreground/40 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-caption font-medium text-foreground">{d.name}</span>
|
||||
<span className="block truncate text-meta text-muted-foreground">{hostOf(d.baseUrl)}</span>
|
||||
</span>
|
||||
{d.siteId ? <StatusBadge className="shrink-0">已添加</StatusBadge> : null}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-3 mb-2 text-meta text-muted-foreground">
|
||||
内置站点支持搜索和定时抓取最新资源,不需要账号。其他公开站可以在下面手动填 RSS 地址。
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
{publicDefs.length ? (
|
||||
<ul className="grid gap-2 sm:grid-cols-2">
|
||||
{publicDefs.map((d) => (
|
||||
<li key={d.key}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy || !!d.siteId}
|
||||
onClick={() => void addPublic(d)}
|
||||
className="flex w-full min-w-0 items-center justify-between gap-2 rounded-lg border border-border/70 px-3 py-2 text-left transition-colors hover:border-foreground/40 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-caption font-medium text-foreground">{d.name}</span>
|
||||
<span className="block truncate text-meta text-muted-foreground">{hostOf(d.baseUrl)}</span>
|
||||
</span>
|
||||
{d.siteId ? <StatusBadge className="shrink-0">已添加</StatusBadge> : null}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : defs.isPending ? (
|
||||
<LoadingState rows={2} />
|
||||
) : null}
|
||||
<FormField label="名称" htmlFor="add-feed-name" required error={errors.name}>
|
||||
<Input
|
||||
id="add-feed-name"
|
||||
autoFocus
|
||||
value={form.name}
|
||||
aria-invalid={!!errors.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
disabled={busy}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="RSS 地址"
|
||||
htmlFor="add-feed-rss"
|
||||
required={kind === "rss_feed"}
|
||||
error={errors.rssUrl}
|
||||
hint={kind === "public_bt" ? "可选;之后可在站点的 RSS 里添加" : "地址加密保存,页面上只显示去掉密钥参数的地址"}
|
||||
>
|
||||
<Input
|
||||
id="add-feed-rss"
|
||||
type="url"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
placeholder="https://"
|
||||
value={rssUrl}
|
||||
aria-invalid={!!errors.rssUrl}
|
||||
onChange={(e) => setRssUrl(e.target.value)}
|
||||
disabled={busy}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="网址"
|
||||
htmlFor="add-feed-url"
|
||||
required={kind === "public_bt"}
|
||||
error={errors.baseUrl}
|
||||
hint={kind === "rss_feed" ? "可选;留空时用 RSS 地址的域名" : undefined}
|
||||
>
|
||||
<Input
|
||||
id="add-feed-url"
|
||||
inputMode="url"
|
||||
placeholder="https://"
|
||||
value={form.baseUrl}
|
||||
aria-invalid={!!errors.baseUrl}
|
||||
onChange={(e) => setForm((f) => ({ ...f, baseUrl: e.target.value }))}
|
||||
disabled={busy}
|
||||
/>
|
||||
</FormField>
|
||||
<p className="text-meta text-muted-foreground">不需要账号。保存后立即抓一次 RSS。</p>
|
||||
</form>
|
||||
<p className="mt-3 text-meta text-muted-foreground">
|
||||
不需要账号;添加后默认走代理,定时抓取最新资源,也能在资源搜索里选用。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const validate = () => {
|
||||
@@ -562,16 +458,10 @@ export function SiteAddDialog({ onClose, initialKind }: { onClose: () => void; i
|
||||
取消
|
||||
</Button>
|
||||
) : feedKind ? (
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setStep(0)} disabled={busy}>
|
||||
<ArrowLeft />
|
||||
重选类型
|
||||
</Button>
|
||||
<Button type="submit" form="site-add-feed" disabled={busy}>
|
||||
{busy ? <LoaderCircle className="animate-spin" /> : null}
|
||||
保存
|
||||
</Button>
|
||||
</>
|
||||
<Button variant="ghost" onClick={() => setStep(0)} disabled={busy}>
|
||||
<ArrowLeft />
|
||||
重选类型
|
||||
</Button>
|
||||
) : step === 2 ? (
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => setStep(1)} disabled={busy || !!created}>
|
||||
|
||||
Reference in new issue
Block a user