feat: add tutorial video button and ranking visibility toggle in navbar settings
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c44c8cff6a
commit
024ec19c70
@ -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}
|
||||
/>
|
||||
<div className="flex-1 pt-16">{children}</div>
|
||||
</div>
|
||||
|
||||
@ -159,6 +159,9 @@ type SiteSettings = {
|
||||
toolbox1Enabled: boolean;
|
||||
toolbox2Name: string;
|
||||
toolbox2Enabled: boolean;
|
||||
tutorialVideoUrl: string | null;
|
||||
tutorialEnabled: boolean;
|
||||
rankingEnabled: boolean;
|
||||
};
|
||||
|
||||
type NavLink = {
|
||||
@ -357,6 +360,9 @@ export function AdminSettingsClient() {
|
||||
toolbox1Enabled: true,
|
||||
toolbox2Name: "工具箱2",
|
||||
toolbox2Enabled: false,
|
||||
tutorialVideoUrl: null,
|
||||
tutorialEnabled: false,
|
||||
rankingEnabled: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -379,6 +385,9 @@ export function AdminSettingsClient() {
|
||||
toolbox1Enabled: siteSettingsData.toolbox1Enabled ?? true,
|
||||
toolbox2Name: siteSettingsData.toolbox2Name || "工具箱2",
|
||||
toolbox2Enabled: siteSettingsData.toolbox2Enabled ?? false,
|
||||
tutorialVideoUrl: siteSettingsData.tutorialVideoUrl || null,
|
||||
tutorialEnabled: siteSettingsData.tutorialEnabled ?? false,
|
||||
rankingEnabled: siteSettingsData.rankingEnabled ?? true,
|
||||
});
|
||||
}
|
||||
}, [siteSettingsData]);
|
||||
@ -2537,8 +2546,51 @@ export function AdminSettingsClient() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 精选榜设置 */}
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label className="text-base">精选榜</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
控制是否在导航栏显示精选榜按钮
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={siteSettings.rankingEnabled}
|
||||
onCheckedChange={(checked) => setSiteSettings({ ...siteSettings, rankingEnabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 功能教程设置 */}
|
||||
<div className="rounded-lg border p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label className="text-base">功能教程</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
启用后在导航栏显示功能教程按钮,点击后弹窗播放视频
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={siteSettings.tutorialEnabled}
|
||||
onCheckedChange={(checked) => setSiteSettings({ ...siteSettings, tutorialEnabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
{siteSettings.tutorialEnabled && (
|
||||
<div className="space-y-2">
|
||||
<Label>视频链接</Label>
|
||||
<Input
|
||||
value={siteSettings.tutorialVideoUrl || ""}
|
||||
onChange={(e) => setSiteSettings({ ...siteSettings, tutorialVideoUrl: e.target.value || null })}
|
||||
placeholder="输入视频播放链接,支持 MP4、YouTube、Bilibili 等"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
支持直接视频链接(如 .mp4)或嵌入式播放链接
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSaveSiteSettings}>
|
||||
<Save className="mr-2 h-4 w-4" /> 保存工具箱设置
|
||||
<Save className="mr-2 h-4 w-4" /> 保存导航设置
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -51,6 +51,9 @@ async function SidebarWrapper({ 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}
|
||||
/>
|
||||
<div className="pt-14 flex h-full w-full overflow-hidden"> {/* Add padding-top for fixed navbar */}
|
||||
<AppSidebar user={session?.user} />
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
<div className="pt-12 h-full">
|
||||
{children}
|
||||
|
||||
@ -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<RankingItem[]>([]);
|
||||
const [rankingOpen, setRankingOpen] = useState(false);
|
||||
const [showTutorialModal, setShowTutorialModal] = useState(false);
|
||||
const [showImageEditor, setShowImageEditor] = useState(false);
|
||||
const [editorImageUrl, setEditorImageUrl] = useState<string>("");
|
||||
// 移除 fileInputRef,不再需要外部上传
|
||||
@ -242,60 +249,77 @@ export function TopNavbar({
|
||||
<span>AI精选</span>
|
||||
</a>
|
||||
|
||||
{/* 功能教程 */}
|
||||
{tutorialEnabled && tutorialVideoUrl && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
|
||||
onClick={() => setShowTutorialModal(true)}
|
||||
type="button"
|
||||
title="功能教程"
|
||||
>
|
||||
<PlayCircle className="h-4 w-4 text-green-400" />
|
||||
<span className="text-sm font-medium">功能教程</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 精选榜单 */}
|
||||
<DropdownMenu open={rankingOpen} onOpenChange={setRankingOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
|
||||
type="button"
|
||||
>
|
||||
<Trophy className="h-4 w-4 text-orange-400" />
|
||||
<span className="text-sm font-medium">精选榜</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[280px] p-3">
|
||||
<div className="flex items-center gap-2 mb-3 pb-2 border-b">
|
||||
<Trophy className="h-5 w-5 text-orange-500" />
|
||||
<span className="font-semibold">精选榜单 TOP5</span>
|
||||
</div>
|
||||
{ranking.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4 text-sm">
|
||||
暂无精选数据
|
||||
{rankingEnabled && (
|
||||
<DropdownMenu open={rankingOpen} onOpenChange={setRankingOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
|
||||
type="button"
|
||||
>
|
||||
<Trophy className="h-4 w-4 text-orange-400" />
|
||||
<span className="text-sm font-medium">精选榜</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[280px] p-3">
|
||||
<div className="flex items-center gap-2 mb-3 pb-2 border-b">
|
||||
<Trophy className="h-5 w-5 text-orange-500" />
|
||||
<span className="font-semibold">精选榜单 TOP5</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{ranking.map((item) => (
|
||||
<div
|
||||
key={item.userId}
|
||||
className={`flex items-center gap-3 p-2 rounded-lg transition-colors ${
|
||||
item.rank === 1 ? "bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20" :
|
||||
item.rank === 2 ? "bg-gray-50 dark:bg-gray-800/50" :
|
||||
item.rank === 3 ? "bg-amber-50 dark:bg-amber-900/20" :
|
||||
"hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center w-6">
|
||||
{getRankIcon(item.rank)}
|
||||
{ranking.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4 text-sm">
|
||||
暂无精选数据
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{ranking.map((item) => (
|
||||
<div
|
||||
key={item.userId}
|
||||
className={`flex items-center gap-3 p-2 rounded-lg transition-colors ${
|
||||
item.rank === 1 ? "bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20" :
|
||||
item.rank === 2 ? "bg-gray-50 dark:bg-gray-800/50" :
|
||||
item.rank === 3 ? "bg-amber-50 dark:bg-amber-900/20" :
|
||||
"hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center w-6">
|
||||
{getRankIcon(item.rank)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm truncate">{item.userName}</p>
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1 truncate">
|
||||
<Building2 className="h-3 w-3" />
|
||||
{item.department}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-orange-500">{item.featuredCount}</span>
|
||||
<span className="text-xs text-muted-foreground ml-1">精选</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm truncate">{item.userName}</p>
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1 truncate">
|
||||
<Building2 className="h-3 w-3" />
|
||||
{item.department}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold text-orange-500">{item.featuredCount}</span>
|
||||
<span className="text-xs text-muted-foreground ml-1">精选</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* 图片编辑 */}
|
||||
<Button
|
||||
@ -367,6 +391,56 @@ export function TopNavbar({
|
||||
onCancel={handleImageEditorCancel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 功能教程弹窗 */}
|
||||
{showTutorialModal && tutorialVideoUrl && (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80"
|
||||
onClick={() => setShowTutorialModal(false)}
|
||||
>
|
||||
<div
|
||||
className="relative w-full max-w-4xl mx-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 关闭按钮 */}
|
||||
<button
|
||||
onClick={() => setShowTutorialModal(false)}
|
||||
className="absolute -top-12 right-0 text-white hover:text-gray-300 transition-colors"
|
||||
title="关闭"
|
||||
>
|
||||
<X className="h-8 w-8" />
|
||||
</button>
|
||||
|
||||
{/* 视频播放器 */}
|
||||
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
|
||||
{tutorialVideoUrl.includes('youtube.com') || tutorialVideoUrl.includes('youtu.be') ? (
|
||||
// YouTube 嵌入
|
||||
<iframe
|
||||
src={tutorialVideoUrl.replace('watch?v=', 'embed/').replace('youtu.be/', 'youtube.com/embed/')}
|
||||
className="w-full h-full"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
) : tutorialVideoUrl.includes('bilibili.com') ? (
|
||||
// Bilibili 嵌入
|
||||
<iframe
|
||||
src={tutorialVideoUrl.replace('www.bilibili.com/video/', 'player.bilibili.com/player.html?bvid=')}
|
||||
className="w-full h-full"
|
||||
allowFullScreen
|
||||
/>
|
||||
) : (
|
||||
// 普通视频链接
|
||||
<video
|
||||
src={tutorialVideoUrl}
|
||||
className="w-full h-full"
|
||||
controls
|
||||
autoPlay
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user