added:增加Linux-server

This commit is contained in:
Vaica committed 2026-06-21 20:42:44 +08:00
1 parent 6fe4e9d23f
commit d78f3f811b
90 files changed
+16784 -1

No files matched your search

+234
View File
@@ -0,0 +1,234 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useRouter } from "next/navigation";
import { ArrowLeft, Plus, Trash2, Loader2, Wifi, WifiOff, Edit, X } from "lucide-react";
interface Feed {
url: string;
feedTitle: string;
siteUrl?: string;
error?: string;
}
interface FeedHealth {
url: string;
status: number;
ok: boolean;
latency?: number;
error?: string;
}
export default function FeedsPage() {
const router = useRouter();
const [feeds, setFeeds] = useState<Feed[]>([]);
const [loading, setLoading] = useState(true);
const [health, setHealth] = useState<Map<string, { ok: boolean; status?: number; latency?: number }>>(new Map());
const [checkingAll, setCheckingAll] = useState(false);
const [checkingMap, setCheckingMap] = useState<Map<string, boolean>>(new Map());
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
// Add form
const [showAdd, setShowAdd] = useState(false);
const [addForm, setAddForm] = useState({ url: "", feedTitle: "" });
const [adding, setAdding] = useState(false);
// Edit
const [editTarget, setEditTarget] = useState<string | null>(null);
const [editForm, setEditForm] = useState({ url: "", feedTitle: "" });
const [editing, setEditing] = useState(false);
const fetchFeeds = useCallback(async () => {
setLoading(true);
try {
const res = await fetch("/api/rss/feeds");
const data = await res.json();
setFeeds(data.feeds || []);
} catch { setMessage({ type: "error", text: "加载失败" }); }
finally { setLoading(false); }
}, []);
useEffect(() => { fetchFeeds(); }, [fetchFeeds]);
const checkOne = async (url: string) => {
setCheckingMap((prev) => new Map(prev).set(url, true));
try {
const res = await fetch(`/api/rss/feeds?health=${encodeURIComponent(url)}`);
const data = await res.json();
setHealth((prev) => new Map(prev).set(url, data));
} catch {} finally {
setCheckingMap((prev) => { const m = new Map(prev); m.delete(url); return m; });
}
};
const checkAll = async () => {
setCheckingAll(true);
for (let i = 0; i < feeds.length; i += 3) {
const batch = feeds.slice(i, i + 3);
await Promise.all(batch.map((f) => checkOne(f.url)));
}
setCheckingAll(false);
};
const handleAdd = async (e: React.FormEvent) => {
e.preventDefault();
if (!addForm.url) return;
setAdding(true);
try {
const res = await fetch("/api/rss/feeds", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(addForm),
});
if (!res.ok) throw new Error("添加失败");
setShowAdd(false);
setAddForm({ url: "", feedTitle: "" });
fetchFeeds();
} catch (err: unknown) {
setMessage({ type: "error", text: err instanceof Error ? err.message : "添加失败" });
} finally { setAdding(false); }
};
const startEdit = (f: Feed) => {
setEditTarget(f.url);
setEditForm({ url: f.url, feedTitle: f.feedTitle });
};
const handleEdit = async () => {
if (!editTarget) return;
setEditing(true);
try {
const res = await fetch("/api/rss/feeds", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ oldUrl: editTarget, url: editForm.url, feedTitle: editForm.feedTitle }),
});
if (!res.ok) throw new Error("保存失败");
setEditTarget(null);
fetchFeeds();
} catch (err: unknown) {
setMessage({ type: "error", text: err instanceof Error ? err.message : "保存失败" });
} finally { setEditing(false); }
};
const handleDelete = async (url: string, title: string) => {
if (!confirm(`确定删除「${title}」?`)) return;
try {
await fetch(`/api/rss/feeds?url=${encodeURIComponent(url)}`, { method: "DELETE" });
setFeeds((prev) => prev.filter((f) => f.url !== url));
} catch { setMessage({ type: "error", text: "删除失败" }); }
};
return (
<div className="px-4 py-6 md:px-8 md:py-8">
<div className="flex items-center justify-between mb-6 sm:mb-8 gap-3 flex-wrap">
<div className="flex items-center gap-3">
<button type="button" onClick={() => router.back()} className="p-1 -ml-1 rounded hover:bg-surface text-muted hover:text-text transition-colors">
<ArrowLeft size={16} />
</button>
<h2 className="text-lg font-bold text-text">订阅源管理</h2>
<span className="text-[13px] text-muted">{feeds.length} 个</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={checkAll} disabled={checkingAll}
className="flex items-center gap-1.5 px-3 py-1.5 text-[12px] rounded-md border border-line text-muted hover:text-accent transition-colors disabled:opacity-50">
{checkingAll ? <Loader2 size={13} className="animate-spin" /> : <Wifi size={13} />}
{checkingAll ? "检测中..." : "检测存活"}
</button>
<button type="button" onClick={() => setShowAdd(true)}
className="flex items-center gap-1.5 px-3 py-1.5 text-[12px] font-medium rounded-md bg-accent text-white hover:bg-accent-hover transition-colors">
<Plus size={13} /> 添加
</button>
</div>
</div>
{message && (
<div className={`mb-4 px-4 py-2.5 rounded-md text-[13px] flex items-center justify-between ${
message.type === "success" ? "bg-[#d1fae5] text-[#065f46] border border-[#a7f3d0]" : "bg-[#fee2e2] text-[#991b1b] border border-[#fecaca]"
}`}>
{message.text}
<button type="button" onClick={() => setMessage(null)} className="underline">关闭</button>
</div>
)}
{showAdd && (
<div className="mb-6 p-5 bg-card border border-line rounded-lg">
<form onSubmit={handleAdd} className="flex flex-col sm:flex-row gap-3 sm:items-end">
<div className="flex-1">
<label className="block text-[12px] font-medium text-text/70 mb-1.5">博客名称</label>
<input type="text" value={addForm.feedTitle} onChange={(e) => setAddForm({ ...addForm, feedTitle: e.target.value })}
className="w-full bg-bg border border-line rounded-md px-3 py-2 text-[13px] focus:outline-none focus:border-accent/30"
placeholder="例如:刘郎阁" />
</div>
<div className="flex-[2]">
<label className="block text-[12px] font-medium text-text/70 mb-1.5">RSS/站点 URL</label>
<input type="text" value={addForm.url} onChange={(e) => setAddForm({ ...addForm, url: e.target.value })}
className="w-full bg-bg border border-line rounded-md px-3 py-2 text-[13px] focus:outline-none focus:border-accent/30"
placeholder="https://example.com" required />
</div>
<button type="submit" disabled={adding} className="px-4 py-2 text-[13px] font-medium rounded-md bg-accent text-white hover:bg-accent-hover disabled:opacity-50 shrink-0" title="添加订阅源">
{adding ? "..." : "添加"}
</button>
<button type="button" onClick={() => setShowAdd(false)} className="px-3 py-2 text-[13px] rounded-md border border-line text-muted hover:bg-surface shrink-0" title="取消">取消</button>
</form>
</div>
)}
{loading ? (
<div className="text-center py-20 text-muted text-sm">加载中...</div>
) : feeds.length === 0 ? (
<div className="text-center py-20 text-muted text-sm">暂无订阅源</div>
) : (
<div className="space-y-2">
{feeds.map((f) => {
const h = health.get(f.url);
const ck = checkingMap.get(f.url);
return (
<div key={f.url} className={`flex items-center gap-4 px-5 py-3.5 bg-card border rounded-md group transition-all ${
editTarget === f.url ? "ring-2 ring-accent/20 border-accent" : "border-line"
}`}>
{ck ? <Loader2 size={13} className="animate-spin text-muted shrink-0" />
: h ? (h.ok ? <span title={`${h.status} ${h.latency}ms`}><Wifi size={13} className="text-success shrink-0" /></span>
: <span title={h.status ? String(h.status) : "不可达"}><WifiOff size={13} className="text-danger shrink-0" /></span>)
: null}
{editTarget === f.url ? (
<div className="flex-1 flex flex-col sm:flex-row gap-2 sm:items-end">
<div className="flex-1">
<input type="text" value={editForm.feedTitle} onChange={(e) => setEditForm({ ...editForm, feedTitle: e.target.value })}
className="w-full bg-bg border border-line rounded-md px-2 py-1.5 text-[13px] focus:outline-none focus:border-accent/30" title="名称" placeholder="博客名称" />
</div>
<div className="flex-[2]">
<input type="text" value={editForm.url} onChange={(e) => setEditForm({ ...editForm, url: e.target.value })}
className="w-full bg-bg border border-line rounded-md px-2 py-1.5 text-[13px] focus:outline-none focus:border-accent/30" title="URL" placeholder="URL" />
</div>
<button type="button" onClick={handleEdit} disabled={editing}
className="px-3 py-1.5 text-[12px] font-medium rounded-md bg-accent text-white hover:bg-accent-hover shrink-0" title="保存">{editing ? "..." : "保存"}</button>
<button type="button" onClick={() => setEditTarget(null)}
className="p-1.5 rounded text-muted hover:text-text" title="取消"><X size={14} /></button>
</div>
) : (
<div className="flex-1 min-w-0">
<div className="font-medium text-[14px] text-text truncate">{f.feedTitle || f.url}</div>
<div className="text-[11px] text-muted truncate mt-0.5">{f.url}</div>
</div>
)}
{editTarget !== f.url && (
<div className="flex items-center gap-0.5 shrink-0 md:opacity-0 md:group-hover:opacity-100 transition-all">
<button type="button" onClick={() => startEdit(f)} className="p-1.5 rounded hover:bg-surface text-muted hover:text-text transition-colors" title="编辑">
<Edit size={13} />
</button>
<button type="button" onClick={() => handleDelete(f.url, f.feedTitle)} className="p-1.5 rounded hover:bg-danger/8 text-muted hover:text-danger transition-colors" title="删除">
<Trash2 size={13} />
</button>
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
}