feat: 首次推送到Gitea - 完整项目代码 + 安全加固 + 知识库
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { LazyImage } from "@/components/ui/LazyImage";
|
||||
|
||||
interface Tool {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -137,6 +139,19 @@ export default function ToolsAdminPage() {
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [verifiedToolIds, setVerifiedToolIds] = useState<Set<number>>(new Set());
|
||||
|
||||
const fetchVerifiedTools = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/admin/tools/verify");
|
||||
const data = await res.json();
|
||||
if (data.verifiedTools) {
|
||||
const ids = new Set<number>();
|
||||
(data.verifiedTools as { toolId: number }[]).forEach((v) => ids.add(v.toolId));
|
||||
setVerifiedToolIds(ids);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
|
||||
const fetchTools = useCallback(async () => {
|
||||
setError("");
|
||||
@@ -173,7 +188,8 @@ export default function ToolsAdminPage() {
|
||||
useEffect(() => {
|
||||
fetchTools();
|
||||
fetchCategories();
|
||||
}, [fetchTools, fetchCategories]);
|
||||
fetchVerifiedTools();
|
||||
}, [fetchTools, fetchCategories, fetchVerifiedTools]);
|
||||
|
||||
const openEdit = (tool: Tool) => {
|
||||
setIsNew(false);
|
||||
@@ -247,6 +263,30 @@ export default function ToolsAdminPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyToggle = async (toolId: number) => {
|
||||
try {
|
||||
const res = await fetch("/api/admin/tools/verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ toolId }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setVerifiedToolIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (data.verified) {
|
||||
next.add(toolId);
|
||||
} else {
|
||||
next.delete(toolId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.error("Verify toggle failed");
|
||||
}
|
||||
};
|
||||
|
||||
const publishedCount = tools.filter((t) => t.status === "published").length;
|
||||
const draftCount = tools.filter((t) => t.status === "draft").length;
|
||||
const totalViews = tools.reduce((sum, t) => sum + t.viewCount, 0);
|
||||
@@ -403,10 +443,12 @@ export default function ToolsAdminPage() {
|
||||
<td className="px-5 py-3.5">
|
||||
<div className="flex items-center gap-3">
|
||||
{tool.logoUrl ? (
|
||||
<img
|
||||
<LazyImage
|
||||
src={tool.logoUrl}
|
||||
referrerPolicy="no-referrer"
|
||||
alt=""
|
||||
width={48}
|
||||
height={48}
|
||||
className="h-9 w-9 rounded-lg border border-border/50 bg-white object-contain p-0.5"
|
||||
/>
|
||||
) : (
|
||||
@@ -441,6 +483,22 @@ export default function ToolsAdminPage() {
|
||||
</td>
|
||||
<td className="px-5 py-3.5">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleVerifyToggle(tool.id)}
|
||||
className={`gap-1.5 rounded-lg text-xs ${
|
||||
verifiedToolIds.has(tool.id)
|
||||
? "border-blue-300/60 bg-blue-50 text-blue-600 hover:bg-blue-100"
|
||||
: "border-border/50"
|
||||
}`}
|
||||
title={verifiedToolIds.has(tool.id) ? "取消验证" : "标记为已验证"}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{verifiedToolIds.has(tool.id) ? "已验证" : "验证"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
|
||||
Reference in New Issue
Block a user