fix: 定时任务修复+管理后台Task7支持 (Task4外键修复, Task7管理员邮箱动态查询, Task5限流防护, Task7加入SCRIPT_MAP)
This commit is contained in:
@@ -13,6 +13,12 @@ export async function POST(request: Request) {
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return NextResponse.json({ error: "ids数组不能为空" }, { status: 400 });
|
||||
}
|
||||
if (ids.length > 500) {
|
||||
return NextResponse.json({ error: "一次最多处理500条" }, { status: 400 });
|
||||
}
|
||||
|
||||
const defaultCat = await prisma.skillCategory.findFirst({ select: { id: true } });
|
||||
const defaultCategoryId = defaultCat?.id ?? 0;
|
||||
|
||||
const pendings = await prisma.pendingSkill.findMany({
|
||||
where: { id: { in: ids } },
|
||||
@@ -92,7 +98,7 @@ export async function POST(request: Request) {
|
||||
logoUrl: pending.logoUrl,
|
||||
sourceUrl: pending.sourceUrl || "",
|
||||
sourceType: pending.sourceType || "github",
|
||||
categoryId: pending.categoryId || 1,
|
||||
categoryId: pending.categoryId || defaultCategoryId,
|
||||
rating: pending.rating || 0,
|
||||
features: pending.features ?? undefined,
|
||||
tags: pending.tags ?? undefined,
|
||||
|
||||
@@ -7,6 +7,9 @@ export async function POST() {
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const defaultCat = await prisma.skillCategory.findFirst({ select: { id: true } });
|
||||
const defaultCategoryId = defaultCat?.id ?? 0;
|
||||
|
||||
const pendingSkills = await prisma.pendingSkill.findMany({
|
||||
where: { status: "pending" },
|
||||
});
|
||||
@@ -62,7 +65,7 @@ export async function POST() {
|
||||
logoUrl: pending.logoUrl,
|
||||
sourceUrl: pending.sourceUrl || "",
|
||||
sourceType: pending.sourceType || "github",
|
||||
categoryId: pending.categoryId || 1,
|
||||
categoryId: pending.categoryId || defaultCategoryId,
|
||||
rating: pending.rating || 0,
|
||||
features: pending.features ?? undefined,
|
||||
tags: pending.tags ?? undefined,
|
||||
|
||||
@@ -13,6 +13,12 @@ export async function POST(request: Request) {
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return NextResponse.json({ error: "ids数组不能为空" }, { status: 400 });
|
||||
}
|
||||
if (ids.length > 500) {
|
||||
return NextResponse.json({ error: "一次最多处理500条" }, { status: 400 });
|
||||
}
|
||||
|
||||
const defaultCat = await prisma.category.findFirst({ select: { id: true } });
|
||||
const defaultCategoryId = defaultCat?.id ?? 0;
|
||||
|
||||
const pendings = await prisma.pendingTool.findMany({
|
||||
where: { id: { in: ids } },
|
||||
@@ -97,7 +103,7 @@ export async function POST(request: Request) {
|
||||
description: pending.description || "",
|
||||
websiteUrl: pending.websiteUrl || "",
|
||||
logoUrl: pending.logoUrl,
|
||||
categoryId: pending.categoryId || 1,
|
||||
categoryId: pending.categoryId || defaultCategoryId,
|
||||
pricingModel: (pending.pricingModel || "freemium") as "free" | "freemium" | "paid" | "subscription",
|
||||
pricingDetail: pending.pricingDetail,
|
||||
features: pending.features ?? undefined,
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET() {
|
||||
{ taskKey: "task5-update-stars", taskName: "更新星值", cronExpr: "0 1 * * *", enabled: true, desc: "更新所有GitHub技能的最新Stars数,计算日/周/月变化趋势" },
|
||||
{ taskKey: "task6-review-hot", taskName: "评测热门", cronExpr: "0 4 * * *", enabled: true, desc: "对未评测的最热门2个AI技能进行五维深度评测并自动发布" },
|
||||
{ taskKey: "daily-news", taskName: "AI日报生成", cronExpr: "30 7 * * *", enabled: true, desc: "搜索真实新闻素材,AI整理格式生成当日AI日报" },
|
||||
{ taskKey: "task7-news-to-community", taskName: "新闻分发到社区", cronExpr: "0 8 * * *", enabled: true, desc: "将AI日报条目自动分发到社区对应板块创建话题" },
|
||||
];
|
||||
|
||||
const configs = await prisma.taskConfig.findMany();
|
||||
|
||||
@@ -14,6 +14,7 @@ const SCRIPT_MAP: Record<string, string> = {
|
||||
"task5-update-stars": "scripts/task5-update-stars.mjs",
|
||||
"task6-review-hot": "scripts/task6-review-hot.mjs",
|
||||
"daily-news": "scripts/daily-news.mjs",
|
||||
"task7-news-to-community": "scripts/task7-news-to-community.mjs",
|
||||
};
|
||||
|
||||
const PROJECT_ROOT = process.env.SCRIPTS_ROOT || process.cwd();
|
||||
|
||||
@@ -53,9 +53,21 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireAdmin();
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.name || !body.websiteUrl || !body.categoryId) {
|
||||
return NextResponse.json({ error: "名称、网址、分类ID不能为空" }, { status: 400 });
|
||||
}
|
||||
|
||||
const existingCategory = await prisma.category.findUnique({ where: { id: body.categoryId } });
|
||||
if (!existingCategory) {
|
||||
return NextResponse.json({ error: "分类ID不存在" }, { status: 400 });
|
||||
}
|
||||
|
||||
let slug = body.slug || body.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "tool";
|
||||
const existing = await prisma.tool.findUnique({ where: { slug } });
|
||||
if (existing) {
|
||||
@@ -66,7 +78,7 @@ export async function POST(request: Request) {
|
||||
data: {
|
||||
name: body.name,
|
||||
slug,
|
||||
categoryId: body.categoryId || 1,
|
||||
categoryId: body.categoryId,
|
||||
description: body.description || "",
|
||||
logoUrl: body.logoUrl || null,
|
||||
websiteUrl: body.websiteUrl || "",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const userId = parseInt(params.id);
|
||||
if (isNaN(userId)) {
|
||||
return NextResponse.json({ error: "无效的用户ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { botConfig: true },
|
||||
});
|
||||
|
||||
if (!user || !user.isBot) {
|
||||
return NextResponse.json({ error: "未找到该Bot" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
avatarUrl: user.avatarUrl,
|
||||
isBot: user.isBot,
|
||||
level: user.level,
|
||||
points: user.points,
|
||||
botConfig: user.botConfig
|
||||
? {
|
||||
displayName: user.botConfig.displayName,
|
||||
personality: user.botConfig.personality,
|
||||
primaryForums: user.botConfig.primaryForums,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,19 @@ import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { getLevelInfo } from "@/lib/level-config";
|
||||
|
||||
const INDUSTRY_FORUMS = [
|
||||
{ name: "电商零售", slug: "ecommerce", icon: "🛒" },
|
||||
{ name: "AI智能科技", slug: "ai-tech", icon: "🤖" },
|
||||
{ name: "房地产", slug: "real-estate", icon: "🏠" },
|
||||
{ name: "金融投资", slug: "finance", icon: "📈" },
|
||||
{ name: "餐饮食品", slug: "food", icon: "🍜" },
|
||||
{ name: "自媒体内容", slug: "content", icon: "🎬" },
|
||||
{ name: "本地生活", slug: "local-life", icon: "🏪" },
|
||||
{ name: "大健康", slug: "health", icon: "💊" },
|
||||
{ name: "教育培训", slug: "education", icon: "📚" },
|
||||
{ name: "外贸跨境", slug: "cross-border", icon: "🌏" },
|
||||
];
|
||||
|
||||
interface UserRanking {
|
||||
id: number;
|
||||
name: string | null;
|
||||
@@ -77,6 +90,30 @@ export function CommunitySidebar() {
|
||||
return (
|
||||
<div className="sidebar-bg rounded-xl p-3 sticky top-[68px] max-h-[calc(100vh-76px)] overflow-y-auto scrollbar-thin space-y-4">
|
||||
|
||||
{/* Industry Forums */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 px-2 py-1 mb-2">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-indigo-500 shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0H5m0 0H3m6-8h1m4 0h1m-6 4h1m4 0h1" />
|
||||
</svg>
|
||||
<span className="text-[12px] font-semibold text-foreground/80">行业论坛</span>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{INDUSTRY_FORUMS.map((forum) => (
|
||||
<Link
|
||||
key={forum.slug}
|
||||
href={`/community/${forum.slug}`}
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2 text-[11px] transition-all duration-200 text-muted-foreground hover:bg-white hover:text-sky-600 hover:shadow-sm"
|
||||
>
|
||||
<span className="text-sm shrink-0">{forum.icon}</span>
|
||||
<span className="flex-1 truncate text-left">{forum.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border/40" />
|
||||
|
||||
{/* Monthly Active Users */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 px-2 py-1 mb-2">
|
||||
|
||||
@@ -15,6 +15,11 @@ export default async function CategoryPage({ params, searchParams }: Props) {
|
||||
|
||||
if (!category) notFound();
|
||||
|
||||
const childCategories = await prisma.forumCategory.findMany({
|
||||
where: { parentId: category.id },
|
||||
select: { id: true, name: true, slug: true },
|
||||
});
|
||||
|
||||
const sort = searchParams.sort || "latest";
|
||||
const page = parseInt(searchParams.page || "1", 10);
|
||||
const pageSize = 20;
|
||||
@@ -30,7 +35,7 @@ export default async function CategoryPage({ params, searchParams }: Props) {
|
||||
where: { categoryId: category.id },
|
||||
include: {
|
||||
category: { select: { name: true, slug: true, icon: true } },
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true, isBot: true } },
|
||||
_count: { select: { posts: true } },
|
||||
},
|
||||
orderBy: [{ isPinned: "desc" }, orderBy],
|
||||
@@ -70,6 +75,26 @@ export default async function CategoryPage({ params, searchParams }: Props) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{childCategories.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2 mb-6">
|
||||
<a
|
||||
href={`/community/${category.slug}`}
|
||||
className="px-3 py-1.5 rounded-lg text-sm bg-primary text-white"
|
||||
>
|
||||
全部
|
||||
</a>
|
||||
{childCategories.map((child) => (
|
||||
<a
|
||||
key={child.id}
|
||||
href={`/community/${child.slug}`}
|
||||
className="px-3 py-1.5 rounded-lg text-sm bg-white border border-border/40 text-muted-foreground hover:border-border/60 hover:text-foreground transition-colors"
|
||||
>
|
||||
{child.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{topics.map((topic) => {
|
||||
const initial = topic.user.name?.charAt(0).toUpperCase() || "?";
|
||||
@@ -99,7 +124,7 @@ export default async function CategoryPage({ params, searchParams }: Props) {
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center gap-3 text-xs text-muted-foreground/50">
|
||||
<span>{topic.user.name || topic.user.email?.split("@")[0]}</span>
|
||||
<span>{topic.user.name || topic.user.email?.split("@")[0]}{topic.user.isBot && <span className="ml-1 px-1 py-0.5 text-[9px] font-medium bg-sky-100 text-sky-600 rounded">AI</span>}</span>
|
||||
<span>{topic.createdAt?.toLocaleDateString("zh-CN")}</span>
|
||||
<span>{topic._count.posts}回复</span>
|
||||
<span>{topic.viewCount}浏览</span>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface ReplyFormProps {
|
||||
topicId: number;
|
||||
isLocked: boolean;
|
||||
}
|
||||
|
||||
export function ReplyForm({ topicId, isLocked }: ReplyFormProps) {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
if (isLocked) {
|
||||
return (
|
||||
<div className="mt-8 text-center py-6 rounded-xl border border-border/40 bg-muted/20">
|
||||
<p className="text-sm text-muted-foreground">此话题已锁定,无法回复</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const content = formData.get("content") as string;
|
||||
if (!content.trim()) {
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
const res = await fetch(`/api/forum/topics/${topicId}/posts`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
setSubmitting(false);
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
className="rounded-xl border border-border/50 bg-white p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
|
||||
>
|
||||
<h3 className="text-sm font-semibold mb-3">发表回复</h3>
|
||||
<textarea
|
||||
name="content"
|
||||
placeholder="写下你的回复..."
|
||||
rows={4}
|
||||
maxLength={5000}
|
||||
className="w-full resize-none rounded-lg border border-border/60 bg-white px-3 py-2 text-sm focus:outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/15 transition-colors placeholder:text-muted-foreground/40 mb-3"
|
||||
disabled={submitting}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="rounded-lg bg-primary text-white text-sm px-5 py-2 hover:bg-primary/90 transition-colors shadow-sm shadow-primary/20 disabled:opacity-50"
|
||||
>
|
||||
{submitting ? "提交中..." : "发表回复"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { prisma } from "@/lib/prisma";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { ReplyForm } from "./ReplyForm";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -17,7 +18,7 @@ export default async function TopicPage({ params }: Props) {
|
||||
where: { id: topicId },
|
||||
include: {
|
||||
category: { select: { name: true, slug: true, icon: true } },
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true, isBot: true } },
|
||||
_count: { select: { posts: true } },
|
||||
},
|
||||
});
|
||||
@@ -32,7 +33,7 @@ export default async function TopicPage({ params }: Props) {
|
||||
const posts = await prisma.forumPost.findMany({
|
||||
where: { topicId },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
|
||||
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true, isBot: true } },
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
take: 50,
|
||||
@@ -79,7 +80,7 @@ export default async function TopicPage({ params }: Props) {
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium">{topic.user.name || topic.user.email?.split("@")[0]}</p>
|
||||
<p className="text-sm font-medium">{topic.user.name || topic.user.email?.split("@")[0]}{topic.user.isBot && <span className="ml-1.5 px-1.5 py-0.5 text-[9px] font-medium bg-gradient-to-r from-sky-100 to-purple-100 text-sky-700 rounded-full border border-sky-200/50">🤖 AI</span>}</p>
|
||||
<p className="text-xs text-muted-foreground/50">
|
||||
{topic.createdAt?.toLocaleDateString("zh-CN")} {topic.createdAt?.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })}
|
||||
</p>
|
||||
@@ -113,7 +114,7 @@ export default async function TopicPage({ params }: Props) {
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium">{post.user.name || post.user.email?.split("@")[0]}</p>
|
||||
<p className="text-sm font-medium">{post.user.name || post.user.email?.split("@")[0]}{post.user.isBot && <span className="ml-1.5 px-1.5 py-0.5 text-[9px] font-medium bg-gradient-to-r from-sky-100 to-purple-100 text-sky-700 rounded-full border border-sky-200/50">🤖 AI</span>}</p>
|
||||
<p className="text-xs text-muted-foreground/50">
|
||||
{post.createdAt?.toLocaleDateString("zh-CN")} {post.createdAt?.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })}
|
||||
</p>
|
||||
@@ -133,52 +134,7 @@ export default async function TopicPage({ params }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!topic.isLocked && (
|
||||
<div className="mt-8">
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const content = formData.get("content") as string;
|
||||
if (!content.trim()) return;
|
||||
|
||||
const res = await fetch(`/api/forum/topics/${topicId}/posts`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
className="rounded-xl border border-border/50 bg-white p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
|
||||
>
|
||||
<h3 className="text-sm font-semibold mb-3">发表回复</h3>
|
||||
<textarea
|
||||
name="content"
|
||||
placeholder="写下你的回复..."
|
||||
rows={4}
|
||||
maxLength={5000}
|
||||
className="w-full resize-none rounded-lg border border-border/60 bg-white px-3 py-2 text-sm focus:outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/15 transition-colors placeholder:text-muted-foreground/40 mb-3"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-lg bg-primary text-white text-sm px-5 py-2 hover:bg-primary/90 transition-colors shadow-sm shadow-primary/20"
|
||||
>
|
||||
发表回复
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{topic.isLocked && (
|
||||
<div className="mt-8 text-center py-6 rounded-xl border border-border/40 bg-muted/20">
|
||||
<p className="text-sm text-muted-foreground">此话题已锁定,无法回复</p>
|
||||
</div>
|
||||
)}
|
||||
<ReplyForm topicId={topicId} isLocked={topic.isLocked} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -251,7 +251,7 @@ export default function SkillsPageClient({
|
||||
<HeroBanner
|
||||
title="追光"
|
||||
highlight="Skills"
|
||||
colorScheme="crimson"
|
||||
colorScheme="orange"
|
||||
stats={[
|
||||
{ label: "AI技能", value: totalSkills, unit: "个" },
|
||||
{ label: "分类", value: categories.length, unit: "个" },
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
|
||||
type ColorScheme = "cosmos" | "aurora" | "dawn" | "forest" | "crimson";
|
||||
type ColorScheme = "cosmos" | "aurora" | "dawn" | "forest" | "crimson" | "orange";
|
||||
|
||||
interface HeroBannerProps {
|
||||
title: string;
|
||||
@@ -127,12 +127,27 @@ const SCHEMES: Record<ColorScheme, SchemeConfig> = {
|
||||
{ position: "75% 60%", color: "rgba(251,113,133,0.12)" },
|
||||
],
|
||||
},
|
||||
orange: {
|
||||
bgGradient: "linear-gradient(135deg, #92400e 0%, #b45309 25%, #d97706 55%, #f59e0b 100%)",
|
||||
highlight: "#fde68a",
|
||||
star: { r: 253, g: 230, b: 138 },
|
||||
shoot: { r: 252, g: 211, b: 77 },
|
||||
topBorder: "linear-gradient(90deg, transparent, rgba(251,191,36,0.5), rgba(245,158,11,0.4), transparent)",
|
||||
bottomBorder: "linear-gradient(90deg, transparent, rgba(251,191,36,0.4), rgba(245,158,11,0.3), transparent)",
|
||||
statBg: "rgba(253,211,77,0.15)",
|
||||
statColor: "#fef3c7",
|
||||
statBorder: "1px solid rgba(245,158,11,0.25)",
|
||||
overlays: [
|
||||
{ position: "40% 40%", color: "rgba(245,158,11,0.18)" },
|
||||
{ position: "75% 60%", color: "rgba(253,211,77,0.12)" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const SCHEME_KEYS: ColorScheme[] = ["cosmos", "aurora", "dawn", "forest", "crimson"];
|
||||
|
||||
const STAR_COUNTS: Record<ColorScheme, number> = {
|
||||
cosmos: 120, aurora: 80, dawn: 100, forest: 90, crimson: 85,
|
||||
cosmos: 120, aurora: 80, dawn: 100, forest: 90, crimson: 85, orange: 80,
|
||||
};
|
||||
const SHOOTING_INTERVAL = 5000;
|
||||
|
||||
@@ -370,27 +385,27 @@ export function HeroBanner({
|
||||
<div className="relative w-36 h-36">
|
||||
{colorScheme === "cosmos" && (
|
||||
<svg className="w-full h-full" viewBox="0 0 144 144" fill="none">
|
||||
<circle cx="72" cy="72" r="60" stroke="rgba(147,197,253,0.25)" strokeWidth="0.5" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="30s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="45" stroke="rgba(191,219,254,0.20)" strokeWidth="0.5" fill="none" strokeDasharray="4 6"><animateTransform attributeName="transform" type="rotate" from="360 72 72" to="0 72 72" dur="25s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="30" stroke="rgba(147,197,253,0.15)" strokeWidth="0.5" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="20s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="8" fill="rgba(147,197,253,0.25)" /><circle cx="72" cy="72" r="3" fill="rgba(191,219,254,0.8)" />
|
||||
<circle cx="72" cy="12" r="2" fill="rgba(191,219,254,0.6)"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="30s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="27" r="1.5" fill="rgba(147,197,253,0.5)"><animateTransform attributeName="transform" type="rotate" from="360 72 72" to="0 72 72" dur="25s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="60" stroke="rgba(147,197,253,0.14)" strokeWidth="0.5" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="30s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="45" stroke="rgba(191,219,254,0.10)" strokeWidth="0.5" fill="none" strokeDasharray="4 6"><animateTransform attributeName="transform" type="rotate" from="360 72 72" to="0 72 72" dur="25s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="30" stroke="rgba(147,197,253,0.08)" strokeWidth="0.5" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="20s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="8" fill="rgba(147,197,253,0.14)" /><circle cx="72" cy="72" r="3" fill="rgba(191,219,254,0.45)" />
|
||||
<circle cx="72" cy="12" r="2" fill="rgba(191,219,254,0.30)"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="30s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="27" r="1.5" fill="rgba(147,197,253,0.25)"><animateTransform attributeName="transform" type="rotate" from="360 72 72" to="0 72 72" dur="25s" repeatCount="indefinite" /></circle>
|
||||
</svg>
|
||||
)}
|
||||
{colorScheme === "dawn" && (
|
||||
<svg className="w-full h-full" viewBox="0 0 144 144" fill="none">
|
||||
<defs><linearGradient id="dawn-sun" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stopColor="#fbbf24" stopOpacity="0.4" /><stop offset="50%" stopColor="#f59e0b" stopOpacity="0.3" /><stop offset="100%" stopColor="#d97706" stopOpacity="0.2" /></linearGradient></defs>
|
||||
<circle cx="72" cy="72" r="60" stroke="rgba(251,191,36,0.20)" strokeWidth="0.5" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="35s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="45" stroke="rgba(245,158,11,0.15)" strokeWidth="0.5" fill="none" strokeDasharray="6 4"><animateTransform attributeName="transform" type="rotate" from="360 72 72" to="0 72 72" dur="28s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="30" stroke="rgba(251,191,36,0.12)" strokeWidth="0.5" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="22s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="18" fill="url(#dawn-sun)" /><circle cx="72" cy="72" r="10" fill="rgba(251,191,36,0.25)" /><circle cx="72" cy="72" r="4" fill="rgba(253,230,138,0.7)" />
|
||||
<defs><linearGradient id="dawn-sun" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stopColor="#fbbf24" stopOpacity="0.22" /><stop offset="50%" stopColor="#f59e0b" stopOpacity="0.15" /><stop offset="100%" stopColor="#d97706" stopOpacity="0.10" /></linearGradient></defs>
|
||||
<circle cx="72" cy="72" r="60" stroke="rgba(251,191,36,0.12)" strokeWidth="0.5" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="35s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="45" stroke="rgba(245,158,11,0.08)" strokeWidth="0.5" fill="none" strokeDasharray="6 4"><animateTransform attributeName="transform" type="rotate" from="360 72 72" to="0 72 72" dur="28s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="30" stroke="rgba(251,191,36,0.06)" strokeWidth="0.5" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="22s" repeatCount="indefinite" /></circle>
|
||||
<circle cx="72" cy="72" r="18" fill="url(#dawn-sun)" /><circle cx="72" cy="72" r="10" fill="rgba(251,191,36,0.12)" /><circle cx="72" cy="72" r="4" fill="rgba(253,230,138,0.35)" />
|
||||
{[0,45,90,135,180,225,270,315].map(deg => (
|
||||
<line key={deg} x1="72" y1="8" x2="72" y2="22" stroke="rgba(251,191,36,0.3)" strokeWidth="1" strokeLinecap="round" transform={`rotate(${deg} 72 72)`}><animateTransform attributeName="transform" type="rotate" from={`${deg} 72 72`} to={`${deg+360} 72 72`} dur="35s" repeatCount="indefinite" /></line>
|
||||
<line key={deg} x1="72" y1="8" x2="72" y2="22" stroke="rgba(251,191,36,0.15)" strokeWidth="1" strokeLinecap="round" transform={`rotate(${deg} 72 72)`}><animateTransform attributeName="transform" type="rotate" from={`${deg} 72 72`} to={`${deg+360} 72 72`} dur="35s" repeatCount="indefinite" /></line>
|
||||
))}
|
||||
</svg>
|
||||
)}
|
||||
{(colorScheme === "aurora" || colorScheme === "forest" || colorScheme === "crimson") && (
|
||||
{(colorScheme === "aurora" || colorScheme === "forest" || colorScheme === "crimson" || colorScheme === "orange") && (
|
||||
<svg className="w-full h-full" viewBox="0 0 144 144" fill="none">
|
||||
<defs><linearGradient id="orb-grad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stopColor={scheme.highlight} stopOpacity="0.3" /><stop offset="50%" stopColor={scheme.highlight} stopOpacity="0.15" /><stop offset="100%" stopColor={scheme.highlight} stopOpacity="0.25" /></linearGradient></defs>
|
||||
<circle cx="72" cy="72" r="55" stroke="url(#orb-grad)" strokeWidth="1" fill="none"><animateTransform attributeName="transform" type="rotate" from="0 72 72" to="360 72 72" dur="40s" repeatCount="indefinite" /></circle>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const toolImportSchema = z.object({
|
||||
pricingDetail: z.string().optional(),
|
||||
features: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})).min(1, "tools 数组不能为空"),
|
||||
})).min(1, "tools 数组不能为空").max(100, "一次最多导入100条"),
|
||||
categoryId: z.number().int().positive("categoryId 必须为正整数"),
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ export const skillImportSchema = z.object({
|
||||
rating: z.number().min(0).max(5).optional(),
|
||||
features: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})).min(1, "skills 数组不能为空"),
|
||||
})).min(1, "skills 数组不能为空").max(100, "一次最多导入100条"),
|
||||
categoryId: z.number().int().positive("categoryId 必须为正整数"),
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ export const newsImportSchema = z.object({
|
||||
category: z.string().optional(),
|
||||
sourceUrl: z.string().optional(),
|
||||
hotScore: z.number().min(0).max(100).optional(),
|
||||
})).min(1, "items 数组不能为空"),
|
||||
})).min(1, "items 数组不能为空").max(50, "一次最多导入50条"),
|
||||
});
|
||||
|
||||
export const checkTypeSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user