diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx
index 14e1523b1..28b38f801 100644
--- a/app/(admin)/layout.tsx
+++ b/app/(admin)/layout.tsx
@@ -31,6 +31,9 @@ export default async function AdminLayout({
toolbox1Enabled={siteSettings.toolbox1Enabled ?? true}
toolbox2Name={siteSettings.toolbox2Name || "工具箱2"}
toolbox2Enabled={siteSettings.toolbox2Enabled ?? false}
+ tutorialVideoUrl={siteSettings.tutorialVideoUrl}
+ tutorialEnabled={siteSettings.tutorialEnabled ?? false}
+ rankingEnabled={siteSettings.rankingEnabled ?? true}
/>
{/* Add padding-top for fixed navbar */}
diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts
index bda162754..a9faae495 100644
--- a/app/api/site-settings/route.ts
+++ b/app/api/site-settings/route.ts
@@ -38,6 +38,9 @@ export async function GET() {
toolbox1Enabled: true,
toolbox2Name: "工具箱2",
toolbox2Enabled: false,
+ tutorialVideoUrl: null,
+ tutorialEnabled: false,
+ rankingEnabled: true,
};
const response = NextResponse.json({
@@ -78,6 +81,9 @@ const updateSchema = z.object({
toolbox1Enabled: z.boolean().optional(),
toolbox2Name: z.string().max(64).optional(),
toolbox2Enabled: z.boolean().optional(),
+ tutorialVideoUrl: z.string().nullable().optional(),
+ tutorialEnabled: z.boolean().optional(),
+ rankingEnabled: z.boolean().optional(),
});
// PUT /api/site-settings - 更新网站设置(仅管理员)
@@ -134,6 +140,9 @@ export async function PUT(request: NextRequest) {
toolbox1Enabled: data.toolbox1Enabled ?? true,
toolbox2Name: data.toolbox2Name ?? "工具箱2",
toolbox2Enabled: data.toolbox2Enabled ?? false,
+ tutorialVideoUrl: data.tutorialVideoUrl,
+ tutorialEnabled: data.tutorialEnabled ?? false,
+ rankingEnabled: data.rankingEnabled ?? true,
updatedBy: session.user.id,
})
.returning();
diff --git a/app/canvas/[id]/layout.tsx b/app/canvas/[id]/layout.tsx
index cdb8e28e1..f57435046 100644
--- a/app/canvas/[id]/layout.tsx
+++ b/app/canvas/[id]/layout.tsx
@@ -47,6 +47,9 @@ async function CanvasLayoutWrapper({ children }: { children: React.ReactNode })
toolbox1Enabled={siteSettings.toolbox1Enabled ?? true}
toolbox2Name={siteSettings.toolbox2Name || "工具箱2"}
toolbox2Enabled={siteSettings.toolbox2Enabled ?? false}
+ tutorialVideoUrl={siteSettings.tutorialVideoUrl}
+ tutorialEnabled={siteSettings.tutorialEnabled ?? false}
+ rankingEnabled={siteSettings.rankingEnabled ?? true}
/>
{children}
diff --git a/components/top-navbar.tsx b/components/top-navbar.tsx
index 9943aa9d7..ae37db574 100644
--- a/components/top-navbar.tsx
+++ b/components/top-navbar.tsx
@@ -2,7 +2,7 @@
import { useRouter } from "next/navigation";
import Link from "next/link";
-import { Settings, ExternalLink, Briefcase, Images, Sparkles, Trophy, Building2, Crown, Medal, Award, Edit, LayoutGrid } from "lucide-react";
+import { Settings, ExternalLink, Briefcase, Images, Sparkles, Trophy, Building2, Crown, Medal, Award, Edit, LayoutGrid, PlayCircle, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTheme } from "next-themes";
import { useState, useEffect, useRef } from "react";
@@ -40,6 +40,9 @@ type TopNavbarProps = {
toolbox1Enabled?: boolean;
toolbox2Name?: string;
toolbox2Enabled?: boolean;
+ tutorialVideoUrl?: string | null;
+ tutorialEnabled?: boolean;
+ rankingEnabled?: boolean;
};
export function TopNavbar({
@@ -52,12 +55,16 @@ export function TopNavbar({
toolbox1Enabled = true,
toolbox2Name = "工具箱2",
toolbox2Enabled = false,
+ tutorialVideoUrl,
+ tutorialEnabled = false,
+ rankingEnabled = true,
}: TopNavbarProps) {
const router = useRouter();
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const [ranking, setRanking] = useState
([]);
const [rankingOpen, setRankingOpen] = useState(false);
+ const [showTutorialModal, setShowTutorialModal] = useState(false);
const [showImageEditor, setShowImageEditor] = useState(false);
const [editorImageUrl, setEditorImageUrl] = useState("");
// 移除 fileInputRef,不再需要外部上传
@@ -242,60 +249,77 @@ export function TopNavbar({
AI精选
+ {/* 功能教程 */}
+ {tutorialEnabled && tutorialVideoUrl && (
+
+ )}
+
{/* 精选榜单 */}
-
-
-
-
-
-
-
- 精选榜单 TOP5
-
- {ranking.length === 0 ? (
-
- 暂无精选数据
+ {rankingEnabled && (
+
+
+
+
+
+
+
+ 精选榜单 TOP5
- ) : (
-
- {ranking.map((item) => (
-
-
- {getRankIcon(item.rank)}
+ {ranking.length === 0 ? (
+
+ 暂无精选数据
+
+ ) : (
+
+ {ranking.map((item) => (
+
+
+ {getRankIcon(item.rank)}
+
+
+
{item.userName}
+
+
+ {item.department}
+
+
+
+ {item.featuredCount}
+ 精选
+
-
-
{item.userName}
-
-
- {item.department}
-
-
-
- {item.featuredCount}
- 精选
-
-
- ))}
-
- )}
-
-
+ ))}
+
+ )}
+
+
+ )}
{/* 图片编辑 */}
)}
+
+ {/* 功能教程弹窗 */}
+ {showTutorialModal && tutorialVideoUrl && (
+
setShowTutorialModal(false)}
+ >
+
e.stopPropagation()}
+ >
+ {/* 关闭按钮 */}
+
+
+ {/* 视频播放器 */}
+
+ {tutorialVideoUrl.includes('youtube.com') || tutorialVideoUrl.includes('youtu.be') ? (
+ // YouTube 嵌入
+
+ ) : tutorialVideoUrl.includes('bilibili.com') ? (
+ // Bilibili 嵌入
+
+ ) : (
+ // 普通视频链接
+
+ )}
+
+
+
+ )}
);
}
diff --git a/lib/db/queries.ts b/lib/db/queries.ts
index 2c5a273d7..1683d5b54 100644
--- a/lib/db/queries.ts
+++ b/lib/db/queries.ts
@@ -1521,6 +1521,9 @@ export async function getSiteSettings() {
toolbox1Enabled: true,
toolbox2Name: "工具箱2",
toolbox2Enabled: false,
+ tutorialVideoUrl: null,
+ tutorialEnabled: false,
+ rankingEnabled: true,
};
try {
const [settings] = await db.select().from(siteSettings).limit(1);
diff --git a/lib/db/schema.ts b/lib/db/schema.ts
index 505711b24..3d34381dc 100644
--- a/lib/db/schema.ts
+++ b/lib/db/schema.ts
@@ -301,6 +301,11 @@ export const siteSettings = pgTable("SiteSettings", {
toolbox1Enabled: boolean("toolbox1Enabled").notNull().default(true), // 是否显示工具箱1
toolbox2Name: varchar("toolbox2Name", { length: 64 }).default("工具箱2"), // 工具箱2名称
toolbox2Enabled: boolean("toolbox2Enabled").notNull().default(false), // 是否显示工具箱2
+ // 功能教程设置
+ tutorialVideoUrl: text("tutorialVideoUrl"), // 功能教程视频链接
+ tutorialEnabled: boolean("tutorialEnabled").notNull().default(false), // 是否显示功能教程按钮
+ // 精选榜设置
+ rankingEnabled: boolean("rankingEnabled").notNull().default(true), // 是否显示精选榜按钮
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
updatedBy: uuid("updatedBy").references(() => user.id),
});